@workday/canvas-kit-preview-react 16.1.7 → 16.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/color-picker/lib/ColorPicker.js +1 -1
- package/dist/commonjs/color-picker/lib/parts/ColorReset.js +1 -1
- package/dist/commonjs/color-picker/lib/parts/SwatchBook.js +1 -1
- package/dist/commonjs/divider/lib/Divider.js +3 -3
- package/dist/commonjs/loading-sparkles/lib/LoadingSparkles.js +2 -2
- package/dist/commonjs/multi-select/lib/MultiSelectInput.js +3 -3
- package/dist/commonjs/radio/lib/RadioGroup.js +3 -3
- package/dist/commonjs/radio/lib/RadioLabel.js +1 -1
- package/dist/commonjs/radio/lib/RadioText.js +3 -3
- package/dist/commonjs/radio/lib/StyledRadioButton.js +5 -5
- package/dist/commonjs/side-panel/lib/SidePanel.js +7 -7
- package/dist/commonjs/side-panel/lib/SidePanelToggleButton.js +11 -11
- package/dist/commonjs/status-indicator/lib/StatusIndicator.js +14 -14
- package/dist/commonjs/status-indicator/lib/StatusIndicatorLabel.js +1 -1
- package/dist/commonjs/switch/lib/SwitchBackground.js +1 -1
- package/dist/commonjs/switch/lib/SwitchCircle.js +2 -2
- package/dist/commonjs/switch/lib/SwitchContainer.js +1 -1
- package/dist/commonjs/switch/lib/SwitchIcon.js +2 -2
- package/dist/commonjs/switch/lib/SwitchInput.js +3 -3
- package/dist/commonjs/tabs/lib/TabsItem.d.ts +3 -1
- package/dist/commonjs/tabs/lib/TabsItem.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsItem.js +60 -5
- package/dist/commonjs/tabs/lib/TabsList.d.ts +330 -2
- package/dist/commonjs/tabs/lib/TabsList.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsList.js +65 -12
- package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts +24 -215
- package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/TabsOverflowButton.js +71 -6
- package/dist/commonjs/tabs/lib/TabsPanels.js +2 -2
- package/dist/commonjs/tabs/lib/useTabsModel.d.ts +17 -12
- package/dist/commonjs/tabs/lib/useTabsModel.d.ts.map +1 -1
- package/dist/commonjs/tabs/lib/useTabsModel.js +144 -5
- package/dist/commonjs/version/lib/version.js +1 -1
- package/dist/es6/color-picker/lib/ColorPicker.js +1 -1
- package/dist/es6/color-picker/lib/parts/ColorReset.js +1 -1
- package/dist/es6/color-picker/lib/parts/SwatchBook.js +1 -1
- package/dist/es6/divider/lib/Divider.js +3 -3
- package/dist/es6/loading-sparkles/lib/LoadingSparkles.js +2 -2
- package/dist/es6/multi-select/lib/MultiSelectInput.js +3 -3
- package/dist/es6/radio/lib/RadioGroup.js +3 -3
- package/dist/es6/radio/lib/RadioLabel.js +1 -1
- package/dist/es6/radio/lib/RadioText.js +3 -3
- package/dist/es6/radio/lib/StyledRadioButton.js +5 -5
- package/dist/es6/side-panel/lib/SidePanel.js +7 -7
- package/dist/es6/side-panel/lib/SidePanelToggleButton.js +11 -11
- package/dist/es6/status-indicator/lib/StatusIndicator.js +14 -14
- package/dist/es6/status-indicator/lib/StatusIndicatorLabel.js +1 -1
- package/dist/es6/switch/lib/SwitchBackground.js +1 -1
- package/dist/es6/switch/lib/SwitchCircle.js +2 -2
- package/dist/es6/switch/lib/SwitchContainer.js +1 -1
- package/dist/es6/switch/lib/SwitchIcon.js +2 -2
- package/dist/es6/switch/lib/SwitchInput.js +3 -3
- package/dist/es6/tabs/lib/TabsItem.d.ts +3 -1
- package/dist/es6/tabs/lib/TabsItem.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsItem.js +38 -6
- package/dist/es6/tabs/lib/TabsList.d.ts +330 -2
- package/dist/es6/tabs/lib/TabsList.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsList.js +67 -15
- package/dist/es6/tabs/lib/TabsOverflowButton.d.ts +24 -215
- package/dist/es6/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
- package/dist/es6/tabs/lib/TabsOverflowButton.js +72 -8
- package/dist/es6/tabs/lib/TabsPanels.js +2 -2
- package/dist/es6/tabs/lib/useTabsModel.d.ts +17 -12
- package/dist/es6/tabs/lib/useTabsModel.d.ts.map +1 -1
- package/dist/es6/tabs/lib/useTabsModel.js +144 -5
- package/dist/es6/version/lib/version.js +1 -1
- package/package.json +4 -4
- package/tabs/lib/TabsItem.tsx +39 -0
- package/tabs/lib/TabsList.tsx +83 -5
- package/tabs/lib/TabsOverflowButton.tsx +77 -5
- package/tabs/lib/TabsPanels.tsx +2 -2
- package/tabs/lib/useTabsModel.tsx +161 -3
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { defaultGetId, useListModel, useOverflowListModel, } from '@workday/canvas-kit-react/collection';
|
|
3
|
-
import { createModelHook, useModalityType } from '@workday/canvas-kit-react/common';
|
|
3
|
+
import { createModelHook, slugify, useModalityType } from '@workday/canvas-kit-react/common';
|
|
4
4
|
import { useMenuModel } from '@workday/canvas-kit-react/menu';
|
|
5
|
+
/**
|
|
6
|
+
* This constant is used as the stable identifier for the overflow button in the tabs collection.
|
|
7
|
+
* It uses a `__` prefix to avoid conflicts with user-provided item IDs.
|
|
8
|
+
*/
|
|
9
|
+
export const TABS_OVERFLOW_BUTTON_ID = '__tabs-overflow__';
|
|
5
10
|
/**
|
|
6
11
|
* The TabsModel extends the [Collection
|
|
7
12
|
* System](/get-started/for-developers/guides/collection-api/). Tabs have tab items and
|
|
@@ -49,11 +54,73 @@ export const useTabsModel = createModelHook({
|
|
|
49
54
|
requiredConfig: useOverflowListModel.requiredConfig,
|
|
50
55
|
contextOverride: useOverflowListModel.Context,
|
|
51
56
|
})(config => {
|
|
52
|
-
var _a;
|
|
57
|
+
var _a, _b;
|
|
53
58
|
const initialSelectedRef = React.useRef(config.initialTab);
|
|
54
|
-
const
|
|
59
|
+
const userGetId = config.getId || defaultGetId;
|
|
55
60
|
const modality = useModalityType();
|
|
61
|
+
// Stable object so getId can recognize the synthetic overflow item. Callers
|
|
62
|
+
// often pass a getId that only understands their own item shape (for example
|
|
63
|
+
// `item => item.contextId`). Without this, the overflow item gets an empty id
|
|
64
|
+
// and Tabs.List renders it as a normal tab.
|
|
65
|
+
const overflowItemRef = React.useRef({
|
|
66
|
+
id: TABS_OVERFLOW_BUTTON_ID,
|
|
67
|
+
text: 'More',
|
|
68
|
+
});
|
|
69
|
+
const getId = (item) => {
|
|
70
|
+
if (item === overflowItemRef.current) {
|
|
71
|
+
return TABS_OVERFLOW_BUTTON_ID;
|
|
72
|
+
}
|
|
73
|
+
const id = userGetId(item);
|
|
74
|
+
if (id === TABS_OVERFLOW_BUTTON_ID) {
|
|
75
|
+
throw new Error(`Tabs item id "${TABS_OVERFLOW_BUTTON_ID}" is reserved for the overflow tab. Choose a different id.`);
|
|
76
|
+
}
|
|
77
|
+
return id;
|
|
78
|
+
};
|
|
79
|
+
// When using dynamic items, append a synthetic overflow button item to the
|
|
80
|
+
// items array. This ensures the overflow button is included in the navigation
|
|
81
|
+
// sequence for roving tabindex. The overflow button component will handle
|
|
82
|
+
// conditional rendering (not showing when there are no hidden items).
|
|
83
|
+
// The overflow button is NOT included in itemSizeCache (measured separately
|
|
84
|
+
// via setOverflowTargetSize), so it won't be incorrectly hidden.
|
|
85
|
+
const itemsWithOverflowButton = React.useMemo(() => {
|
|
86
|
+
var _a;
|
|
87
|
+
if ((_a = config.items) === null || _a === void 0 ? void 0 : _a.length) {
|
|
88
|
+
return [...config.items, overflowItemRef.current];
|
|
89
|
+
}
|
|
90
|
+
return config.items;
|
|
91
|
+
}, [config.items]);
|
|
92
|
+
// Track whether the overflow button should be non-interactive based on previous
|
|
93
|
+
// render's hiddenIds. We use a ref because hiddenIds isn't available until after
|
|
94
|
+
// the model is created, but we need to pass nonInteractiveIds to the model config.
|
|
95
|
+
// On first render, assume non-interactive (no overflow calculated yet).
|
|
96
|
+
// On subsequent renders, use the previous hiddenIds.length to determine.
|
|
97
|
+
const prevHiddenIdsLengthRef = React.useRef(0);
|
|
98
|
+
const [, setNonInteractiveTick] = React.useState(0);
|
|
99
|
+
const overflowButtonNonInteractive = !!((_a = config.items) === null || _a === void 0 ? void 0 : _a.length) && prevHiddenIdsLengthRef.current === 0;
|
|
100
|
+
// Compute nonInteractiveIds to pass to the model, including overflow button when appropriate
|
|
101
|
+
const nonInteractiveIdsForModel = React.useMemo(() => {
|
|
102
|
+
const baseIds = config.nonInteractiveIds || [];
|
|
103
|
+
if (overflowButtonNonInteractive) {
|
|
104
|
+
return [...baseIds, TABS_OVERFLOW_BUTTON_ID];
|
|
105
|
+
}
|
|
106
|
+
return baseIds;
|
|
107
|
+
}, [config.nonInteractiveIds, overflowButtonNonInteractive]);
|
|
108
|
+
// Stores the ID of a tab that should receive focus after the menu closes.
|
|
109
|
+
// This is used when selecting a tab from the overflow menu - we want focus
|
|
110
|
+
// to go to the newly selected tab, not back to the overflow button.
|
|
111
|
+
const pendingFocusTabIdRef = React.useRef(null);
|
|
112
|
+
const pendingFocusRafOuterRef = React.useRef(0);
|
|
113
|
+
const pendingFocusRafInnerRef = React.useRef(0);
|
|
114
|
+
React.useLayoutEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
cancelAnimationFrame(pendingFocusRafOuterRef.current);
|
|
117
|
+
cancelAnimationFrame(pendingFocusRafInnerRef.current);
|
|
118
|
+
};
|
|
119
|
+
}, []);
|
|
56
120
|
const model = useOverflowListModel(useOverflowListModel.mergeConfig(config, {
|
|
121
|
+
items: itemsWithOverflowButton,
|
|
122
|
+
getId,
|
|
123
|
+
nonInteractiveIds: nonInteractiveIdsForModel,
|
|
57
124
|
shouldCalculateOverflow: modality !== 'touch',
|
|
58
125
|
orientation: config.orientation || 'horizontal',
|
|
59
126
|
onRegisterItem(data) {
|
|
@@ -64,11 +131,64 @@ export const useTabsModel = createModelHook({
|
|
|
64
131
|
},
|
|
65
132
|
initialSelectedIds: config.initialTab
|
|
66
133
|
? [config.initialTab]
|
|
67
|
-
: ((
|
|
134
|
+
: ((_b = config.items) === null || _b === void 0 ? void 0 : _b.length)
|
|
68
135
|
? [getId(config.items[0])]
|
|
69
136
|
: [],
|
|
70
137
|
shouldVirtualize: false,
|
|
71
138
|
}));
|
|
139
|
+
// Update the ref after each render so the next render knows the current hiddenIds.length.
|
|
140
|
+
// If hiddenIds.length changed (e.g. overflow just calculated), force a re-render so
|
|
141
|
+
// we pass the correct nonInteractiveIds and the overflow button is not stuck disabled.
|
|
142
|
+
React.useLayoutEffect(() => {
|
|
143
|
+
const nextLen = model.state.hiddenIds.length;
|
|
144
|
+
if (prevHiddenIdsLengthRef.current !== nextLen) {
|
|
145
|
+
prevHiddenIdsLengthRef.current = nextLen;
|
|
146
|
+
setNonInteractiveTick(t => t + 1);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
prevHiddenIdsLengthRef.current = nextLen;
|
|
150
|
+
}
|
|
151
|
+
}, [model.state.hiddenIds.length]);
|
|
152
|
+
// When the cursor is on an item that isn't in the DOM, move it to a visible one so the
|
|
153
|
+
// roving tabindex lands on a focusable element. This covers: (1) cursor on a hidden tab when
|
|
154
|
+
// only "More" is visible, and (2) cursor on the overflow button when we widen and it unmounts.
|
|
155
|
+
React.useLayoutEffect(() => {
|
|
156
|
+
var _a;
|
|
157
|
+
const { cursorId, hiddenIds, items } = model.state;
|
|
158
|
+
if (!cursorId) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const currentCursorId = typeof cursorId === 'string' ? cursorId : ((_a = cursorId.slice(-1)[0]) !== null && _a !== void 0 ? _a : '');
|
|
162
|
+
const cursorOnHidden = currentCursorId ? hiddenIds.includes(currentCursorId) : false;
|
|
163
|
+
const cursorOnOverflowWhenNoOverflow = currentCursorId === TABS_OVERFLOW_BUTTON_ID && hiddenIds.length === 0;
|
|
164
|
+
if (cursorOnHidden) {
|
|
165
|
+
// Move to first visible item (including overflow button when it's the only one visible)
|
|
166
|
+
const firstVisible = items.find(item => !hiddenIds.includes(item.id));
|
|
167
|
+
if (firstVisible) {
|
|
168
|
+
model.events.goTo({ id: firstVisible.id });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (cursorOnOverflowWhenNoOverflow) {
|
|
172
|
+
// Overflow button unmounted; move to selected tab or first tab (not the overflow button)
|
|
173
|
+
const selectedIds = model.state.selectedIds;
|
|
174
|
+
const selectedId = selectedIds !== 'all' && Array.isArray(selectedIds) && selectedIds.length
|
|
175
|
+
? selectedIds[0]
|
|
176
|
+
: undefined;
|
|
177
|
+
const firstVisibleTab = items.find(item => item.id !== TABS_OVERFLOW_BUTTON_ID && !hiddenIds.includes(item.id));
|
|
178
|
+
const targetId = selectedId && !hiddenIds.includes(selectedId) ? selectedId : firstVisibleTab === null || firstVisibleTab === void 0 ? void 0 : firstVisibleTab.id;
|
|
179
|
+
if (targetId) {
|
|
180
|
+
model.events.goTo({ id: targetId });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Use field-level deps instead of `model.state` so this does not re-run on every model update.
|
|
184
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
185
|
+
}, [
|
|
186
|
+
model.state.cursorId,
|
|
187
|
+
model.state.hiddenIds,
|
|
188
|
+
model.state.items,
|
|
189
|
+
model.state.selectedIds,
|
|
190
|
+
model.events,
|
|
191
|
+
]);
|
|
72
192
|
const panels = useListModel();
|
|
73
193
|
const state = {
|
|
74
194
|
...model.state,
|
|
@@ -105,8 +225,27 @@ export const useTabsModel = createModelHook({
|
|
|
105
225
|
items: overflowItems,
|
|
106
226
|
nonInteractiveIds: state.nonInteractiveIds.filter(key => !state.hiddenIds.includes(key)),
|
|
107
227
|
onSelect(data) {
|
|
108
|
-
menu
|
|
228
|
+
// Store the tab ID to focus after the menu closes and React re-renders
|
|
229
|
+
pendingFocusTabIdRef.current = data.id;
|
|
230
|
+
// Update state: select the tab, move cursor, then close menu
|
|
109
231
|
events.select(data);
|
|
232
|
+
model.events.goTo({ id: data.id });
|
|
233
|
+
menu.events.hide();
|
|
234
|
+
// Focus the selected tab AFTER React has re-rendered.
|
|
235
|
+
// Double rAF is required because useReturnFocus restores focus to the overflow
|
|
236
|
+
// button on a single rAF after hide; the second frame runs after that restore.
|
|
237
|
+
pendingFocusRafOuterRef.current = requestAnimationFrame(() => {
|
|
238
|
+
pendingFocusRafInnerRef.current = requestAnimationFrame(() => {
|
|
239
|
+
const tabId = pendingFocusTabIdRef.current;
|
|
240
|
+
if (tabId) {
|
|
241
|
+
const tabElement = document.querySelector(`[data-focus-id="${slugify(`${model.state.id}-${tabId}`)}"]`);
|
|
242
|
+
if (tabElement) {
|
|
243
|
+
tabElement.focus();
|
|
244
|
+
}
|
|
245
|
+
pendingFocusTabIdRef.current = null;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
110
249
|
},
|
|
111
250
|
onShow() {
|
|
112
251
|
// Always select the first item when the menu is opened
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '16.1.
|
|
1
|
+
export const version = '16.1.9';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workday/canvas-kit-preview-react",
|
|
3
|
-
"version": "16.1.
|
|
3
|
+
"version": "16.1.9",
|
|
4
4
|
"description": "Canvas Kit Preview is made up of components that have the full design and a11y review, are part of the DS ecosystem and are approved for use in product. The API's could be subject to change, but not without strong communication and migration strategies.",
|
|
5
5
|
"author": "Workday, Inc. (https://www.workday.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@emotion/react": "^11.7.1",
|
|
50
50
|
"@emotion/styled": "^11.6.0",
|
|
51
|
-
"@workday/canvas-kit-react": "^16.1.
|
|
52
|
-
"@workday/canvas-kit-styling": "^16.1.
|
|
51
|
+
"@workday/canvas-kit-react": "^16.1.9",
|
|
52
|
+
"@workday/canvas-kit-styling": "^16.1.9",
|
|
53
53
|
"@workday/canvas-system-icons-web": "^5.0.3",
|
|
54
54
|
"@workday/canvas-tokens-web": "^4.4.0",
|
|
55
55
|
"@workday/design-assets-types": "^0.3.0"
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"react-hook-form": "7.36.1",
|
|
61
61
|
"yup": "^0.32.11"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "5a204161d968a951c6e5bca4010893f7e09fd73d"
|
|
64
64
|
}
|
package/tabs/lib/TabsItem.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
ListRenderItemContext,
|
|
4
5
|
isSelected,
|
|
5
6
|
useListItemRegister,
|
|
6
7
|
useListItemRovingFocus,
|
|
@@ -229,6 +230,43 @@ export const StyledTabItem = createComponent('button')<TabsItemProps>({
|
|
|
229
230
|
},
|
|
230
231
|
});
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* When the selected tab receives ArrowDown, move focus to its tab panel.
|
|
235
|
+
* Returning `null` is the mergeCallback sentinel that stops later-merged
|
|
236
|
+
* `onKeyDown` handlers (including roving focus) from also handling this key.
|
|
237
|
+
* This hook must be composed before `useListItemRovingFocus` so it runs after
|
|
238
|
+
* that hook and its return value can skip the roving handler.
|
|
239
|
+
* Tabs.OverflowButton does not use this hook.
|
|
240
|
+
*
|
|
241
|
+
* ArrowDown is only intercepted in horizontal orientation. In a vertical tablist,
|
|
242
|
+
* ArrowDown is the roving-navigation key and must reach `useListItemRovingFocus`.
|
|
243
|
+
*/
|
|
244
|
+
const useTabsItemFocusPanelOnArrowDown = createElemPropsHook(useTabsModel)((
|
|
245
|
+
{state},
|
|
246
|
+
_,
|
|
247
|
+
elemProps: {'data-id'?: string} = {}
|
|
248
|
+
) => {
|
|
249
|
+
const {item} = React.useContext(ListRenderItemContext);
|
|
250
|
+
const name = elemProps['data-id'] || item?.id || '';
|
|
251
|
+
const selected = !!name && isSelected(name, state);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
|
|
255
|
+
if (!selected || state.orientation !== 'horizontal' || event.key !== 'ArrowDown') {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
const panelId = slugify(`tabpanel-${state.id}-${name}`);
|
|
260
|
+
const panel = document.getElementById(panelId);
|
|
261
|
+
if (panel) {
|
|
262
|
+
(panel as HTMLElement).focus();
|
|
263
|
+
}
|
|
264
|
+
// mergeCallback: `null` prevents the roving-focus onKeyDown from running.
|
|
265
|
+
return null;
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
232
270
|
export const useTabsItem = composeHooks(
|
|
233
271
|
createElemPropsHook(useTabsModel)(({state}, _, elemProps: {'data-id'?: string} = {}) => {
|
|
234
272
|
const name = elemProps['data-id'] || '';
|
|
@@ -244,6 +282,7 @@ export const useTabsItem = composeHooks(
|
|
|
244
282
|
}),
|
|
245
283
|
useListItemSelect,
|
|
246
284
|
useOverflowListItemMeasure,
|
|
285
|
+
useTabsItemFocusPanelOnArrowDown,
|
|
247
286
|
useListItemRovingFocus,
|
|
248
287
|
useListItemRegister
|
|
249
288
|
);
|
package/tabs/lib/TabsList.tsx
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
ListRenderItemContext,
|
|
5
|
+
isCursor,
|
|
6
|
+
orientationKeyMap,
|
|
6
7
|
useOverflowListMeasure,
|
|
7
8
|
} from '@workday/canvas-kit-react/collection';
|
|
8
9
|
import {
|
|
9
10
|
ExtractProps,
|
|
11
|
+
Generic,
|
|
10
12
|
composeHooks,
|
|
11
13
|
createElemPropsHook,
|
|
12
14
|
createSubcomponent,
|
|
13
15
|
useLocalRef,
|
|
14
16
|
useModalityType,
|
|
17
|
+
useMountLayout,
|
|
15
18
|
} from '@workday/canvas-kit-react/common';
|
|
16
19
|
import {Flex, mergeStyles} from '@workday/canvas-kit-react/layout';
|
|
17
20
|
import {createStencil} from '@workday/canvas-kit-styling';
|
|
18
21
|
import {system} from '@workday/canvas-tokens-web';
|
|
19
22
|
|
|
20
|
-
import {useTabsModel} from './useTabsModel';
|
|
23
|
+
import {TABS_OVERFLOW_BUTTON_ID, useTabsModel} from './useTabsModel';
|
|
21
24
|
|
|
22
25
|
export interface TabListProps<T = any> extends Omit<ExtractProps<typeof Flex, never>, 'children'> {
|
|
23
26
|
/**
|
|
@@ -100,6 +103,49 @@ export const useTabOverflowScroll = createElemPropsHook(useTabsModel)((
|
|
|
100
103
|
};
|
|
101
104
|
});
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Resets the tablist cursor on blur to the selected tab when visible, or to the first visible
|
|
108
|
+
* item (e.g. the overflow button) when the selected tab is hidden. This ensures the roving
|
|
109
|
+
* tabindex always lands on a focusable element when only the "More" button is visible.
|
|
110
|
+
*/
|
|
111
|
+
export const useTabsListResetCursorOnBlur = createElemPropsHook(useTabsModel)(({state, events}) => {
|
|
112
|
+
const programmaticFocusRef = React.useRef(false);
|
|
113
|
+
const requestAnimationFrameRef = React.useRef(0);
|
|
114
|
+
|
|
115
|
+
useMountLayout(() => {
|
|
116
|
+
return () => {
|
|
117
|
+
cancelAnimationFrame(requestAnimationFrameRef.current);
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
onKeyDown(event: React.KeyboardEvent) {
|
|
123
|
+
if (Object.keys(orientationKeyMap[state.orientation]).indexOf(event.key) !== -1) {
|
|
124
|
+
programmaticFocusRef.current = true;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
onFocus() {
|
|
128
|
+
programmaticFocusRef.current = false;
|
|
129
|
+
},
|
|
130
|
+
onBlur() {
|
|
131
|
+
if (!programmaticFocusRef.current) {
|
|
132
|
+
requestAnimationFrameRef.current = requestAnimationFrame(() => {
|
|
133
|
+
const selectedId =
|
|
134
|
+
state.selectedIds !== 'all' && state.selectedIds.length
|
|
135
|
+
? state.selectedIds[0]
|
|
136
|
+
: undefined;
|
|
137
|
+
const visibleItems = state.items.filter(item => !state.hiddenIds.includes(item.id));
|
|
138
|
+
const targetId =
|
|
139
|
+
selectedId && !state.hiddenIds.includes(selectedId) ? selectedId : visibleItems[0]?.id;
|
|
140
|
+
if (targetId && !isCursor(state, targetId)) {
|
|
141
|
+
events.goTo({id: targetId});
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
|
|
103
149
|
export const useTabsList = composeHooks(
|
|
104
150
|
useTabOverflowScroll,
|
|
105
151
|
createElemPropsHook(useTabsModel)(model => {
|
|
@@ -109,13 +155,14 @@ export const useTabsList = composeHooks(
|
|
|
109
155
|
} as const;
|
|
110
156
|
}),
|
|
111
157
|
useOverflowListMeasure,
|
|
112
|
-
|
|
158
|
+
useTabsListResetCursorOnBlur
|
|
113
159
|
);
|
|
114
160
|
|
|
115
161
|
export const tabsListStencil = createStencil({
|
|
116
162
|
base: {
|
|
117
163
|
display: 'flex',
|
|
118
164
|
position: 'relative',
|
|
165
|
+
minWidth: 0,
|
|
119
166
|
gap: system.legacy.gap.sm,
|
|
120
167
|
|
|
121
168
|
'@media screen and (max-width: 768px)': {
|
|
@@ -156,6 +203,37 @@ export const tabsListStencil = createStencil({
|
|
|
156
203
|
},
|
|
157
204
|
});
|
|
158
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Custom render function for tabs that filters out the synthetic overflow button item.
|
|
208
|
+
* This is needed because the overflow button is included in the model's items array
|
|
209
|
+
* for navigation purposes, but should not be rendered by the list or panel render function.
|
|
210
|
+
*/
|
|
211
|
+
export function useTabsCollectionRenderItems(
|
|
212
|
+
model: ReturnType<typeof useTabsModel>,
|
|
213
|
+
children: ((item: Generic, index: number) => React.ReactNode) | React.ReactNode
|
|
214
|
+
): React.ReactNode {
|
|
215
|
+
const itemsToRender = model.state.items.filter(item => item.id !== TABS_OVERFLOW_BUTTON_ID);
|
|
216
|
+
|
|
217
|
+
const items =
|
|
218
|
+
typeof children === 'function' ? (
|
|
219
|
+
itemsToRender.map(item => {
|
|
220
|
+
const child = (children as (item: Generic, index: number) => React.ReactNode)(
|
|
221
|
+
item.value,
|
|
222
|
+
item.index
|
|
223
|
+
);
|
|
224
|
+
return (
|
|
225
|
+
<ListRenderItemContext.Provider key={item.id || item.index} value={{item}}>
|
|
226
|
+
{child}
|
|
227
|
+
</ListRenderItemContext.Provider>
|
|
228
|
+
);
|
|
229
|
+
})
|
|
230
|
+
) : (
|
|
231
|
+
<ListRenderItemContext.Provider value={{}}>{children}</ListRenderItemContext.Provider>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return items;
|
|
235
|
+
}
|
|
236
|
+
|
|
159
237
|
export const TabsList = createSubcomponent('div')({
|
|
160
238
|
displayName: 'Tabs.List',
|
|
161
239
|
modelHook: useTabsModel,
|
|
@@ -173,7 +251,7 @@ export const TabsList = createSubcomponent('div')({
|
|
|
173
251
|
})
|
|
174
252
|
)}
|
|
175
253
|
>
|
|
176
|
-
{
|
|
254
|
+
{useTabsCollectionRenderItems(model, children)}
|
|
177
255
|
{overflowButton}
|
|
178
256
|
</Element>
|
|
179
257
|
);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useListItemRegister,
|
|
5
|
+
useListItemRovingFocus,
|
|
6
|
+
useOverflowListModel,
|
|
7
|
+
} from '@workday/canvas-kit-react/collection';
|
|
4
8
|
import {
|
|
5
9
|
composeHooks,
|
|
6
10
|
createElemPropsHook,
|
|
7
11
|
createSubModelElemPropsHook,
|
|
8
12
|
createSubcomponent,
|
|
13
|
+
useLocalRef,
|
|
14
|
+
useResizeObserver,
|
|
9
15
|
} from '@workday/canvas-kit-react/common';
|
|
10
16
|
import {SystemIcon} from '@workday/canvas-kit-react/icon';
|
|
11
17
|
import {mergeStyles} from '@workday/canvas-kit-react/layout';
|
|
@@ -14,7 +20,10 @@ import {createStencil} from '@workday/canvas-kit-styling';
|
|
|
14
20
|
import {chevronDownSmallIcon} from '@workday/canvas-system-icons-web';
|
|
15
21
|
|
|
16
22
|
import {StyledTabItem} from './TabsItem';
|
|
17
|
-
import {useTabsModel} from './useTabsModel';
|
|
23
|
+
import {TABS_OVERFLOW_BUTTON_ID, useTabsModel} from './useTabsModel';
|
|
24
|
+
|
|
25
|
+
// Re-export for consumers who may need to reference the overflow button ID
|
|
26
|
+
export {TABS_OVERFLOW_BUTTON_ID} from './useTabsModel';
|
|
18
27
|
|
|
19
28
|
export interface OverflowButtonProps {
|
|
20
29
|
/**
|
|
@@ -32,14 +41,68 @@ const tabsOverflowButtonStencil = createStencil({
|
|
|
32
41
|
},
|
|
33
42
|
});
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Measures the overflow button and reports its size to the model for overflow calculation.
|
|
46
|
+
* Unlike useOverflowListTarget, this does NOT set aria-hidden or visibility styles because
|
|
47
|
+
* we handle visibility via conditional rendering in the TabsOverflowButton component.
|
|
48
|
+
*/
|
|
49
|
+
const useTabsOverflowButtonMeasure = createElemPropsHook(useOverflowListModel)((model, ref) => {
|
|
50
|
+
const {elementRef, localRef} = useLocalRef(ref as React.Ref<HTMLElement>);
|
|
51
|
+
|
|
52
|
+
useResizeObserver({
|
|
53
|
+
ref: localRef,
|
|
54
|
+
onResize: ({width = 0, height = 0}) => {
|
|
55
|
+
if (localRef.current && (width > 0 || height > 0)) {
|
|
56
|
+
const styles = getComputedStyle(localRef.current);
|
|
57
|
+
const w = width + parseFloat(styles.marginLeft) + parseFloat(styles.marginRight);
|
|
58
|
+
const h = height + parseFloat(styles.marginTop) + parseFloat(styles.marginBottom);
|
|
59
|
+
model.events.setOverflowTargetSize({width: w, height: h});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ref: elementRef,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
35
69
|
export const useTabsOverflowButton = composeHooks(
|
|
70
|
+
// Measures the overflow button for overflow calculation (no aria-hidden or visibility styles)
|
|
71
|
+
useTabsOverflowButtonMeasure,
|
|
72
|
+
// Adds roving tabindex behavior for arrow key navigation within the tablist
|
|
73
|
+
useListItemRovingFocus,
|
|
74
|
+
// Registers the overflow button as an item in the collection for cursor navigation
|
|
75
|
+
useListItemRegister,
|
|
76
|
+
// Provides the stable data-id needed for collection registration
|
|
36
77
|
createElemPropsHook(useTabsModel)(() => {
|
|
37
78
|
return {
|
|
38
|
-
'
|
|
79
|
+
'data-id': TABS_OVERFLOW_BUTTON_ID,
|
|
39
80
|
} as const;
|
|
40
81
|
}),
|
|
41
|
-
|
|
42
|
-
createSubModelElemPropsHook(useTabsModel)(m => m.menu, useMenuTarget)
|
|
82
|
+
// Connects to the menu sub-model for popup behavior
|
|
83
|
+
createSubModelElemPropsHook(useTabsModel)(m => m.menu, useMenuTarget),
|
|
84
|
+
// Syncs the cursor to the overflow button when it receives focus.
|
|
85
|
+
// This is needed because when the menu closes and focus returns to the overflow button,
|
|
86
|
+
// the cursor state needs to be updated so arrow key navigation works correctly.
|
|
87
|
+
createElemPropsHook(useTabsModel)(model => {
|
|
88
|
+
return {
|
|
89
|
+
onFocus() {
|
|
90
|
+
model.events.goTo({id: TABS_OVERFLOW_BUTTON_ID});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
// Runs FIRST - Sets ARIA attributes for the overflow tab
|
|
95
|
+
// In composeHooks, hooks run right-to-left, and mergeProps gives precedence to elemProps
|
|
96
|
+
// (props from earlier-running hooks). So this runs first and its props win.
|
|
97
|
+
// Note: We intentionally do NOT include useListItemSelect here because clicking
|
|
98
|
+
// the overflow button should open the menu, not select it as the active tab
|
|
99
|
+
createElemPropsHook(useTabsModel)(() => {
|
|
100
|
+
return {
|
|
101
|
+
'aria-haspopup': 'menu',
|
|
102
|
+
'aria-selected': false,
|
|
103
|
+
role: 'tab',
|
|
104
|
+
} as const;
|
|
105
|
+
})
|
|
43
106
|
);
|
|
44
107
|
|
|
45
108
|
export const TabsOverflowButton = createSubcomponent('button')({
|
|
@@ -47,6 +110,15 @@ export const TabsOverflowButton = createSubcomponent('button')({
|
|
|
47
110
|
modelHook: useTabsModel,
|
|
48
111
|
elemPropsHook: useTabsOverflowButton,
|
|
49
112
|
})<OverflowButtonProps>(({children, ...elemProps}, Element, model) => {
|
|
113
|
+
// Don't render the overflow button if there are no hidden items.
|
|
114
|
+
// This removes it from the DOM entirely, which:
|
|
115
|
+
// - Removes it from keyboard navigation (roving focus)
|
|
116
|
+
// - Removes it from screen reader announcements
|
|
117
|
+
// - Prevents the "8 tabs" announcement when only 7 are visible
|
|
118
|
+
if (model.state.hiddenIds.length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
50
122
|
return (
|
|
51
123
|
<StyledTabItem
|
|
52
124
|
type="button"
|
package/tabs/lib/TabsPanels.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import {useListRenderItems} from '@workday/canvas-kit-react/collection';
|
|
4
3
|
import {createSubcomponent} from '@workday/canvas-kit-react/common';
|
|
5
4
|
|
|
5
|
+
import {useTabsCollectionRenderItems} from './TabsList';
|
|
6
6
|
import {useTabsModel} from './useTabsModel';
|
|
7
7
|
|
|
8
8
|
export interface TabsPanelsProps<T = any> {
|
|
@@ -16,5 +16,5 @@ export const TabsPanels = createSubcomponent()({
|
|
|
16
16
|
displayName: 'Tabs.Panels',
|
|
17
17
|
modelHook: useTabsModel,
|
|
18
18
|
})<TabsPanelsProps>(({children}, _, model) => {
|
|
19
|
-
return <>{
|
|
19
|
+
return <>{useTabsCollectionRenderItems(model, children)}</>;
|
|
20
20
|
});
|