Skip to content

Commit cb9ecff

Browse files
shairezadamgen
andcommitted
test(headless): add tabs stories and todolist
Co-authored-by: Adam <[email protected]>
1 parent ba8ae7f commit cb9ecff

File tree

4 files changed

+85
-12
lines changed

4 files changed

+85
-12
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"commit": "git-cz",
1212
"format:fix": "pretty-quick --staged",
1313
"prepare": "husky install",
14-
"test:headless": "nx component-test headless --watch"
14+
"test:headless": "nx storybook headless",
15+
"website": "nx serve website"
1516
},
1617
"devDependencies": {
1718
"@builder.io/qwik": "0.101.0",

packages/kit-headless/src/components/accordion/accordion.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const Primary: Story = {
1515
render: () => (
1616
<Accordion>
1717
<AccordionItem label="Item 1">This is a test</AccordionItem>
18+
<AccordionItem label="Item 2">This is a test 2</AccordionItem>
1819
</Accordion>
1920
),
2021
play: async ({ canvasElement }) => {
@@ -23,5 +24,7 @@ export const Primary: Story = {
2324
await userEvent.click(canvas.getByText('Item 1'));
2425

2526
await expect(canvas.getByText('This is a test')).toBeInTheDocument();
27+
await userEvent.click(canvas.getByText('Item 2'));
28+
await expect(canvas.getByText('This is a test 2')).toBeInTheDocument();
2629
},
2730
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Meta, StoryObj } from 'storybook-framework-qwik';
2+
import { Tab, TabList, TabPanel, Tabs, TabsProps } from './tabs';
3+
import { userEvent, within } from '@storybook/testing-library';
4+
import { expect } from '@storybook/jest';
5+
6+
const meta: Meta<TabsProps> = {
7+
component: Tabs,
8+
};
9+
10+
export default meta;
11+
12+
type Story = StoryObj<TabsProps>;
13+
14+
export const Primary: Story = {
15+
render: () => (
16+
<Tabs>
17+
<TabList>
18+
<Tab>Tab 1</Tab>
19+
<Tab>Tab 2</Tab>
20+
<Tab>Tab 3</Tab>
21+
</TabList>
22+
23+
<TabPanel>Panel 1</TabPanel>
24+
<TabPanel>Panel 2</TabPanel>
25+
<TabPanel>Panel 3</TabPanel>
26+
</Tabs>
27+
),
28+
play: async ({ canvasElement }) => {
29+
const canvas = within(canvasElement);
30+
31+
const tab2 = canvas.getByRole('tab', { name: 'Tab 2' });
32+
await userEvent.click(tab2);
33+
34+
// const tabPanel2 = canvas.getByRole('tabpanel');
35+
// await expect(tabPanel2).toContain('Panel 2');
36+
},
37+
};

packages/kit-headless/src/components/tabs/tabs.tsx

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,49 @@ import {
1111
useTask$,
1212
useSignal,
1313
useVisibleTask$,
14+
useId,
1415
} from '@builder.io/qwik';
1516

17+
/**
18+
* TABS TODOs
19+
* - Get storybook testing to work
20+
*
21+
* - selectedIndex / default
22+
* - Orientation
23+
* - aria-label https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby
24+
* - NOTE: Radix manually handle the value/id for each tab while we calculate it behind the scenes
25+
* If we end up implementing this, we need to expose a way to set this value in the root
26+
* - keyboard interactions (arrowDown, ARrowRight, ArrowUp, ArrowLeft, Home, End, PageUp, PageDown)
27+
* Support Loop
28+
* - expose selectedIndex in the root
29+
* - onValueChange
30+
* POST V1:
31+
* - RTL
32+
33+
*
34+
* TAB
35+
* Disable
36+
* NOTE: radix / headlessui: expose data-state data-disable data-orientation
37+
* NOTE: Headless UI: explorer the render props
38+
* NOTE: remove tab, switch position
39+
* NOTE: scrolling support? or multiple lines? (probably not for headless but for tailwind / material )
40+
*
41+
* PANEL
42+
*
43+
* aria Tabs Pattern https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
44+
* a11y lint plugin https://www.npmjs.com/package/eslint-plugin-jsx-a11y
45+
*
46+
*/
1647
export type Behavior = 'automatic' | 'manual';
1748

1849
interface TabsContext {
1950
selectedIndex: Signal<number>;
2051
getNextTabIndex: QRL<() => number>;
2152
getNextPanelIndex: QRL<() => number>;
22-
tabsHash: string;
2353
behavior: Behavior;
2454
}
2555

26-
export const tabsContext = createContextId<TabsContext>('tabList');
56+
export const tabsContextId = createContextId<TabsContext>('qui--tabList');
2757

2858
export interface TabsProps {
2959
behavior?: Behavior;
@@ -35,8 +65,6 @@ export const Tabs = component$((props: TabsProps) => {
3565
const lastTabIndex = useSignal(0);
3666
const lastPanelIndex = useSignal(0);
3767

38-
const tabsHash = `${Math.random() * 1000}`;
39-
4068
const getNextTabIndex = $(() => {
4169
return lastTabIndex.value++;
4270
});
@@ -51,11 +79,10 @@ export const Tabs = component$((props: TabsProps) => {
5179
selectedIndex: selected,
5280
getNextTabIndex,
5381
getNextPanelIndex,
54-
tabsHash,
5582
behavior,
5683
};
5784

58-
useContextProvider(tabsContext, contextService);
85+
useContextProvider(tabsContextId, contextService);
5986

6087
return (
6188
<div {...props}>
@@ -89,12 +116,15 @@ interface TabProps {
89116
// Tab button inside of a tab list
90117
export const Tab = component$(
91118
({ selectedClassName, onClick, ...props }: TabProps) => {
92-
const contextService = useContext(tabsContext);
119+
const contextService = useContext(tabsContextId);
93120
const thisTabIndex = useSignal(0);
94121

95-
useTask$(async () => {
122+
useVisibleTask$(async () => {
96123
thisTabIndex.value = await contextService.getNextTabIndex();
124+
console.log('useVisibleTask$', thisTabIndex.value);
97125
});
126+
127+
// TODO: Ask Manu about this 😊
98128
const isSelected = () =>
99129
thisTabIndex.value === contextService.selectedIndex.value;
100130

@@ -106,7 +136,7 @@ export const Tab = component$(
106136

107137
return (
108138
<button
109-
id={`${contextService.tabsHash}-tab-${thisTabIndex.value}`}
139+
id={useId()}
110140
type="button"
111141
role="tab"
112142
onFocus$={selectIfAutomatic}
@@ -136,7 +166,7 @@ interface TabPanelProps {
136166
// Tab Panel implementation
137167
export const TabPanel = component$(({ ...props }: TabPanelProps) => {
138168
const { class: classNames, ...rest } = props;
139-
const contextService = useContext(tabsContext);
169+
const contextService = useContext(tabsContextId);
140170
const thisPanelIndex = useSignal(0);
141171
const isSelected = () =>
142172
thisPanelIndex.value === contextService.selectedIndex.value;
@@ -145,7 +175,7 @@ export const TabPanel = component$(({ ...props }: TabPanelProps) => {
145175
});
146176
return (
147177
<div
148-
id={`${contextService.tabsHash}-tabpanel-${thisPanelIndex}`}
178+
id={useId()}
149179
role="tabpanel"
150180
tabIndex={0}
151181
aria-labelledby={`tab-${thisPanelIndex}`}
@@ -155,6 +185,8 @@ export const TabPanel = component$(({ ...props }: TabPanelProps) => {
155185
style={isSelected() ? 'display: block' : 'display: none'}
156186
{...rest}
157187
>
188+
<p>thisPanelIndex.value: {thisPanelIndex.value} </p>
189+
<p>contextService.selectedIndex: {contextService.selectedIndex} </p>
158190
<Slot />
159191
</div>
160192
);

0 commit comments

Comments
 (0)