@workday/canvas-kit-labs-react 16.1.4 → 16.2.0-0604-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 (66) hide show
  1. package/dist/commonjs/ai-ingress-button/lib/AIIngressButton.js +5 -5
  2. package/dist/commonjs/index.d.ts +1 -0
  3. package/dist/commonjs/index.d.ts.map +1 -1
  4. package/dist/commonjs/index.js +1 -0
  5. package/dist/commonjs/kbd/lib/KBD.js +6 -6
  6. package/dist/commonjs/kbd/lib/KBDItem.js +6 -6
  7. package/dist/commonjs/selection-group/index.d.ts +7 -0
  8. package/dist/commonjs/selection-group/index.d.ts.map +1 -0
  9. package/dist/commonjs/selection-group/index.js +22 -0
  10. package/dist/commonjs/selection-group/lib/SelectionGroup.d.ts +1499 -0
  11. package/dist/commonjs/selection-group/lib/SelectionGroup.d.ts.map +1 -0
  12. package/dist/commonjs/selection-group/lib/SelectionGroup.js +61 -0
  13. package/dist/commonjs/selection-group/lib/SelectionGroupIcon.d.ts +17 -0
  14. package/dist/commonjs/selection-group/lib/SelectionGroupIcon.d.ts.map +1 -0
  15. package/dist/commonjs/selection-group/lib/SelectionGroupIcon.js +24 -0
  16. package/dist/commonjs/selection-group/lib/SelectionGroupItem.d.ts +173 -0
  17. package/dist/commonjs/selection-group/lib/SelectionGroupItem.d.ts.map +1 -0
  18. package/dist/commonjs/selection-group/lib/SelectionGroupItem.js +37 -0
  19. package/dist/commonjs/selection-group/lib/SelectionGroupList.d.ts +172 -0
  20. package/dist/commonjs/selection-group/lib/SelectionGroupList.d.ts.map +1 -0
  21. package/dist/commonjs/selection-group/lib/SelectionGroupList.js +35 -0
  22. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.d.ts +146 -0
  23. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.d.ts.map +1 -0
  24. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.js +29 -0
  25. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.d.ts +1602 -0
  26. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.d.ts.map +1 -0
  27. package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.js +166 -0
  28. package/dist/commonjs/version/lib/version.js +1 -1
  29. package/dist/es6/ai-ingress-button/lib/AIIngressButton.js +5 -5
  30. package/dist/es6/index.d.ts +1 -0
  31. package/dist/es6/index.d.ts.map +1 -1
  32. package/dist/es6/index.js +1 -0
  33. package/dist/es6/kbd/lib/KBD.js +6 -6
  34. package/dist/es6/kbd/lib/KBDItem.js +6 -6
  35. package/dist/es6/selection-group/index.d.ts +7 -0
  36. package/dist/es6/selection-group/index.d.ts.map +1 -0
  37. package/dist/es6/selection-group/index.js +6 -0
  38. package/dist/es6/selection-group/lib/SelectionGroup.d.ts +1499 -0
  39. package/dist/es6/selection-group/lib/SelectionGroup.d.ts.map +1 -0
  40. package/dist/es6/selection-group/lib/SelectionGroup.js +58 -0
  41. package/dist/es6/selection-group/lib/SelectionGroupIcon.d.ts +17 -0
  42. package/dist/es6/selection-group/lib/SelectionGroupIcon.d.ts.map +1 -0
  43. package/dist/es6/selection-group/lib/SelectionGroupIcon.js +21 -0
  44. package/dist/es6/selection-group/lib/SelectionGroupItem.d.ts +173 -0
  45. package/dist/es6/selection-group/lib/SelectionGroupItem.d.ts.map +1 -0
  46. package/dist/es6/selection-group/lib/SelectionGroupItem.js +34 -0
  47. package/dist/es6/selection-group/lib/SelectionGroupList.d.ts +172 -0
  48. package/dist/es6/selection-group/lib/SelectionGroupList.d.ts.map +1 -0
  49. package/dist/es6/selection-group/lib/SelectionGroupList.js +32 -0
  50. package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.d.ts +146 -0
  51. package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.d.ts.map +1 -0
  52. package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.js +26 -0
  53. package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.d.ts +1602 -0
  54. package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.d.ts.map +1 -0
  55. package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.js +160 -0
  56. package/dist/es6/version/lib/version.js +1 -1
  57. package/index.ts +1 -0
  58. package/package.json +4 -4
  59. package/selection-group/index.ts +6 -0
  60. package/selection-group/lib/SelectionGroup.tsx +65 -0
  61. package/selection-group/lib/SelectionGroupIcon.tsx +60 -0
  62. package/selection-group/lib/SelectionGroupItem.tsx +143 -0
  63. package/selection-group/lib/SelectionGroupList.tsx +89 -0
  64. package/selection-group/lib/hooks/useSelectionGroupItem.tsx +46 -0
  65. package/selection-group/lib/hooks/useSelectionGroupModel.tsx +195 -0
  66. package/selection-group/package.json +6 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSelectionGroupModel.d.ts","sourceRoot":"","sources":["../../../../../selection-group/lib/hooks/useSelectionGroupModel.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import { multiSelectionManager, singleSelectionManager, useListModel, } from '@workday/canvas-kit-react/collection';
3
+ import { createModelHook } from '@workday/canvas-kit-react/common';
4
+ /**
5
+ * `createModelHook` runs a model's guards and callbacks unless the event it is wrapping is already
6
+ * a wrapped event. The overrides below delegate to events from the list model, which runs those
7
+ * same guards and callbacks because both models share a config, so they are marked as wrapped too.
8
+ * Without this, the `onGoTo*` callbacks would fire twice for a single key press.
9
+ */
10
+ const asWrappedEvent = (fn) => {
11
+ fn._wrapped = true;
12
+ return fn;
13
+ };
14
+ export const useSelectionGroupModel = createModelHook({
15
+ defaultConfig: {
16
+ ...useListModel.defaultConfig,
17
+ /**
18
+ * Optional id for the whole `SelectionGroup`. If not provided, a unique id will be created.
19
+ * @default useUniqueId()
20
+ */
21
+ id: '',
22
+ /**
23
+ * The selection mode of the group. `single` allows one selection at a time and behaves like a
24
+ * radio group. `multiple` allows more than one selection and behaves like a checkbox group.
25
+ *
26
+ * This drives more than selection - it also determines the `radio`/`checkbox` role of an item,
27
+ * the `radiogroup`/`group` role of the list, whether the group is one tab stop or many, and the
28
+ * shape of the item's icon. The matching `selection` manager is derived from it, so most groups
29
+ * should set `mode` rather than passing a `selection` manager directly.
30
+ * @default 'single'
31
+ */
32
+ mode: 'single',
33
+ /**
34
+ * The selection manager the underlying list model uses. Leave this unset unless you need
35
+ * selection to behave differently from what `mode` provides - `mode` picks
36
+ * `singleSelectionManager` or `multiSelectionManager` for you. Setting this does not change
37
+ * the roles or keyboard behavior that `mode` controls.
38
+ * @default `singleSelectionManager`, or `multiSelectionManager` when `mode` is `'multiple'`
39
+ */
40
+ selection: undefined,
41
+ /**
42
+ * Controls how item widths are calculated within the group. All items in a group use the same
43
+ * sizing method.
44
+ * - `content`: each item is sized to its own label.
45
+ * - `equal`: each item is sized to match the widest item.
46
+ * - `column`: each item spans the full width of the group.
47
+ * @default 'content'
48
+ */
49
+ width: 'content',
50
+ /**
51
+ * The initially selected ids of the group. Use this for uncontrolled groups. For `single` mode,
52
+ * provide a single id. If not provided, nothing is selected until the user interacts.
53
+ */
54
+ initialSelectedIds: [],
55
+ /**
56
+ * The selected ids of the group. Providing this prop puts the model in a controlled state: the
57
+ * group renders exactly what is passed in and will not change on its own. Use `onSelect` to
58
+ * respond to selection.
59
+ */
60
+ selectedIds: undefined,
61
+ /**
62
+ * Sets disabled state for every item in the group. To disable individual items instead, pass
63
+ * their ids to `nonInteractiveIds` so keyboard navigation skips over them.
64
+ * @default false
65
+ */
66
+ disabled: false,
67
+ /**
68
+ * The type of error associated with the group (if applicable). Notification states apply to the
69
+ * whole group rather than to individual items.
70
+ *
71
+ * This prop only changes the group's border color, which is not sufficient on its own. Always
72
+ * pair it with visible text describing the state and reference that text with
73
+ * `aria-describedby`. Wrapping the group in a `FormField` with a `FormField.Hint` does this
74
+ * for you.
75
+ */
76
+ error: undefined,
77
+ /**
78
+ * If true, the group is marked invalid to assistive technology. Only applies when `mode` is
79
+ * `'single'`, because `aria-invalid` is not allowed on the `group` role that a multi-select
80
+ * group renders.
81
+ * @default true when `error` is `'error'`, otherwise undefined
82
+ */
83
+ 'aria-invalid': undefined,
84
+ /**
85
+ * The id of the element describing the group, such as the hint text of a `FormField`. This is
86
+ * set automatically when the group is used as a `FormField.Input`.
87
+ */
88
+ 'aria-describedby': undefined,
89
+ /**
90
+ * The orientation of keyboard navigation within the group.
91
+ * @default 'horizontal'
92
+ */
93
+ orientation: 'horizontal',
94
+ },
95
+ requiredConfig: useListModel.requiredConfig,
96
+ })(config => {
97
+ var _a;
98
+ const isControlled = config.selectedIds !== undefined;
99
+ const selection = config.selection ||
100
+ (config.mode === 'multiple' ? multiSelectionManager : singleSelectionManager);
101
+ const listModel = useListModel(useListModel.mergeConfig(config, {
102
+ orientation: config.orientation || 'horizontal',
103
+ selection,
104
+ initialSelectedIds: config.selectedIds || config.initialSelectedIds,
105
+ shouldVirtualize: false,
106
+ // `mergeConfig` combines this with a `shouldSelect` from the consumer rather than replacing
107
+ // it. Guarding here means a blocked selection never reaches `onSelect`.
108
+ shouldSelect: () => !config.disabled,
109
+ }));
110
+ // A single-select group is a radio group, so selection follows focus while arrowing through
111
+ // items. The cursor events below set this, and the effect selects whatever the cursor landed on.
112
+ const shouldSelectCursorRef = React.useRef(false);
113
+ // A navigation event doesn't always move the cursor - a single-item group, or a non-wrapping
114
+ // navigation manager at either end of the list, leaves it where it is. Counting the events gives
115
+ // the effect below something that always changes, so an arrow key still selects in those cases.
116
+ const [navigationCount, setNavigationCount] = React.useState(0);
117
+ const state = {
118
+ ...listModel.state,
119
+ mode: config.mode,
120
+ width: config.width,
121
+ disabled: config.disabled,
122
+ error: config.error,
123
+ 'aria-invalid': (_a = config['aria-invalid']) !== null && _a !== void 0 ? _a : (config.error === 'error' ? true : undefined),
124
+ 'aria-describedby': config['aria-describedby'],
125
+ selectedIds: isControlled ? config.selectedIds : listModel.state.selectedIds,
126
+ };
127
+ const navigationEvents = Object.keys(listModel.events).filter(name => name.startsWith('goTo') && name !== 'goTo');
128
+ const selectFollowsFocusEvents = navigationEvents.reduce((result, eventName) => {
129
+ result[eventName] = asWrappedEvent(() => {
130
+ // Let the cursor model resolve the target and move the cursor. Navigation skips
131
+ // `nonInteractiveIds`, so this can't land selection on a non-interactive item.
132
+ listModel.events[eventName]();
133
+ shouldSelectCursorRef.current = true;
134
+ setNavigationCount(count => count + 1);
135
+ });
136
+ return result;
137
+ }, {});
138
+ const events = {
139
+ ...listModel.events,
140
+ ...(config.mode === 'single' ? selectFollowsFocusEvents : {}),
141
+ };
142
+ const { cursorId } = listModel.state;
143
+ React.useEffect(() => {
144
+ if (!shouldSelectCursorRef.current) {
145
+ return;
146
+ }
147
+ shouldSelectCursorRef.current = false;
148
+ if (typeof cursorId === 'string' && cursorId && !state.nonInteractiveIds.includes(cursorId)) {
149
+ listModel.events.select({ id: cursorId });
150
+ }
151
+ // Selection follows the cursor, so this should only run after a navigation event, whether or
152
+ // not that event moved the cursor.
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
154
+ }, [cursorId, navigationCount]);
155
+ return {
156
+ ...listModel,
157
+ state,
158
+ events,
159
+ };
160
+ });
@@ -1 +1 @@
1
- export const version = '16.1.4';
1
+ export const version = '16.1.1';
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './ai-ingress-button';
2
2
  export * from './kbd';
3
+ export * from './selection-group';
3
4
  export * from './version';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workday/canvas-kit-labs-react",
3
- "version": "16.1.4",
3
+ "version": "16.2.0-0604-next.0",
4
4
  "description": "Canvas Kit Labs is an incubator for new and experimental components. Since we have a rather rigorous process for getting components in at a production level, it can be valuable to make them available earlier while we continuously iterate on the API/functionality. The Labs modules allow us to do that as needed.",
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.4",
52
- "@workday/canvas-kit-styling": "^16.1.4",
51
+ "@workday/canvas-kit-react": "^16.2.0-0604-next.0",
52
+ "@workday/canvas-kit-styling": "^16.2.0-0604-next.0",
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
  "devDependencies": {
61
61
  "@types/lodash.flatten": "^4.4.6"
62
62
  },
63
- "gitHead": "431b7a4d30f20e2b836e1db1a7edcab005308f30"
63
+ "gitHead": "58ce933e857e255360611f2330a230b167cead8d"
64
64
  }
@@ -0,0 +1,6 @@
1
+ export * from './lib/SelectionGroup';
2
+ export * from './lib/SelectionGroupIcon';
3
+ export * from './lib/SelectionGroupList';
4
+ export * from './lib/SelectionGroupItem';
5
+ export * from './lib/hooks/useSelectionGroupModel';
6
+ export * from './lib/hooks/useSelectionGroupItem';
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+
3
+ import {createContainer} from '@workday/canvas-kit-react/common';
4
+
5
+ import {SelectionGroupItem} from './SelectionGroupItem';
6
+ import {SelectionGroupList} from './SelectionGroupList';
7
+ import {useSelectionGroupModel} from './hooks/useSelectionGroupModel';
8
+
9
+ export interface SelectionGroupProps {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ /**
14
+ * `SelectionGroup` is a container component that allows users to select one or more options from a
15
+ * predefined list using chip-style controls with a lightweight glyph indicator. It is responsible
16
+ * for creating a {@link useSelectionGroupModel SelectionGroupModel} and sharing it with its
17
+ * subcomponents using React context. It does not represent a real element.
18
+ *
19
+ * ```tsx
20
+ * <SelectionGroup mode="single">
21
+ * <SelectionGroup.List aria-label="Select one option">
22
+ * <SelectionGroup.Item data-id="first">First</SelectionGroup.Item>
23
+ * <SelectionGroup.Item data-id="second">Second</SelectionGroup.Item>
24
+ * </SelectionGroup.List>
25
+ * </SelectionGroup>
26
+ * ```
27
+ *
28
+ * Alternatively, you may pass in a model using the [hoisted model
29
+ * pattern](/getting-started/for-developers/resources/compound-components/#configuring-a-model).
30
+ *
31
+ * ```tsx
32
+ * const model = useSelectionGroupModel({mode: 'multiple'});
33
+ *
34
+ * <SelectionGroup model={model}>{Child components}</SelectionGroup>
35
+ * ```
36
+ */
37
+ export const SelectionGroup = createContainer()({
38
+ displayName: 'SelectionGroup',
39
+ modelHook: useSelectionGroupModel,
40
+ subComponents: {
41
+ /**
42
+ * `SelectionGroup.List` is a container for {@link SelectionGroupItem SelectionGroup.Item}
43
+ * subcomponents. It renders a `radiogroup` in `single` mode and a `group` in `multiple` mode,
44
+ * and carries the group's accessible label and notification state.
45
+ *
46
+ * ```tsx
47
+ * <SelectionGroup.List aria-label="Select all that apply">
48
+ * {SelectionGroup.Items}
49
+ * </SelectionGroup.List>
50
+ * ```
51
+ */
52
+ List: SelectionGroupList,
53
+ /**
54
+ * `SelectionGroup.Item` is a `button` element with a glyph and a label. Each item requires a
55
+ * `data-id` prop to handle selection events properly.
56
+ *
57
+ * ```tsx
58
+ * <SelectionGroup.Item data-id="option-a">Option A</SelectionGroup.Item>
59
+ * ```
60
+ */
61
+ Item: SelectionGroupItem,
62
+ },
63
+ })<SelectionGroupProps>(({children}) => {
64
+ return <>{children}</>;
65
+ });
@@ -0,0 +1,60 @@
1
+ import {ExtractProps, createComponent} from '@workday/canvas-kit-react/common';
2
+ import {SystemIcon} from '@workday/canvas-kit-react/icon';
3
+ import {createStencil, handleCsProp} from '@workday/canvas-kit-styling';
4
+ import {
5
+ checkCircleFillIcon,
6
+ checkboxFillIcon,
7
+ circleIcon,
8
+ squareIcon,
9
+ } from '@workday/canvas-system-icons-web';
10
+
11
+ import {SelectionGroupMode} from './hooks/useSelectionGroupModel';
12
+
13
+ export interface SelectionGroupIconProps
14
+ extends Omit<ExtractProps<typeof SystemIcon, never>, 'icon'> {
15
+ /**
16
+ * The selection mode of the group the item belongs to. A circle is used for `single` and a
17
+ * square for `multiple`, matching the radio and checkbox controls each mode behaves like.
18
+ */
19
+ mode: SelectionGroupMode;
20
+ /**
21
+ * If true, the checked form of the icon is rendered.
22
+ */
23
+ selected: boolean;
24
+ }
25
+
26
+ export const selectionGroupIconStencil = createStencil({
27
+ base: {
28
+ flexShrink: 0,
29
+ },
30
+ });
31
+
32
+ /**
33
+ * The icon is always rendered, in both modes and in both selected states, so items never shift
34
+ * width on selection. The shape communicates the mode and the checkmark communicates selection, so
35
+ * selected state is never conveyed by color alone.
36
+ */
37
+ const icons = {
38
+ single: {selected: checkCircleFillIcon, unselected: circleIcon},
39
+ multiple: {selected: checkboxFillIcon, unselected: squareIcon},
40
+ } as const;
41
+
42
+ export const SelectionGroupIcon = createComponent('span')({
43
+ displayName: 'SelectionGroupIcon',
44
+ Component: (
45
+ {mode, selected, size = 'xs', ...elemProps}: SelectionGroupIconProps,
46
+ ref,
47
+ Element
48
+ ) => (
49
+ <SystemIcon
50
+ as={Element}
51
+ ref={ref}
52
+ size={size}
53
+ // The item's `aria-checked` already conveys selection and its label provides the accessible
54
+ // name, so the icon is decorative.
55
+ aria-hidden={true}
56
+ icon={icons[mode][selected ? 'selected' : 'unselected']}
57
+ {...handleCsProp(elemProps, selectionGroupIconStencil())}
58
+ />
59
+ ),
60
+ });
@@ -0,0 +1,143 @@
1
+ import * as React from 'react';
2
+
3
+ import {isSelected} from '@workday/canvas-kit-react/collection';
4
+ import {cornerShapeStencil, createSubcomponent, focusRing} from '@workday/canvas-kit-react/common';
5
+ import {systemIconStencil} from '@workday/canvas-kit-react/icon';
6
+ import {createStencil, handleCsProp, px2rem} from '@workday/canvas-kit-styling';
7
+ import {base, system} from '@workday/canvas-tokens-web';
8
+
9
+ import {SelectionGroupIcon} from './SelectionGroupIcon';
10
+ import {useSelectionGroupItem} from './hooks/useSelectionGroupItem';
11
+ import {useSelectionGroupModel} from './hooks/useSelectionGroupModel';
12
+
13
+ export interface SelectionGroupItemProps {
14
+ /**
15
+ * Optionally pass index to the item. This should be done if `SelectionGroup.Item` components were
16
+ * created via a `map` function. This index will ensure keyboard navigation works even if items
17
+ * are inserted out of order.
18
+ */
19
+ index?: number;
20
+ /**
21
+ * The contents of the item. This is text used as the accessible name of the button for screen
22
+ * readers. Labels should be short and concise.
23
+ */
24
+ children?: React.ReactNode;
25
+ /**
26
+ * The identifier of the item. This identifier will be used in change events.
27
+ * If this property is not provided, it will default to a string representation
28
+ * of the zero-based index of the item when it was initialized.
29
+ */
30
+ 'data-id'?: string;
31
+ /**
32
+ * Optional id. If not set, it will inherit the ID passed to the `SelectionGroup` component and
33
+ * append the index at the end. Only set this for advanced cases.
34
+ */
35
+ id?: string;
36
+ }
37
+
38
+ export const selectionGroupItemStencil = createStencil({
39
+ extends: cornerShapeStencil,
40
+ parts: {
41
+ label: 'selection-group-item-label',
42
+ },
43
+ base: ({labelPart}) => ({
44
+ [cornerShapeStencil.vars.shape]: system.legacy.shape.lg,
45
+ ...system.legacy.type.subtext.lg,
46
+ display: 'inline-flex',
47
+ alignItems: 'center',
48
+ gap: base.legacy.size75,
49
+ border: `${px2rem(1)} solid ${system.color.border.default}`,
50
+ padding: `${system.legacy.padding.xs} ${system.legacy.padding.sm}`,
51
+ backgroundColor: system.legacy.color.surface.default,
52
+ color: system.color.fg.strong,
53
+ cursor: 'pointer',
54
+ textAlign: 'start',
55
+ transition: 'background-color 80ms ease, color 80ms ease, border-color 80ms ease',
56
+ [systemIconStencil.vars.color]: 'currentColor',
57
+ [systemIconStencil.vars.accentColor]: 'currentColor',
58
+
59
+ [labelPart]: {
60
+ whiteSpace: 'nowrap',
61
+ },
62
+
63
+ '&:not([aria-checked="true"]):is(:hover, .hover)': {
64
+ backgroundColor: system.legacy.color.surface.overlay.hover.default,
65
+ },
66
+
67
+ '&:not([aria-checked="true"]):is(:active, .active)': {
68
+ backgroundColor: system.legacy.color.surface.overlay.pressed.default,
69
+ },
70
+
71
+ // The ring sits outside the item so it never competes with the selected background, and so a
72
+ // selected item and a focused item remain visually distinct.
73
+ '&:is(:focus-visible, .focus)': {
74
+ ...focusRing({separation: 1}),
75
+ },
76
+
77
+ '&:is(:disabled, .disabled)': {
78
+ cursor: 'default',
79
+ opacity: system.opacity.disabled,
80
+
81
+ '&:is(:hover, .hover)': {
82
+ backgroundColor: system.legacy.color.surface.default,
83
+ },
84
+ },
85
+
86
+ // No border on a selected item - a selected border would compete with the focus ring. The
87
+ // transparent border keeps the item the same size as an unselected one.
88
+ '&[aria-checked="true"]': {
89
+ backgroundColor: system.legacy.color.brand.surface.selected,
90
+ border: `${px2rem(1)} solid transparent`,
91
+ color: system.legacy.color.brand.fg.selected,
92
+
93
+ '&:is(:hover, .hover)': {
94
+ backgroundColor: system.legacy.color.brand.surface.selected,
95
+ },
96
+
97
+ '&:is(:active, .active)': {
98
+ backgroundColor: system.legacy.color.brand.surface.selected,
99
+ },
100
+
101
+ '&:is(:disabled, .disabled)': {
102
+ backgroundColor: system.legacy.color.brand.surface.selected,
103
+ color: system.legacy.color.brand.fg.selected,
104
+ },
105
+ },
106
+ }),
107
+ modifiers: {
108
+ width: {
109
+ content: {
110
+ width: 'fit-content',
111
+ },
112
+ equal: {
113
+ width: '100%',
114
+ },
115
+ column: {
116
+ width: '100%',
117
+ },
118
+ },
119
+ },
120
+ });
121
+
122
+ export const SelectionGroupItem = createSubcomponent('button')({
123
+ displayName: 'SelectionGroup.Item',
124
+ modelHook: useSelectionGroupModel,
125
+ elemPropsHook: useSelectionGroupItem,
126
+ })<SelectionGroupItemProps>(({children, ...elemProps}, Element, model) => {
127
+ const name = elemProps['data-id'] || '';
128
+ const selected = !!name && isSelected(name, model.state);
129
+
130
+ return (
131
+ <Element
132
+ {...handleCsProp(
133
+ elemProps,
134
+ selectionGroupItemStencil({
135
+ width: model.state.width,
136
+ })
137
+ )}
138
+ >
139
+ <SelectionGroupIcon mode={model.state.mode} selected={selected} />
140
+ {children && <span {...selectionGroupItemStencil.parts.label}>{children}</span>}
141
+ </Element>
142
+ );
143
+ });
@@ -0,0 +1,89 @@
1
+ import * as React from 'react';
2
+
3
+ import {useListRenderItems} from '@workday/canvas-kit-react/collection';
4
+ import {cornerShapeStencil, createSubcomponent} from '@workday/canvas-kit-react/common';
5
+ import {CSProps, createStencil, handleCsProp, px2rem} from '@workday/canvas-kit-styling';
6
+ import {system} from '@workday/canvas-tokens-web';
7
+
8
+ import {useSelectionGroupModel} from './hooks/useSelectionGroupModel';
9
+
10
+ export interface SelectionGroupListProps<T = any> extends CSProps {
11
+ /**
12
+ * Accessible label for the group. The label should state the selection mode, for example
13
+ * `"Select one"` or `"Select all that apply"`, so the mode is clear before interacting. Required
14
+ * unless `aria-labelledby` is provided or the group is wrapped in a `FormField` with a visible
15
+ * label.
16
+ */
17
+ 'aria-label'?: string;
18
+ /**
19
+ * ID of an element that labels the group. Use when the group label is rendered outside the list.
20
+ */
21
+ 'aria-labelledby'?: string;
22
+ children: ((item: T) => React.ReactNode) | React.ReactNode;
23
+ }
24
+
25
+ export const selectionGroupListStencil = createStencil({
26
+ extends: cornerShapeStencil,
27
+ base: {
28
+ [cornerShapeStencil.vars.shape]: system.legacy.shape.lg,
29
+ display: 'flex',
30
+ flexWrap: 'wrap',
31
+ gap: system.legacy.gap.xs,
32
+ width: 'fit-content',
33
+ boxSizing: 'border-box',
34
+ // Notification states draw a ring around the whole group, so the container always reserves the
35
+ // space the ring occupies. Without it the ring would sit underneath the opaque items.
36
+ border: `${px2rem(2)} solid transparent`,
37
+ padding: system.legacy.padding.xxs,
38
+ transition: '100ms border-color',
39
+ },
40
+ modifiers: {
41
+ width: {
42
+ content: {},
43
+ // `1fr` auto columns inside a `fit-content` grid all resolve to the widest item's
44
+ // max-content width, which sizes every item to the longest label without measuring the DOM.
45
+ equal: {
46
+ display: 'grid',
47
+ gridAutoFlow: 'column',
48
+ gridAutoColumns: '1fr',
49
+ },
50
+ column: {
51
+ flexDirection: 'column',
52
+ flexWrap: 'nowrap',
53
+ width: '100%',
54
+ },
55
+ },
56
+ error: {
57
+ error: {
58
+ borderColor: system.legacy.color.brand.border.critical,
59
+ },
60
+ caution: {
61
+ borderColor: system.legacy.color.brand.border.caution,
62
+ },
63
+ },
64
+ },
65
+ });
66
+
67
+ export const SelectionGroupList = createSubcomponent('div')({
68
+ displayName: 'SelectionGroup.List',
69
+ modelHook: useSelectionGroupModel,
70
+ })<SelectionGroupListProps>(({children, ...elemProps}, Element, model) => {
71
+ return (
72
+ <Element
73
+ role={model.state.mode === 'single' ? 'radiogroup' : 'group'}
74
+ // `aria-invalid` is not an allowed attribute of the `group` role, so a multi-select group
75
+ // conveys its error state through the text referenced by `aria-describedby` instead.
76
+ aria-invalid={model.state.mode === 'single' ? model.state['aria-invalid'] : undefined}
77
+ aria-describedby={model.state['aria-describedby']}
78
+ {...handleCsProp(
79
+ elemProps,
80
+ selectionGroupListStencil({
81
+ width: model.state.width,
82
+ error: model.state.error,
83
+ })
84
+ )}
85
+ >
86
+ {useListRenderItems(model, children)}
87
+ </Element>
88
+ );
89
+ });
@@ -0,0 +1,46 @@
1
+ import {
2
+ isSelected,
3
+ useListItemRegister,
4
+ useListItemRovingFocus,
5
+ useListItemSelect,
6
+ } from '@workday/canvas-kit-react/collection';
7
+ import {composeHooks, createElemPropsHook} from '@workday/canvas-kit-react/common';
8
+
9
+ import {useSelectionGroupModel} from './useSelectionGroupModel';
10
+
11
+ const useSelectionGroupItemProps = createElemPropsHook(useSelectionGroupModel)((
12
+ model,
13
+ _ref,
14
+ elemProps: {'data-id'?: string} = {}
15
+ ) => {
16
+ const name = elemProps['data-id'] || '';
17
+
18
+ return {
19
+ role: model.state.mode === 'single' ? 'radio' : 'checkbox',
20
+ 'aria-checked': !!name && isSelected(name, model.state),
21
+ type: 'button',
22
+ // A single-select group is a radio group and uses a single roving tab stop. A multi-select
23
+ // group is a checkbox group, where every item is its own tab stop, so the roving tabindex
24
+ // applied by `useListItemRovingFocus` is overridden here.
25
+ ...(model.state.mode === 'multiple' ? {tabIndex: 0} : {}),
26
+ };
27
+ });
28
+
29
+ const useSelectionGroupItemDisabled = createElemPropsHook(useSelectionGroupModel)(({state}) => {
30
+ return {
31
+ // `useListItemRegister` resolves the item's own `disabled` prop and `nonInteractiveIds`, but
32
+ // it doesn't know about the group-level `disabled`. The key is only added when the group is
33
+ // disabled so that an `undefined` doesn't overwrite what `useListItemRegister` resolved. The
34
+ // native `disabled` attribute already conveys the state to assistive technology, so no
35
+ // `aria-disabled` is added alongside it.
36
+ ...(state.disabled ? {disabled: true} : {}),
37
+ };
38
+ });
39
+
40
+ export const useSelectionGroupItem = composeHooks(
41
+ useListItemSelect,
42
+ useListItemRovingFocus,
43
+ useSelectionGroupItemProps,
44
+ useListItemRegister,
45
+ useSelectionGroupItemDisabled
46
+ );