Skip to content

Commit a43c87a

Browse files
shaireznaorpeledigalklebanov
committed
feat(headless/tabs): onSelectedIndexChange
Co-authored-by: Naor Peled <[email protected]> Co-authored-by: Igal Klebanov <[email protected]>
1 parent 40a7b26 commit a43c87a

File tree

5 files changed

+104
-79
lines changed

5 files changed

+104
-79
lines changed

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

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
useTask$,
99
$,
1010
useSignal,
11+
useVisibleTask$,
1112
} from '@builder.io/qwik';
1213
import { tabsContextId } from './tabs-context-id';
1314
import { KeyCode } from '../../utils/key-code.type';
@@ -22,15 +23,18 @@ export interface TabProps {
2223

2324
export const Tab = component$((props: TabProps) => {
2425
const contextService = useContext(tabsContextId);
25-
26+
const isSelectedSig = useSignal(false);
2627
const serverAssignedIndexSig = useSignal<number | undefined>(undefined);
27-
const uniqueId = useId();
28+
const matchedTabPanelIdSig = useSignal<string | undefined>(undefined);
29+
const uniqueTabId = useId();
2830

31+
// Index task
2932
useTask$(async ({ cleanup }) => {
3033
if (isServer) {
3134
serverAssignedIndexSig.value =
3235
await contextService.getNextServerAssignedTabIndex$();
3336
}
37+
3438
if (isBrowser) {
3539
contextService.reIndexTabs$();
3640
}
@@ -39,47 +43,40 @@ export const Tab = component$((props: TabProps) => {
3943
});
4044
});
4145

46+
// is selected task
47+
useTask$(async ({ track }) => {
48+
if (isServer) {
49+
isSelectedSig.value = await contextService.isIndexSelected$(
50+
serverAssignedIndexSig.value
51+
);
52+
return;
53+
}
54+
isSelectedSig.value = await track(() =>
55+
contextService.isTabSelected$(uniqueTabId)
56+
);
57+
});
58+
59+
// disabled task
4260
useTask$(({ track }) => {
4361
track(() => props.disabled);
4462

4563
if (props.disabled) {
46-
contextService.updateTabState$();
47-
}
48-
});
49-
50-
const currentTabIndexSig = useComputed$(() => {
51-
if (isServer) {
52-
return serverAssignedIndexSig.value;
64+
contextService.updateTabState$(uniqueTabId, { disabled: true });
5365
}
54-
return;
5566
});
5667

57-
const isSelectedSignalSig = useComputed$(() => {
58-
if (isServer) {
59-
return (
60-
serverAssignedIndexSig.value === contextService.selectedIndexSig.value
61-
);
62-
}
63-
return (
64-
contextService.selectedIndexSig.value ===
65-
contextService.tabsMap[uniqueId]?.index
68+
// matched panel id task
69+
useVisibleTask$(async ({ track }) => {
70+
matchedTabPanelIdSig.value = await track(() =>
71+
contextService.getMatchedPanelId$(uniqueTabId)
6672
);
6773
});
6874

69-
const matchedTabPanelId = useComputed$(
70-
() => contextService.tabsMap[uniqueId]?.tabPanelId
71-
);
72-
7375
const selectTab$ = $(() => {
74-
// TODO: try to move this to the Tabs component
75-
7676
if (props.disabled) {
7777
return;
7878
}
79-
contextService.selectedIndexSig.value =
80-
contextService.tabsMap[uniqueId]?.index || 0;
81-
82-
contextService.selectTab$(uniqueId);
79+
contextService.selectTab$(uniqueTabId);
8380
});
8481

8582
const selectIfAutomatic$ = $(() => {
@@ -90,21 +87,19 @@ export const Tab = component$((props: TabProps) => {
9087

9188
return (
9289
<button
93-
id={'tab-' + uniqueId}
94-
data-tab-id={uniqueId}
90+
id={'tab-' + uniqueTabId}
91+
data-tab-id={uniqueTabId}
9592
type="button"
9693
role="tab"
9794
disabled={props.disabled}
9895
aria-disabled={props.disabled}
9996
onFocus$={selectIfAutomatic$}
10097
onMouseEnter$={selectIfAutomatic$}
101-
aria-selected={isSelectedSignalSig.value}
102-
tabIndex={isSelectedSignalSig.value ? 0 : -1}
103-
aria-controls={'tabpanel-' + matchedTabPanelId.value}
98+
aria-selected={isSelectedSig.value}
99+
tabIndex={isSelectedSig.value ? 0 : -1}
100+
aria-controls={'tabpanel-' + matchedTabPanelIdSig.value}
104101
class={`${
105-
isSelectedSignalSig.value
106-
? `selected ${props.selectedClassName || ''}`
107-
: ''
102+
isSelectedSig.value ? `selected ${props.selectedClassName || ''}` : ''
108103
}${props.class ? ` ${props.class}` : ''}`}
109104
onClick$={() => {
110105
selectTab$();
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1-
import { Signal, QRL } from '@builder.io/qwik';
1+
import { QRL } from '@builder.io/qwik';
22
import { Behavior } from './behavior.type';
33
import { TabInfo } from './tabs';
44
import { KeyCode } from '../../utils/key-code.type';
55

66
export interface TabsContext {
77
selectTab$: QRL<(tabId: string) => void>;
8-
setSelectedIndex$: QRL<(index: number) => void>;
98
getNextServerAssignedTabIndex$: QRL<() => number>;
109
getNextServerAssignedPanelIndex$: QRL<() => number>;
11-
updateTabState$: QRL<(tabIndex: number, state: Partial<TabInfo>) => number>;
10+
updateTabState$: QRL<(tabId: string, state: Partial<TabInfo>) => void>;
1211
reIndexTabs$: QRL<() => void>;
12+
getMatchedPanelId$: QRL<(tabId: string) => string | undefined>;
13+
getMatchedTabId$: QRL<(tabId: string) => string | undefined>;
1314
onTabKeyDown$: QRL<(key: KeyCode, tabId: string) => void>;
14-
selectedIndexSig: Signal<number>;
15-
selectedTabIdSig: Signal<string>;
16-
tabsMap: { [key: string]: TabInfo };
17-
tabPanelsMap: { [key: string]: TabInfo };
15+
isIndexSelected$: QRL<(index?: number) => boolean>;
16+
isTabSelected$: QRL<(tabId: string) => boolean>;
17+
isPanelSelected$: QRL<(panelId: string) => boolean>;
1818
behavior: Behavior;
19-
20-
lastAssignedTabIndexSig: Signal<number>;
21-
lastAssignedPanelIndexSig: Signal<number>;
2219
}

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

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
useId,
55
Slot,
66
useTask$,
7-
useComputed$,
87
useSignal,
8+
useVisibleTask$,
99
} from '@builder.io/qwik';
1010
import { tabsContextId } from './tabs-context-id';
1111
import { isBrowser, isServer } from '@builder.io/qwik/build';
@@ -16,20 +16,17 @@ export interface TabPanelProps {
1616

1717
export const TabPanel = component$(({ ...props }: TabPanelProps) => {
1818
const contextService = useContext(tabsContextId);
19-
19+
const isSelectedSig = useSignal(false);
2020
const serverAssignedIndexSig = useSignal<number | undefined>(undefined);
21+
const matchedTabIdSig = useSignal<string | undefined>(undefined);
2122

2223
const panelUID = useId();
2324

24-
const matchedTabId = useComputed$(
25-
() => contextService.tabPanelsMap[panelUID]?.tabId
26-
);
27-
28-
useTask$(({ cleanup }) => {
25+
// Index task
26+
useTask$(async ({ cleanup }) => {
2927
if (isServer) {
3028
serverAssignedIndexSig.value =
31-
contextService.lastAssignedPanelIndexSig.value;
32-
contextService.lastAssignedPanelIndexSig.value++;
29+
await contextService.getNextServerAssignedPanelIndex$();
3330
}
3431
if (isBrowser) {
3532
contextService.reIndexTabs$();
@@ -39,16 +36,24 @@ export const TabPanel = component$(({ ...props }: TabPanelProps) => {
3936
});
4037
});
4138

42-
const isSelectedSignal = useComputed$(() => {
39+
// matched panel id task
40+
useVisibleTask$(async ({ track }) => {
41+
matchedTabIdSig.value = await track(() =>
42+
contextService.getMatchedTabId$(panelUID)
43+
);
44+
});
45+
46+
// is selected task
47+
useTask$(async ({ track }) => {
4348
if (isServer) {
44-
return (
45-
serverAssignedIndexSig.value === contextService.selectedIndexSig.value
49+
isSelectedSig.value = await contextService.isIndexSelected$(
50+
serverAssignedIndexSig.value
4651
);
52+
return;
4753
}
4854

49-
return (
50-
contextService.selectedIndexSig.value ===
51-
contextService.tabPanelsMap[panelUID]?.index
55+
isSelectedSig.value = await track(() =>
56+
contextService.isPanelSelected$(panelUID)
5257
);
5358
});
5459

@@ -58,12 +63,12 @@ export const TabPanel = component$(({ ...props }: TabPanelProps) => {
5863
id={'tabpanel-' + panelUID}
5964
role="tabpanel"
6065
tabIndex={0}
61-
hidden={isSelectedSignal.value ? (null as unknown as undefined) : true}
62-
aria-labelledby={`tab-${matchedTabId.value}`}
63-
class={`${isSelectedSignal.value ? '' : 'is-hidden'}${
66+
hidden={isSelectedSig.value ? (null as unknown as undefined) : true}
67+
aria-labelledby={`tab-${matchedTabIdSig.value}`}
68+
class={`${isSelectedSig.value ? '' : 'is-hidden'}${
6469
props.class ? ` ${props.class}` : ''
6570
}`}
66-
style={isSelectedSignal.value ? 'display: block' : 'display: none'}
71+
style={isSelectedSig.value ? 'display: block' : 'display: none'}
6772
>
6873
<Slot />
6974
</div>

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { component$, useSignal, useStore, $, QRL } from '@builder.io/qwik';
1+
import { component$, useSignal, useStore, $ } from '@builder.io/qwik';
22
import { Tab } from './tab';
33
import { Tabs } from './tabs';
44
import { TabList } from './tabs-list';
@@ -160,6 +160,7 @@ describe('Tabs', () => {
160160

161161
cy.checkA11yForComponent();
162162
});
163+
163164
it(`GIVEN 3 tabs
164165
WHEN clicking the middle one
165166
THEN render the middle panel`, () => {
@@ -182,7 +183,7 @@ describe('Tabs', () => {
182183
cy.findByRole('tabpanel').should('contain', 'Dynamic Tab 2 Panel');
183184
});
184185

185-
it.only(`GIVEN 3 tabs
186+
it(`GIVEN 3 tabs
186187
WHEN clicking the middle one
187188
THEN onSelectedIndexChange should be called`, () => {
188189
cy.mount(<ThreeTabsComponent />);
@@ -298,6 +299,8 @@ describe('Tabs', () => {
298299

299300
cy.findByRole('button', { name: 'Toggle middle tab disabled' }).click();
300301

302+
cy.findByRole('tab', { name: /Tab 2/i }).should('be.disabled');
303+
301304
cy.findByRole('tab', { name: /Tab 1/i }).type('{rightarrow}');
302305

303306
cy.findByRole('tab', { name: /Tab 3/i }).should('have.focus');

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ export const Tabs = component$((props: TabsProps) => {
6767
selectedIndexSig.value = props.selectedIndex || 0;
6868
});
6969

70+
useTask$(({ track }) => {
71+
track(() => selectedIndexSig.value);
72+
if (props.onSelectedIndexChange$) {
73+
props.onSelectedIndexChange$(selectedIndexSig.value);
74+
}
75+
});
76+
7077
const selectedTabIdSig = useSignal<string>('');
7178
const reIndexTabsSig = useSignal(true);
7279

@@ -80,13 +87,22 @@ export const Tabs = component$((props: TabsProps) => {
8087
reIndexTabsSig.value = true;
8188
});
8289

90+
const getMatchedPanelId$ = $((tabId: string) => {
91+
return tabsMap[tabId]?.tabPanelId;
92+
});
93+
94+
const getMatchedTabId$ = $((panelId: string) => {
95+
return tabPanelsMap[panelId]?.tabId;
96+
});
97+
8398
const selectTab$ = $((tabId: string) => {
8499
selectedTabIdSig.value = tabId;
100+
selectedIndexSig.value = tabsMap[tabId]?.index || 0;
85101
});
86102

87-
const updateTabState$ = $((tabIndex: number, state: Partial<TabInfo>) => {
88-
const prevState = tabPairsList[tabIndex];
89-
tabPairsList[tabIndex] = { ...prevState, ...state };
103+
const updateTabState$ = $((tabId: string, state: Partial<TabInfo>) => {
104+
const prevState = tabsMap[tabId];
105+
tabsMap[tabId] = { ...prevState, ...state };
90106
});
91107

92108
const getNextServerAssignedTabIndex$ = $(() => {
@@ -99,7 +115,17 @@ export const Tabs = component$((props: TabsProps) => {
99115
return lastAssignedPanelIndexSig.value;
100116
});
101117

102-
const setSelectedIndex$ = $((index: number) => {});
118+
const isIndexSelected$ = $((index?: number) => {
119+
return selectedIndexSig.value === index;
120+
});
121+
122+
const isTabSelected$ = $((tabId: string) => {
123+
return selectedIndexSig.value === tabsMap[tabId]?.index;
124+
});
125+
126+
const isPanelSelected$ = $((panelId: string) => {
127+
return selectedIndexSig.value === tabPanelsMap[panelId]?.index;
128+
});
103129

104130
const onTabKeyDown$ = $((key: KeyCode, tabId: string) => {
105131
const tabsRootElement = ref.value;
@@ -156,18 +182,17 @@ export const Tabs = component$((props: TabsProps) => {
156182

157183
const contextService: TabsContext = {
158184
selectTab$,
159-
setSelectedIndex$,
185+
isTabSelected$,
186+
isPanelSelected$,
187+
updateTabState$,
160188
getNextServerAssignedTabIndex$,
161189
getNextServerAssignedPanelIndex$,
190+
getMatchedPanelId$,
191+
getMatchedTabId$,
192+
isIndexSelected$,
162193
reIndexTabs$,
163194
onTabKeyDown$,
164-
selectedTabIdSig,
165-
selectedIndexSig,
166-
tabsMap,
167-
tabPanelsMap,
168195
behavior,
169-
lastAssignedTabIndexSig,
170-
lastAssignedPanelIndexSig,
171196
};
172197

173198
useContextProvider(tabsContextId, contextService);

0 commit comments

Comments
 (0)