@workday/canvas-kit-labs-react 16.1.3 → 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.
- package/dist/commonjs/ai-ingress-button/lib/AIIngressButton.js +5 -5
- package/dist/commonjs/index.d.ts +1 -0
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +1 -0
- package/dist/commonjs/kbd/lib/KBD.js +6 -6
- package/dist/commonjs/kbd/lib/KBDItem.js +6 -6
- package/dist/commonjs/selection-group/index.d.ts +7 -0
- package/dist/commonjs/selection-group/index.d.ts.map +1 -0
- package/dist/commonjs/selection-group/index.js +22 -0
- package/dist/commonjs/selection-group/lib/SelectionGroup.d.ts +1499 -0
- package/dist/commonjs/selection-group/lib/SelectionGroup.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/SelectionGroup.js +61 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupIcon.d.ts +17 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupIcon.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupIcon.js +24 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupItem.d.ts +173 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupItem.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupItem.js +37 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupList.d.ts +172 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupList.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/SelectionGroupList.js +35 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.d.ts +146 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupItem.js +29 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.d.ts +1602 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.d.ts.map +1 -0
- package/dist/commonjs/selection-group/lib/hooks/useSelectionGroupModel.js +166 -0
- package/dist/commonjs/version/lib/version.js +1 -1
- package/dist/es6/ai-ingress-button/lib/AIIngressButton.js +5 -5
- package/dist/es6/index.d.ts +1 -0
- package/dist/es6/index.d.ts.map +1 -1
- package/dist/es6/index.js +1 -0
- package/dist/es6/kbd/lib/KBD.js +6 -6
- package/dist/es6/kbd/lib/KBDItem.js +6 -6
- package/dist/es6/selection-group/index.d.ts +7 -0
- package/dist/es6/selection-group/index.d.ts.map +1 -0
- package/dist/es6/selection-group/index.js +6 -0
- package/dist/es6/selection-group/lib/SelectionGroup.d.ts +1499 -0
- package/dist/es6/selection-group/lib/SelectionGroup.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/SelectionGroup.js +58 -0
- package/dist/es6/selection-group/lib/SelectionGroupIcon.d.ts +17 -0
- package/dist/es6/selection-group/lib/SelectionGroupIcon.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/SelectionGroupIcon.js +21 -0
- package/dist/es6/selection-group/lib/SelectionGroupItem.d.ts +173 -0
- package/dist/es6/selection-group/lib/SelectionGroupItem.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/SelectionGroupItem.js +34 -0
- package/dist/es6/selection-group/lib/SelectionGroupList.d.ts +172 -0
- package/dist/es6/selection-group/lib/SelectionGroupList.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/SelectionGroupList.js +32 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.d.ts +146 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupItem.js +26 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.d.ts +1602 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.d.ts.map +1 -0
- package/dist/es6/selection-group/lib/hooks/useSelectionGroupModel.js +160 -0
- package/dist/es6/version/lib/version.js +1 -1
- package/index.ts +1 -0
- package/package.json +4 -4
- package/selection-group/index.ts +6 -0
- package/selection-group/lib/SelectionGroup.tsx +65 -0
- package/selection-group/lib/SelectionGroupIcon.tsx +60 -0
- package/selection-group/lib/SelectionGroupItem.tsx +143 -0
- package/selection-group/lib/SelectionGroupList.tsx +89 -0
- package/selection-group/lib/hooks/useSelectionGroupItem.tsx +46 -0
- package/selection-group/lib/hooks/useSelectionGroupModel.tsx +195 -0
- 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.
|
|
1
|
+
export const version = '16.1.1';
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workday/canvas-kit-labs-react",
|
|
3
|
-
"version": "16.
|
|
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.
|
|
52
|
-
"@workday/canvas-kit-styling": "^16.
|
|
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": "
|
|
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
|
+
);
|