Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/mosaic-separator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions packages/mosaic/src/components/separator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Separator } from './separator';
export type { SeparatorProps } from './separator';
33 changes: 33 additions & 0 deletions packages/mosaic/src/components/separator/separator.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as stylex from '@stylexjs/stylex';

import { colorVars, fontFamilyVars, space, typeScaleVars } from '../../tokens.stylex';

export const styles = stylex.create({
plain: {
backgroundColor: colorVars['--cl-color-border'],
height: '1px',
width: '100%',
},
labelled: {
gap: space['4'],
alignItems: 'center',
color: colorVars['--cl-color-foreground-secondary'],
display: 'flex',
fontFamily: fontFamilyVars['--cl-font-family-sans'],
fontSize: typeScaleVars['--cl-text-sm-size'],
lineHeight: typeScaleVars['--cl-text-sm-leading'],
width: '100%',
'::after': {
backgroundColor: colorVars['--cl-color-border'],
content: '""',
flexGrow: 1,
height: '1px',
},
'::before': {
backgroundColor: colorVars['--cl-color-border'],
content: '""',
flexGrow: 1,
height: '1px',
},
},
});
Comment thread
alexcarpenter marked this conversation as resolved.
Resolved
55 changes: 55 additions & 0 deletions packages/mosaic/src/components/separator/separator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as stylex from '@stylexjs/stylex';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, it } from 'vitest';

import { Separator } from './separator';

const atoms = stylex.create({
spaced: { marginTop: '8px' },
});

describe('Mosaic Separator', () => {
it('renders a plain separator without children', () => {
render(<Separator />);
const separator = screen.getByRole('separator');
expect(separator).toHaveClass('cl-separator');
expect(separator).not.toHaveAttribute('data-labelled');
expect(separator).toBeEmptyDOMElement();
});

it('renders a labelled separator with children', () => {
render(<Separator>or</Separator>);
const separator = screen.getByRole('separator');
expect(separator).toHaveTextContent('or');
expect(separator).toHaveAttribute('data-labelled', '');
});

it.each([
['null', null],
['false', false],
['an empty string', ''],
])('renders the plain separator when children is %s', (_, children) => {
render(<Separator>{children}</Separator>);
expect(screen.getByRole('separator')).not.toHaveAttribute('data-labelled');
});

it('merges xstyle atoms after the slot atoms', () => {
render(<Separator xstyle={atoms.spaced} />);
expect(screen.getByRole('separator')).toHaveClass('cl-separator', stylex.props(atoms.spaced).className ?? '');
});

it('renders a custom element via render, keeping the styling contract', () => {
render(<Separator render={<section />}>or</Separator>);
const separator = screen.getByRole('separator');
expect(separator.tagName).toBe('SECTION');
expect(separator).toHaveClass('cl-separator');
expect(separator).toHaveAttribute('data-labelled', '');
});

it('forwards the ref', () => {
const ref = React.createRef<HTMLDivElement>();
render(<Separator ref={ref} />);
expect(ref.current).toBe(screen.getByRole('separator'));
});
});
45 changes: 45 additions & 0 deletions packages/mosaic/src/components/separator/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as stylex from '@stylexjs/stylex';
import React from 'react';

import { useRender } from '../../primitives/utils';
import type { MosaicComponentProps } from '../../props';
import { mergeStyleProps, themeProps } from '../../props';
import { reset } from '../../styles/reset.styles';
import { styles } from './separator.styles';

export type SeparatorProps = MosaicComponentProps<'div'>;

/**
* A horizontal rule between blocks of content. Without children it is a plain 1px line;
* with children it is a labelled divider, with a line on each side of the label.
*
* @example
* <Separator />
*
* @example
* <Separator>or</Separator>
*/
export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(function MosaicSeparator(
{ children, render, xstyle, ...rest },
ref,
) {
const labelled = hasLabel(children);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return useRender({
defaultTagName: 'div',
render,
ref,
props: {
...mergeStyleProps(
themeProps('separator', { labelled }),
stylex.props(reset.base, labelled ? styles.labelled : styles.plain, xstyle),
rest,
),
role: 'separator',
children,
},
});
});

function hasLabel(children: React.ReactNode): boolean {
return children != null && children !== false && children !== '';
}
2 changes: 2 additions & 0 deletions packages/mosaic/src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export type {
} from '../components/table';
export { SegmentedControl } from '../components/segmented-control';
export type { SegmentedControlItemProps, SegmentedControlRootProps } from '../components/segmented-control';
export { Separator } from '../components/separator';
export type { SeparatorProps } from '../components/separator';
export { Tabs } from '../components/tabs';
export type {
TabsListProps,
Expand Down
1 change: 1 addition & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
radio: dynamic(() => import('../stories/radio.mdx')),
reveal: dynamic(() => import('../stories/reveal.component.mdx')),
section: dynamic(() => import('../stories/section.mdx')),
separator: dynamic(() => import('../stories/separator.mdx')),
'segmented-control': dynamic(() => import('../stories/segmented-control.mdx')),
select: dynamic(() => import('../stories/select.component.mdx')),
switch: dynamic(() => import('../stories/switch.mdx')),
Expand Down
12 changes: 12 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ import {
Placeholder as SelectComponentPlaceholder,
} from '../stories/select.component.stories';
import { meta as selectMeta } from '../stories/select.stories';
import {
Default as SeparatorDefault,
Labelled as SeparatorLabelled,
meta as separatorMeta,
} from '../stories/separator.stories';
import {
Elements as SkeletonElements,
HoldSpace as SkeletonHoldSpace,
Expand Down Expand Up @@ -903,6 +908,12 @@ const segmentedControlModule: StoryModule = {
Disabled: SegmentedControlDisabled,
};

const separatorModule: StoryModule = {
meta: separatorMeta,
Default: SeparatorDefault,
Labelled: SeparatorLabelled,
};

const switchModule: StoryModule = {
meta: switchMeta,
Default: SwitchDefault,
Expand Down Expand Up @@ -1179,6 +1190,7 @@ export const registry: StoryModule[] = [
revealModule,
sectionModule,
segmentedControlModule,
separatorModule,
selectComponentModule,
switchModule,
tableModule,
Expand Down
46 changes: 46 additions & 0 deletions packages/swingset/src/stories/separator.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as SeparatorStories from './separator.stories';

# Separator

Separator draws a horizontal line between blocks of content. Without children it is a plain 1px line. With children it is a labelled divider, with a line on each side of the label. It renders a `div` with `role='separator'`; the label is presentational and is not announced by screen readers.

## Example

<Story
name='Default'
storyModule={SeparatorStories}
/>

## Usage

```tsx
import { Separator } from '@clerk/mosaic/components/separator';

<Separator />
<Separator>or</Separator>
```

<Story
name='Labelled'
storyModule={SeparatorStories}
/>

## Parts

| Part | Element | Class | Description |
| ----------- | ------- | --------------- | --------------------------------------------------------------- |
| `Separator` | `div` | `.cl-separator` | The line, or with children the label and a line on either side. |

`Separator` accepts `render` and `xstyle` and forwards its ref.

## Styling

`data-labelled` is set when the separator has children. The labelled lines are the root's `::before` and `::after`. Both forms read `--cl-color-border` for the line; the label reads `--cl-color-foreground-secondary`.

```css
.cl-separator:not([data-labelled]),
.cl-separator::before,
.cl-separator::after {
background-color: var(--cl-color-border-subtle);
}
```
20 changes: 20 additions & 0 deletions packages/swingset/src/stories/separator.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Separator } from '@clerk/mosaic/components/separator';

import type { StoryMeta } from '@/lib/types';

export { default as __source } from './separator.stories?raw';

export const meta: StoryMeta = {
group: 'Components',
status: 'stable',
title: 'Separator',
source: 'packages/mosaic/src/components/separator/separator.tsx',
};

export function Default() {
return <Separator />;
}

export function Labelled() {
return <Separator>or</Separator>;
}
Loading