Skip to content

Commit c921b04

Browse files
committed
refactor(headless/tabs): clearer names
1 parent aa15c83 commit c921b04

File tree

1 file changed

+131
-101
lines changed
  • packages/kit-headless/src/components/tabs

1 file changed

+131
-101
lines changed

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

Lines changed: 131 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -64,49 +64,6 @@ export type TabInfo = {
6464
tabProps: TabProps;
6565
panelProps: TabPanelProps;
6666
};
67-
export const isDisabled = (i: TabInfo) => i.tabProps.disabled;
68-
// Find an enabled tab including the index
69-
export const findEnabledTab = (tabs: TabInfo[], index: number, wrap?: boolean) => {
70-
let info;
71-
for (let i = Math.max(0, index); i < tabs.length; i++) {
72-
info = tabs[i];
73-
if (!isDisabled(info)) {
74-
return info;
75-
}
76-
}
77-
if (wrap) {
78-
for (let i = 0; i < index; i++) {
79-
info = tabs[i];
80-
if (!isDisabled(info)) {
81-
return info;
82-
}
83-
}
84-
}
85-
return;
86-
};
87-
88-
// Find an enabled tab before the index
89-
export const findPrevEnabledTab = (tabs: TabInfo[], index: number, wrap?: boolean) => {
90-
let info;
91-
for (let i = Math.min(tabs.length, index) - 1; i >= 0; i--) {
92-
info = tabs[i];
93-
if (!isDisabled(info)) {
94-
return info;
95-
}
96-
}
97-
if (wrap) {
98-
for (let i = tabs.length - 1; i > index; i--) {
99-
info = tabs[i];
100-
if (!isDisabled(info)) {
101-
return info;
102-
}
103-
}
104-
}
105-
return;
106-
};
107-
108-
export const getEnabledTab = (tabInfoList: TabInfo[], index: number) =>
109-
findEnabledTab(tabInfoList, index) || findPrevEnabledTab(tabInfoList, index);
11067

11168
// This function reads the children, assigns indexes and creates a
11269
// standard structure. It must take care to retain the props objects
@@ -119,7 +76,7 @@ export const Tabs: FunctionComponent<TabsProps> = (props) => {
11976
let tabListElement: JSXNode | undefined;
12077
const tabComponents: JSXNode[] = [];
12178
const panelComponents: JSXNode[] = [];
122-
const tabsInfo: TabInfo[] = [];
79+
const tabInfoList: TabInfo[] = [];
12380
let panelIndex = 0;
12481
let selectedIndex;
12582

@@ -173,7 +130,7 @@ export const Tabs: FunctionComponent<TabsProps> = (props) => {
173130
child.props._extraClass = panelClass;
174131

175132
panelComponents.push(child);
176-
tabsInfo.push({
133+
tabInfoList.push({
177134
tabId,
178135
index: panelIndex,
179136
panelProps: child.props
@@ -195,11 +152,11 @@ export const Tabs: FunctionComponent<TabsProps> = (props) => {
195152
}
196153

197154
tabComponents.forEach((tab, index) => {
198-
const tabId = tabsInfo[index]?.tabId;
155+
const tabId = tabInfoList[index]?.tabId;
199156
tab.key = tabId;
200157
tab.props._tabId = tabId;
201158
tab.props._extraClass = tabClass;
202-
tabsInfo[index].tabProps = tab.props;
159+
tabInfoList[index].tabProps = tab.props;
203160
});
204161

205162
if (tabListElement) {
@@ -215,47 +172,18 @@ export const Tabs: FunctionComponent<TabsProps> = (props) => {
215172
}
216173

217174
return (
218-
<TabsImpl tabs={tabsInfo} {...rest}>
175+
<TabsImpl tabInfoList={tabInfoList} {...rest}>
219176
{tabListElement}
220177
{panelComponents}
221178
</TabsImpl>
222179
);
223180
};
224181

225-
// This helper function is separate so that it doesn't have to be a QRL
226-
// and it doesn't result in race conditions between tasks
227-
// We were seeing tabId signal task running before updateSignals when it was a QRL
228-
export const updateSignals = (
229-
tabs: TabInfo[],
230-
indexSig: Signal<number | undefined>,
231-
tabIdSig: Signal<string | undefined>,
232-
{ idx, tabId }: { idx?: number; tabId?: string },
233-
tryHarder?: boolean
234-
) => {
235-
if (tabId) {
236-
idx = tabs.findIndex((t) => t.tabId === tabId);
237-
}
238-
if (typeof idx !== 'number') return;
239-
if (idx && idx < 0) {
240-
if (!tryHarder) {
241-
return;
242-
}
243-
// given index doesn't exist, find one nearby
244-
idx = indexSig.value;
245-
if (typeof idx !== 'number') return;
246-
}
247-
const tab = getEnabledTab(tabs, idx);
248-
if (tab && (tab.index !== indexSig.value || tab.tabId !== tabIdSig.value)) {
249-
indexSig.value = tab.index;
250-
tabIdSig.value = tab.tabId;
251-
}
252-
};
253-
254-
export const TabsImpl = component$((props: TabsProps & { tabs: TabInfo[] }) => {
182+
export const TabsImpl = component$((props: TabsProps & { tabInfoList: TabInfo[] }) => {
255183
const {
256184
// We take these out of the props for the DOM element but we must refer
257185
// to them as e.g. props.tabs for reactivity
258-
tabs: _0,
186+
tabInfoList: _0,
259187
behavior = 'manual',
260188
selectedTabId: _1,
261189
selectedIndex: _2,
@@ -278,35 +206,49 @@ export const TabsImpl = component$((props: TabsProps & { tabs: TabInfo[] }) => {
278206
useTask$(function syncTabsTask({ track }) {
279207
// Possible optimizer bug: tracking only works with props.tabs
280208
// TODO: Write a test in Qwik optimizer to prove this bug
281-
const tabs = track(() => props.tabs);
209+
const tabInfoList = track(() => props.tabInfoList);
282210
const tabId = selectedTabIdSig.value;
283-
updateSignals(tabs, selectedIndexSig, selectedTabIdSig, { tabId }, true);
211+
syncSelectedStateSignals(
212+
tabInfoList,
213+
selectedIndexSig,
214+
selectedTabIdSig,
215+
{ tabIdToSelect: tabId },
216+
true
217+
);
284218
});
285219
useTask$(function syncPropSelectedIndexTask({ track }) {
286-
const idx = track(() => props.selectedIndex);
287-
updateSignals(props.tabs, selectedIndexSig, selectedTabIdSig, { idx });
220+
const updatedIndexFromProps = track(() => props.selectedIndex);
221+
syncSelectedStateSignals(props.tabInfoList, selectedIndexSig, selectedTabIdSig, {
222+
indexToSelect: updatedIndexFromProps
223+
});
288224
});
289225
useTask$(function syncSelectedIndexSigTask({ track }) {
290-
const idx = track(() => selectedIndexSig.value);
291-
updateSignals(props.tabs, selectedIndexSig, selectedTabIdSig, { idx });
226+
const updatedIndexSignal = track(() => selectedIndexSig.value);
227+
syncSelectedStateSignals(props.tabInfoList, selectedIndexSig, selectedTabIdSig, {
228+
indexToSelect: updatedIndexSignal
229+
});
292230
if (typeof selectedIndexSig.value !== 'undefined') {
293231
onSelectedIndexChange$?.(selectedIndexSig.value);
294232
}
295233
});
296234
useTask$(function syncPropSelectedTabIdTask({ track }) {
297-
const tabId = track(() => props.selectedTabId);
298-
updateSignals(props.tabs, selectedIndexSig, selectedTabIdSig, { tabId });
235+
const updatedTabIdFromProps = track(() => props.selectedTabId);
236+
syncSelectedStateSignals(props.tabInfoList, selectedIndexSig, selectedTabIdSig, {
237+
tabIdToSelect: updatedTabIdFromProps
238+
});
299239
});
300240
useTask$(function syncSelectedTabIdSigTask({ track }) {
301-
let tabId = track(() => selectedTabIdSig.value);
241+
let updatedTabId = track(() => selectedTabIdSig.value);
302242
// If we don't have a tabId by the time this task runs, select the first enabled tab
303-
if (typeof tabId !== 'string') {
304-
const tab = getEnabledTab(props.tabs, 0);
243+
if (typeof updatedTabId !== 'string') {
244+
const tab = getEnabledTab(props.tabInfoList, 0);
305245
if (tab) {
306-
tabId = tab.tabId;
246+
updatedTabId = tab.tabId;
307247
}
308248
}
309-
updateSignals(props.tabs, selectedIndexSig, selectedTabIdSig, { tabId });
249+
syncSelectedStateSignals(props.tabInfoList, selectedIndexSig, selectedTabIdSig, {
250+
tabIdToSelect: updatedTabId
251+
});
310252
if (typeof selectedTabIdSig.value !== 'undefined') {
311253
onSelectedTabIdChange$?.(selectedTabIdSig.value);
312254
}
@@ -319,7 +261,9 @@ export const TabsImpl = component$((props: TabsProps & { tabs: TabInfo[] }) => {
319261
});
320262

321263
const selectTab$ = $((tabId: string) => {
322-
updateSignals(props.tabs, selectedIndexSig, selectedTabIdSig, { tabId });
264+
syncSelectedStateSignals(props.tabInfoList, selectedIndexSig, selectedTabIdSig, {
265+
tabIdToSelect: tabId
266+
});
323267
});
324268

325269
const selectIfAutomatic$ = $((tabId: string) => {
@@ -331,28 +275,28 @@ export const TabsImpl = component$((props: TabsProps & { tabs: TabInfo[] }) => {
331275
const onTabKeyDown$ = $((key: KeyCode, currentTabId: string) => {
332276
const tabsRootElement = ref.value;
333277

334-
const currentFocusedTabIndex = props.tabs.findIndex(
278+
const currentFocusedTabIndex = props.tabInfoList.findIndex(
335279
(tabData) => tabData.tabId === currentTabId
336280
);
337281

338-
let tab;
282+
let tabInfo;
339283
if (
340284
(!vertical && key === KeyCode.ArrowRight) ||
341285
(vertical && key === KeyCode.ArrowDown)
342286
) {
343-
tab = findEnabledTab(props.tabs, currentFocusedTabIndex + 1, true);
287+
tabInfo = findNextEnabledTab(props.tabInfoList, currentFocusedTabIndex + 1, true);
344288
} else if (
345289
(!vertical && key === KeyCode.ArrowLeft) ||
346290
(vertical && key === KeyCode.ArrowUp)
347291
) {
348-
tab = findPrevEnabledTab(props.tabs, currentFocusedTabIndex, true);
292+
tabInfo = findPrevEnabledTab(props.tabInfoList, currentFocusedTabIndex, true);
349293
} else if (key === KeyCode.Home || key === KeyCode.PageUp) {
350-
tab = findEnabledTab(props.tabs, 0);
294+
tabInfo = findNextEnabledTab(props.tabInfoList, 0);
351295
} else if (key === KeyCode.End || key === KeyCode.PageDown) {
352-
tab = findPrevEnabledTab(props.tabs, props.tabs.length);
296+
tabInfo = findPrevEnabledTab(props.tabInfoList, props.tabInfoList.length);
353297
}
354-
if (tab) {
355-
focusOnTab(tab.index);
298+
if (tabInfo) {
299+
focusOnTab(tabInfo.index);
356300
}
357301

358302
function focusOnTab(index: number) {
@@ -379,3 +323,89 @@ export const TabsImpl = component$((props: TabsProps & { tabs: TabInfo[] }) => {
379323
</div>
380324
);
381325
});
326+
327+
// This helper function is separate so that it doesn't have to be a QRL
328+
// and it doesn't result in race conditions between tasks
329+
// We were seeing tabId signal task running before updateSignals when it was a QRL
330+
export const syncSelectedStateSignals = (
331+
tabsInfoList: TabInfo[],
332+
selectedIndexSig: Signal<number | undefined>,
333+
selectedTabIdSig: Signal<string | undefined>,
334+
{ indexToSelect, tabIdToSelect }: { indexToSelect?: number; tabIdToSelect?: string },
335+
ignoreIndexNotFound?: boolean
336+
) => {
337+
if (tabIdToSelect) {
338+
indexToSelect = tabsInfoList.findIndex((tabInfo) => tabInfo.tabId === tabIdToSelect);
339+
}
340+
if (typeof indexToSelect !== 'number') return;
341+
342+
if (indexToSelect && indexToSelect < 0) {
343+
if (!ignoreIndexNotFound) {
344+
return;
345+
}
346+
// given index doesn't exist, find one nearby
347+
indexToSelect = selectedIndexSig.value;
348+
if (typeof indexToSelect !== 'number') return;
349+
}
350+
const tab = getEnabledTab(tabsInfoList, indexToSelect);
351+
if (
352+
tab &&
353+
(tab.index !== selectedIndexSig.value || tab.tabId !== selectedTabIdSig.value)
354+
) {
355+
selectedIndexSig.value = tab.index;
356+
selectedTabIdSig.value = tab.tabId;
357+
}
358+
};
359+
360+
export const getEnabledTab = (tabInfoList: TabInfo[], index: number) =>
361+
findNextEnabledTab(tabInfoList, index) || findPrevEnabledTab(tabInfoList, index);
362+
363+
// Find an enabled tab including the index
364+
export const findNextEnabledTab = (
365+
tabsInfo: TabInfo[],
366+
index: number,
367+
wrap?: boolean
368+
) => {
369+
let info;
370+
for (let i = Math.max(0, index); i < tabsInfo.length; i++) {
371+
info = tabsInfo[i];
372+
if (!isDisabled(info)) {
373+
return info;
374+
}
375+
}
376+
if (wrap) {
377+
for (let i = 0; i < index; i++) {
378+
info = tabsInfo[i];
379+
if (!isDisabled(info)) {
380+
return info;
381+
}
382+
}
383+
}
384+
return;
385+
};
386+
387+
// Find an enabled tab before the index
388+
export const findPrevEnabledTab = (
389+
tabsInfo: TabInfo[],
390+
index: number,
391+
wrap?: boolean
392+
) => {
393+
let info;
394+
for (let i = Math.min(tabsInfo.length, index) - 1; i >= 0; i--) {
395+
info = tabsInfo[i];
396+
if (!isDisabled(info)) {
397+
return info;
398+
}
399+
}
400+
if (wrap) {
401+
for (let i = tabsInfo.length - 1; i > index; i--) {
402+
info = tabsInfo[i];
403+
if (!isDisabled(info)) {
404+
return info;
405+
}
406+
}
407+
}
408+
return;
409+
};
410+
411+
export const isDisabled = (tabInfo: TabInfo) => tabInfo.tabProps.disabled;

0 commit comments

Comments
 (0)