@workday/canvas-kit-preview-react 16.2.0-0630-next.0 → 16.2.0-0638-next.0

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.
Files changed (72) hide show
  1. package/dist/commonjs/color-picker/lib/ColorPicker.js +1 -1
  2. package/dist/commonjs/color-picker/lib/parts/ColorReset.js +1 -1
  3. package/dist/commonjs/color-picker/lib/parts/SwatchBook.js +1 -1
  4. package/dist/commonjs/divider/lib/Divider.js +3 -3
  5. package/dist/commonjs/loading-sparkles/lib/LoadingSparkles.js +2 -2
  6. package/dist/commonjs/multi-select/lib/MultiSelectInput.js +3 -3
  7. package/dist/commonjs/radio/lib/RadioGroup.js +3 -3
  8. package/dist/commonjs/radio/lib/RadioLabel.js +1 -1
  9. package/dist/commonjs/radio/lib/RadioText.js +3 -3
  10. package/dist/commonjs/radio/lib/StyledRadioButton.js +5 -5
  11. package/dist/commonjs/side-panel/lib/SidePanel.js +7 -7
  12. package/dist/commonjs/side-panel/lib/SidePanelToggleButton.js +11 -11
  13. package/dist/commonjs/status-indicator/lib/StatusIndicator.js +14 -14
  14. package/dist/commonjs/status-indicator/lib/StatusIndicatorLabel.js +1 -1
  15. package/dist/commonjs/switch/lib/SwitchBackground.js +1 -1
  16. package/dist/commonjs/switch/lib/SwitchCircle.js +2 -2
  17. package/dist/commonjs/switch/lib/SwitchContainer.js +1 -1
  18. package/dist/commonjs/switch/lib/SwitchIcon.js +2 -2
  19. package/dist/commonjs/switch/lib/SwitchInput.js +3 -3
  20. package/dist/commonjs/tabs/lib/TabsItem.d.ts +3 -1
  21. package/dist/commonjs/tabs/lib/TabsItem.d.ts.map +1 -1
  22. package/dist/commonjs/tabs/lib/TabsItem.js +60 -5
  23. package/dist/commonjs/tabs/lib/TabsList.d.ts +330 -2
  24. package/dist/commonjs/tabs/lib/TabsList.d.ts.map +1 -1
  25. package/dist/commonjs/tabs/lib/TabsList.js +65 -12
  26. package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts +24 -215
  27. package/dist/commonjs/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
  28. package/dist/commonjs/tabs/lib/TabsOverflowButton.js +71 -6
  29. package/dist/commonjs/tabs/lib/TabsPanels.js +2 -2
  30. package/dist/commonjs/tabs/lib/useTabsModel.d.ts +17 -12
  31. package/dist/commonjs/tabs/lib/useTabsModel.d.ts.map +1 -1
  32. package/dist/commonjs/tabs/lib/useTabsModel.js +144 -5
  33. package/dist/commonjs/version/lib/version.js +1 -1
  34. package/dist/es6/color-picker/lib/ColorPicker.js +1 -1
  35. package/dist/es6/color-picker/lib/parts/ColorReset.js +1 -1
  36. package/dist/es6/color-picker/lib/parts/SwatchBook.js +1 -1
  37. package/dist/es6/divider/lib/Divider.js +3 -3
  38. package/dist/es6/loading-sparkles/lib/LoadingSparkles.js +2 -2
  39. package/dist/es6/multi-select/lib/MultiSelectInput.js +3 -3
  40. package/dist/es6/radio/lib/RadioGroup.js +3 -3
  41. package/dist/es6/radio/lib/RadioLabel.js +1 -1
  42. package/dist/es6/radio/lib/RadioText.js +3 -3
  43. package/dist/es6/radio/lib/StyledRadioButton.js +5 -5
  44. package/dist/es6/side-panel/lib/SidePanel.js +7 -7
  45. package/dist/es6/side-panel/lib/SidePanelToggleButton.js +11 -11
  46. package/dist/es6/status-indicator/lib/StatusIndicator.js +14 -14
  47. package/dist/es6/status-indicator/lib/StatusIndicatorLabel.js +1 -1
  48. package/dist/es6/switch/lib/SwitchBackground.js +1 -1
  49. package/dist/es6/switch/lib/SwitchCircle.js +2 -2
  50. package/dist/es6/switch/lib/SwitchContainer.js +1 -1
  51. package/dist/es6/switch/lib/SwitchIcon.js +2 -2
  52. package/dist/es6/switch/lib/SwitchInput.js +3 -3
  53. package/dist/es6/tabs/lib/TabsItem.d.ts +3 -1
  54. package/dist/es6/tabs/lib/TabsItem.d.ts.map +1 -1
  55. package/dist/es6/tabs/lib/TabsItem.js +38 -6
  56. package/dist/es6/tabs/lib/TabsList.d.ts +330 -2
  57. package/dist/es6/tabs/lib/TabsList.d.ts.map +1 -1
  58. package/dist/es6/tabs/lib/TabsList.js +67 -15
  59. package/dist/es6/tabs/lib/TabsOverflowButton.d.ts +24 -215
  60. package/dist/es6/tabs/lib/TabsOverflowButton.d.ts.map +1 -1
  61. package/dist/es6/tabs/lib/TabsOverflowButton.js +72 -8
  62. package/dist/es6/tabs/lib/TabsPanels.js +2 -2
  63. package/dist/es6/tabs/lib/useTabsModel.d.ts +17 -12
  64. package/dist/es6/tabs/lib/useTabsModel.d.ts.map +1 -1
  65. package/dist/es6/tabs/lib/useTabsModel.js +144 -5
  66. package/dist/es6/version/lib/version.js +1 -1
  67. package/package.json +4 -4
  68. package/tabs/lib/TabsItem.tsx +39 -0
  69. package/tabs/lib/TabsList.tsx +83 -5
  70. package/tabs/lib/TabsOverflowButton.tsx +77 -5
  71. package/tabs/lib/TabsPanels.tsx +2 -2
  72. package/tabs/lib/useTabsModel.tsx +161 -3
@@ -5,9 +5,15 @@ import {
5
5
  useListModel,
6
6
  useOverflowListModel,
7
7
  } from '@workday/canvas-kit-react/collection';
8
- import {createModelHook, useModalityType} from '@workday/canvas-kit-react/common';
8
+ import {createModelHook, slugify, useModalityType} from '@workday/canvas-kit-react/common';
9
9
  import {useMenuModel} from '@workday/canvas-kit-react/menu';
10
10
 
11
+ /**
12
+ * This constant is used as the stable identifier for the overflow button in the tabs collection.
13
+ * It uses a `__` prefix to avoid conflicts with user-provided item IDs.
14
+ */
15
+ export const TABS_OVERFLOW_BUTTON_ID = '__tabs-overflow__';
16
+
11
17
  /**
12
18
  * The TabsModel extends the [Collection
13
19
  * System](/get-started/for-developers/guides/collection-api/). Tabs have tab items and
@@ -56,11 +62,83 @@ export const useTabsModel = createModelHook({
56
62
  contextOverride: useOverflowListModel.Context,
57
63
  })(config => {
58
64
  const initialSelectedRef = React.useRef(config.initialTab);
59
- const getId = config.getId || defaultGetId;
65
+ const userGetId = config.getId || defaultGetId;
60
66
  const modality = useModalityType();
61
67
 
68
+ // Stable object so getId can recognize the synthetic overflow item. Callers
69
+ // often pass a getId that only understands their own item shape (for example
70
+ // `item => item.contextId`). Without this, the overflow item gets an empty id
71
+ // and Tabs.List renders it as a normal tab.
72
+ const overflowItemRef = React.useRef({
73
+ id: TABS_OVERFLOW_BUTTON_ID,
74
+ text: 'More',
75
+ });
76
+
77
+ const getId = (item: Parameters<typeof userGetId>[0]) => {
78
+ if (item === overflowItemRef.current) {
79
+ return TABS_OVERFLOW_BUTTON_ID;
80
+ }
81
+ const id = userGetId(item);
82
+ if (id === TABS_OVERFLOW_BUTTON_ID) {
83
+ throw new Error(
84
+ `Tabs item id "${TABS_OVERFLOW_BUTTON_ID}" is reserved for the overflow tab. Choose a different id.`
85
+ );
86
+ }
87
+ return id;
88
+ };
89
+
90
+ // When using dynamic items, append a synthetic overflow button item to the
91
+ // items array. This ensures the overflow button is included in the navigation
92
+ // sequence for roving tabindex. The overflow button component will handle
93
+ // conditional rendering (not showing when there are no hidden items).
94
+ // The overflow button is NOT included in itemSizeCache (measured separately
95
+ // via setOverflowTargetSize), so it won't be incorrectly hidden.
96
+ const itemsWithOverflowButton = React.useMemo(() => {
97
+ if (config.items?.length) {
98
+ return [...config.items, overflowItemRef.current];
99
+ }
100
+ return config.items;
101
+ }, [config.items]);
102
+
103
+ // Track whether the overflow button should be non-interactive based on previous
104
+ // render's hiddenIds. We use a ref because hiddenIds isn't available until after
105
+ // the model is created, but we need to pass nonInteractiveIds to the model config.
106
+ // On first render, assume non-interactive (no overflow calculated yet).
107
+ // On subsequent renders, use the previous hiddenIds.length to determine.
108
+ const prevHiddenIdsLengthRef = React.useRef(0);
109
+ const [, setNonInteractiveTick] = React.useState(0);
110
+
111
+ const overflowButtonNonInteractive =
112
+ !!config.items?.length && prevHiddenIdsLengthRef.current === 0;
113
+
114
+ // Compute nonInteractiveIds to pass to the model, including overflow button when appropriate
115
+ const nonInteractiveIdsForModel = React.useMemo(() => {
116
+ const baseIds = config.nonInteractiveIds || [];
117
+ if (overflowButtonNonInteractive) {
118
+ return [...baseIds, TABS_OVERFLOW_BUTTON_ID];
119
+ }
120
+ return baseIds;
121
+ }, [config.nonInteractiveIds, overflowButtonNonInteractive]);
122
+
123
+ // Stores the ID of a tab that should receive focus after the menu closes.
124
+ // This is used when selecting a tab from the overflow menu - we want focus
125
+ // to go to the newly selected tab, not back to the overflow button.
126
+ const pendingFocusTabIdRef = React.useRef<string | null>(null);
127
+ const pendingFocusRafOuterRef = React.useRef(0);
128
+ const pendingFocusRafInnerRef = React.useRef(0);
129
+
130
+ React.useLayoutEffect(() => {
131
+ return () => {
132
+ cancelAnimationFrame(pendingFocusRafOuterRef.current);
133
+ cancelAnimationFrame(pendingFocusRafInnerRef.current);
134
+ };
135
+ }, []);
136
+
62
137
  const model = useOverflowListModel(
63
138
  useOverflowListModel.mergeConfig(config, {
139
+ items: itemsWithOverflowButton,
140
+ getId,
141
+ nonInteractiveIds: nonInteractiveIdsForModel,
64
142
  shouldCalculateOverflow: modality !== 'touch',
65
143
  orientation: config.orientation || 'horizontal',
66
144
  onRegisterItem(data) {
@@ -78,6 +156,63 @@ export const useTabsModel = createModelHook({
78
156
  })
79
157
  );
80
158
 
159
+ // Update the ref after each render so the next render knows the current hiddenIds.length.
160
+ // If hiddenIds.length changed (e.g. overflow just calculated), force a re-render so
161
+ // we pass the correct nonInteractiveIds and the overflow button is not stuck disabled.
162
+ React.useLayoutEffect(() => {
163
+ const nextLen = model.state.hiddenIds.length;
164
+ if (prevHiddenIdsLengthRef.current !== nextLen) {
165
+ prevHiddenIdsLengthRef.current = nextLen;
166
+ setNonInteractiveTick(t => t + 1);
167
+ } else {
168
+ prevHiddenIdsLengthRef.current = nextLen;
169
+ }
170
+ }, [model.state.hiddenIds.length]);
171
+
172
+ // When the cursor is on an item that isn't in the DOM, move it to a visible one so the
173
+ // roving tabindex lands on a focusable element. This covers: (1) cursor on a hidden tab when
174
+ // only "More" is visible, and (2) cursor on the overflow button when we widen and it unmounts.
175
+ React.useLayoutEffect(() => {
176
+ const {cursorId, hiddenIds, items} = model.state;
177
+ if (!cursorId) {
178
+ return;
179
+ }
180
+ const currentCursorId = typeof cursorId === 'string' ? cursorId : (cursorId.slice(-1)[0] ?? '');
181
+ const cursorOnHidden = currentCursorId ? hiddenIds.includes(currentCursorId) : false;
182
+ const cursorOnOverflowWhenNoOverflow =
183
+ currentCursorId === TABS_OVERFLOW_BUTTON_ID && hiddenIds.length === 0;
184
+ if (cursorOnHidden) {
185
+ // Move to first visible item (including overflow button when it's the only one visible)
186
+ const firstVisible = items.find(item => !hiddenIds.includes(item.id));
187
+ if (firstVisible) {
188
+ model.events.goTo({id: firstVisible.id});
189
+ }
190
+ } else if (cursorOnOverflowWhenNoOverflow) {
191
+ // Overflow button unmounted; move to selected tab or first tab (not the overflow button)
192
+ const selectedIds = model.state.selectedIds;
193
+ const selectedId =
194
+ selectedIds !== 'all' && Array.isArray(selectedIds) && selectedIds.length
195
+ ? selectedIds[0]
196
+ : undefined;
197
+ const firstVisibleTab = items.find(
198
+ item => item.id !== TABS_OVERFLOW_BUTTON_ID && !hiddenIds.includes(item.id)
199
+ );
200
+ const targetId =
201
+ selectedId && !hiddenIds.includes(selectedId) ? selectedId : firstVisibleTab?.id;
202
+ if (targetId) {
203
+ model.events.goTo({id: targetId});
204
+ }
205
+ }
206
+ // Use field-level deps instead of `model.state` so this does not re-run on every model update.
207
+ // eslint-disable-next-line react-hooks/exhaustive-deps
208
+ }, [
209
+ model.state.cursorId,
210
+ model.state.hiddenIds,
211
+ model.state.items,
212
+ model.state.selectedIds,
213
+ model.events,
214
+ ]);
215
+
81
216
  const panels = useListModel();
82
217
 
83
218
  const state = {
@@ -122,8 +257,31 @@ export const useTabsModel = createModelHook({
122
257
  items: overflowItems,
123
258
  nonInteractiveIds: state.nonInteractiveIds.filter(key => !state.hiddenIds.includes(key)),
124
259
  onSelect(data) {
125
- menu.events.hide();
260
+ // Store the tab ID to focus after the menu closes and React re-renders
261
+ pendingFocusTabIdRef.current = data.id;
262
+
263
+ // Update state: select the tab, move cursor, then close menu
126
264
  events.select(data);
265
+ model.events.goTo({id: data.id});
266
+ menu.events.hide();
267
+
268
+ // Focus the selected tab AFTER React has re-rendered.
269
+ // Double rAF is required because useReturnFocus restores focus to the overflow
270
+ // button on a single rAF after hide; the second frame runs after that restore.
271
+ pendingFocusRafOuterRef.current = requestAnimationFrame(() => {
272
+ pendingFocusRafInnerRef.current = requestAnimationFrame(() => {
273
+ const tabId = pendingFocusTabIdRef.current;
274
+ if (tabId) {
275
+ const tabElement = document.querySelector<HTMLElement>(
276
+ `[data-focus-id="${slugify(`${model.state.id}-${tabId}`)}"]`
277
+ );
278
+ if (tabElement) {
279
+ tabElement.focus();
280
+ }
281
+ pendingFocusTabIdRef.current = null;
282
+ }
283
+ });
284
+ });
127
285
  },
128
286
  onShow() {
129
287
  // Always select the first item when the menu is opened