Skip to content

Commit a8cdc4f

Browse files
Merge pull request #403 from NexonTreeHouse/feat/seperator-component
feat(headless): separator component
2 parents eaf872d + 83cbf0c commit a8cdc4f

File tree

8 files changed

+228
-1
lines changed

8 files changed

+228
-1
lines changed

apps/website/src/_state/component-statuses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const statusByComponent: ComponentKitsStatuses = {
4444
Combobox: ComponentStatus.Draft,
4545
Popover: ComponentStatus.Draft,
4646
Select: ComponentStatus.Draft,
47+
Separator: ComponentStatus.Beta,
4748
Tabs: ComponentStatus.Beta,
4849
Toggle: ComponentStatus.Planned,
4950
Tooltip: ComponentStatus.Draft

apps/website/src/routes/docs/_components/status-banner/status-banner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function getMessageByStatus(status?: ComponentStatus) {
1717
case ComponentStatus.Ready:
1818
return (
1919
<>
20-
This component is <strong>Production Readty</strong>
20+
This component is <strong>Production Ready</strong>
2121
</>
2222
);
2323
case ComponentStatus.Beta:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { component$, Slot } from '@builder.io/qwik';
2+
import { Separator } from '@qwik-ui/headless';
3+
import { PreviewCodeExample } from '../../../_components/preview-code-example/preview-code-example';
4+
5+
export const MainExample = component$(() => {
6+
return (
7+
<PreviewCodeExample>
8+
<div q:slot="actualComponent">
9+
<div>
10+
<h1 class="text-lg">Qwik UI Headless</h1>
11+
<p class="text-sm">Accessible, Unstyled Qwik UI Components</p>
12+
</div>
13+
<Separator orientation="horizontal" class="h-px my-1 bg-primary" />
14+
<menu class="flex gap-2">
15+
<li>
16+
<a href="/docs/headless/introduction/">Introduction</a>
17+
</li>
18+
<Separator orientation="vertical" class="w-px mx-1 bg-primary" />
19+
<li>
20+
<a href="/docs/headless/install/">Installation</a>
21+
</li>
22+
<Separator orientation="vertical" class="w-px mx-1 bg-primary" />
23+
<li>
24+
<a href="/docs/headless/contributing/">Contributing</a>
25+
</li>
26+
</menu>
27+
</div>
28+
<div q:slot="codeExample">
29+
<Slot />
30+
</div>
31+
</PreviewCodeExample>
32+
);
33+
});
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: Qwik UI | Separator
3+
---
4+
import { StatusBanner } from '../../../_components/status-banner/status-banner';
5+
import {statusByComponent} from '../../../../../_state/component-statuses';
6+
import { MainExample } from './examples';
7+
import { APITable } from '../../../_components/api-table/api-table';
8+
9+
<StatusBanner status={statusByComponent.headless.Separator}/>
10+
11+
# Separator
12+
13+
#### A separator separates and distinguishes sections of content or groups of menuitems
14+
15+
<MainExample>
16+
```tsx
17+
<div>
18+
<div>
19+
<h1 class="text-lg">Qwik UI Headless</h1>
20+
<p class="text-sm">Accessible, Unstyled Qwik UI Components</p>
21+
</div>
22+
<Separator orientation="horizontal" class="h-px my-1 bg-primary" />
23+
<menu class="flex gap-2">
24+
<li>
25+
<a href="/docs/headless/introduction/">Introduction</a>
26+
</li>
27+
<Separator orientation="vertical" class="w-px mx-1 bg-primary" />
28+
<li>
29+
<a href="/docs/headless/install/">Installation</a>
30+
</li>
31+
<Separator orientation="vertical" class="w-px mx-1 bg-primary" />
32+
<li>
33+
<a href="/docs/headless/contributing/">Contributing</a>
34+
</li>
35+
</menu>
36+
</div>
37+
```
38+
</MainExample>
39+
40+
Qwik UI's Separator implementation follows [WAI-Aria separator role requirements](https://www.w3.org/TR/wai-aria-1.2/#separator). This separator is static and should not be used for interactivity.
41+
42+
##### ✨ Features
43+
- Supports both horizontal and vertical orientation
44+
45+
<div class="mb-6 flex flex-col gap-2">
46+
[View Source Code ↗️](https://github.com/qwikifiers/qwik-ui/tree/main/packages/kit-headless/src/components/separator)
47+
48+
[Report an issue 🚨](https://github.com/qwikifiers/qwik-ui/issues)
49+
50+
[Edit This Page 🗒️](<https://github.com/qwikifiers/qwik-ui/edit/main/apps/website/src/routes/docs/headless/(components)/separator/index.mdx>)
51+
52+
</div>
53+
54+
## API
55+
<APITable
56+
propDescriptors={[
57+
{
58+
name: "orientation",
59+
type: "enum",
60+
info: '"horizontal" | "vertical"',
61+
description:
62+
"The orientation of the separator. Defaults to horizontal.",
63+
},
64+
{
65+
name: "decorative",
66+
type: "boolean",
67+
description:
68+
"Indicates that the element is purely decorative and should not be included in the accessibility tree.",
69+
},
70+
]}
71+
/>

apps/website/src/routes/docs/headless/menu.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Combobox](/docs/headless/combobox)
1313
- [Popover](/docs/headless/popover)
1414
- [Select](/docs/headless/select)
15+
- [Separator](/docs/headless/separator)
1516
- [Tabs](/docs/headless/tabs)
1617
- [Toggle](/docs/headless/toggle)
1718
- [Tooltip](/docs/headless/tooltip)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Separator } from './separator';
2+
3+
describe('Critical Functionality', () => {
4+
it('INIT', () => {
5+
cy.mount(<Separator />);
6+
7+
cy.checkA11yForComponent();
8+
});
9+
10+
it('GIVEN no orientation prop THEN aria-orientation is set unset', () => {
11+
cy.mount(<Separator />);
12+
13+
cy.findByRole('separator').should('not.have.attr', 'aria-orientation');
14+
});
15+
16+
it("GIVEN orientation prop 'horizontal' THEN aria-orientation is unset", () => {
17+
cy.mount(<Separator orientation="horizontal" />);
18+
19+
cy.findByRole('separator').should('not.have.attr', 'aria-orientation');
20+
});
21+
22+
it("GIVEN orientation prop 'vertical' THEN aria-orientation is set to 'vertical'", () => {
23+
cy.mount(<Separator orientation="vertical" />);
24+
25+
cy.findByRole('separator').should('have.attr', 'aria-orientation', 'vertical');
26+
});
27+
28+
it("GIVEN no orientation prop THEN data-orientation is set to 'horizontal'", () => {
29+
cy.mount(<Separator />);
30+
31+
cy.findByRole('separator').should('not.have.attr', 'aria-orientation');
32+
});
33+
34+
it("GIVEN orientation prop 'horizontal' THEN data-orientation is set to 'horizontal'", () => {
35+
cy.mount(<Separator orientation="horizontal" />);
36+
37+
cy.findByRole('separator').should('not.have.attr', 'aria-orientation');
38+
});
39+
40+
it("GIVEN orientation prop 'vertical' THEN data-orientation is set to 'vertical'", () => {
41+
cy.mount(<Separator orientation="vertical" />);
42+
43+
cy.findByRole('separator').should('have.attr', 'aria-orientation', 'vertical');
44+
});
45+
46+
it("GIVEN decorative prop THEN role is set to 'none' AND aria-orientation is unset", () => {
47+
cy.mount(<Separator decorative />);
48+
49+
cy.findByRole('none').should('not.have.attr', 'aria-orientation');
50+
});
51+
52+
it('GIVEN invalid orientation prop THEN console.warn is called', () => {
53+
const consoleSpy = cy.spy(console, 'warn');
54+
cy.mount(<Separator orientation={'invalid' as any} />);
55+
56+
cy.wrap(consoleSpy).should('have.been.calledWithMatch', /Invalid prop 'orientation'/);
57+
});
58+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { QwikIntrinsicElements } from '@builder.io/qwik';
2+
import { component$, useComputed$ } from '@builder.io/qwik';
3+
4+
const ORIENTATIONS = ['horizontal', 'vertical'] as const;
5+
6+
type Orientation = (typeof ORIENTATIONS)[number];
7+
8+
type QwikDiv = QwikIntrinsicElements['div'];
9+
10+
export interface SeparatorProps extends QwikDiv {
11+
/**
12+
* Either `vertical` or `horizontal`. Defaults to `horizontal`.
13+
*/
14+
orientation?: Orientation;
15+
/**
16+
* If true, accessibility-related attributes
17+
* are updated so that that the element is not included in the accessibility tree.
18+
*/
19+
decorative?: boolean;
20+
}
21+
22+
export const Separator = component$(
23+
({
24+
orientation: orientationProp = 'horizontal',
25+
decorative,
26+
...props
27+
}: SeparatorProps) => {
28+
const orientation = useComputed$(() => {
29+
if (ORIENTATIONS.includes(orientationProp)) {
30+
return orientationProp;
31+
}
32+
33+
console.warn(
34+
`Invalid prop 'orientation' of value '${orientationProp}' supplied to 'separator',
35+
expected one of:
36+
- horizontal
37+
- vertical
38+
39+
Defaulting to 'horizontal'.`
40+
);
41+
return 'horizontal';
42+
});
43+
44+
// `aria-orientation` defaults to `horizontal` so we only need it if `orientation` is vertical
45+
const ariaOrientation = useComputed$(() =>
46+
orientation.value === 'vertical' ? orientation.value : undefined
47+
);
48+
49+
const semanticProps = useComputed$(() =>
50+
decorative
51+
? { role: 'none' }
52+
: {
53+
role: 'separator',
54+
'aria-orientation': ariaOrientation.value
55+
}
56+
);
57+
58+
return (
59+
<div data-orientation={orientation.value} {...semanticProps.value} {...props} />
60+
);
61+
}
62+
);

packages/kit-headless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './components/tooltip/tooltip';
2121
export * as Checkbox from './components/checkbox/checkbox';
2222
export * as CheckboxProps from './components/checkbox/checkbox';
2323
export * from './components/select';
24+
export * from './components/separator/separator';
2425
export * from './components/slider';
2526
export * from './components/breadcrumb';
2627
export * from './components/navigation-bar/navigation-bar';

0 commit comments

Comments
 (0)