@khanacademy/wonder-blocks-dropdown 2.7.3 → 2.7.6
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/CHANGELOG.md +24 -0
- package/dist/es/index.js +92 -162
- package/dist/index.js +285 -374
- package/package.json +6 -6
- package/src/components/__docs__/action-menu.argtypes.js +44 -0
- package/src/components/__docs__/action-menu.stories.js +435 -0
- package/src/components/__docs__/base-select.argtypes.js +54 -0
- package/src/components/__docs__/multi-select.stories.js +509 -0
- package/src/components/__docs__/single-select.accessibility.stories.mdx +59 -0
- package/src/components/__docs__/single-select.argtypes.js +54 -0
- package/src/components/__docs__/single-select.stories.js +464 -0
- package/src/components/__tests__/dropdown-core-virtualized.test.js +0 -15
- package/src/components/__tests__/dropdown-core.test.js +114 -208
- package/src/components/__tests__/multi-select.test.js +1 -3
- package/src/components/__tests__/single-select.test.js +15 -47
- package/src/components/action-menu.js +11 -0
- package/src/components/dropdown-core-virtualized.js +0 -5
- package/src/components/dropdown-core.js +140 -126
- package/src/components/multi-select.js +17 -33
- package/src/components/single-select.js +15 -30
- package/src/util/__tests__/dropdown-menu-styles.test.js +0 -26
- package/src/util/constants.js +0 -11
- package/src/util/dropdown-menu-styles.js +0 -5
- package/src/util/types.js +2 -5
- package/src/components/__tests__/search-text-input.test.js +0 -212
- package/src/components/action-menu.stories.js +0 -48
- package/src/components/multi-select.stories.js +0 -124
- package/src/components/search-text-input.js +0 -115
- package/src/components/single-select.stories.js +0 -247
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {StyleSheet} from "aphrodite";
|
|
4
|
+
|
|
5
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
6
|
+
|
|
7
|
+
import type {Labels} from "@khanacademy/wonder-blocks-dropdown";
|
|
8
|
+
import type {StoryComponentType} from "@storybook/react";
|
|
9
|
+
|
|
10
|
+
import Button from "@khanacademy/wonder-blocks-button";
|
|
11
|
+
import Color from "@khanacademy/wonder-blocks-color";
|
|
12
|
+
import {MultiSelect, OptionItem} from "@khanacademy/wonder-blocks-dropdown";
|
|
13
|
+
import {Checkbox} from "@khanacademy/wonder-blocks-form";
|
|
14
|
+
import {OnePaneDialog, ModalLauncher} from "@khanacademy/wonder-blocks-modal";
|
|
15
|
+
import Spacing from "@khanacademy/wonder-blocks-spacing";
|
|
16
|
+
import {HeadingLarge} from "@khanacademy/wonder-blocks-typography";
|
|
17
|
+
|
|
18
|
+
import ComponentInfo from "../../../../../.storybook/components/component-info.js";
|
|
19
|
+
import {name, version} from "../../../package.json";
|
|
20
|
+
import multiSelectArgtypes from "./base-select.argtypes.js";
|
|
21
|
+
import {defaultLabels} from "../../util/constants.js";
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
title: "Dropdown / MultiSelect",
|
|
25
|
+
component: MultiSelect,
|
|
26
|
+
argTypes: {
|
|
27
|
+
...multiSelectArgtypes,
|
|
28
|
+
labels: {
|
|
29
|
+
defaultValue: defaultLabels,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
isFilterable: false,
|
|
34
|
+
opened: false,
|
|
35
|
+
disabled: false,
|
|
36
|
+
light: false,
|
|
37
|
+
shortcuts: false,
|
|
38
|
+
implicitAllEnabled: false,
|
|
39
|
+
id: "",
|
|
40
|
+
testId: "",
|
|
41
|
+
},
|
|
42
|
+
decorators: [
|
|
43
|
+
(Story: StoryComponentType): React.Element<typeof View> => (
|
|
44
|
+
<View style={styles.example}>
|
|
45
|
+
<Story />
|
|
46
|
+
</View>
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
parameters: {
|
|
50
|
+
componentSubtitle: ((
|
|
51
|
+
<ComponentInfo name={name} version={version} />
|
|
52
|
+
): any),
|
|
53
|
+
docs: {
|
|
54
|
+
description: {
|
|
55
|
+
component: null,
|
|
56
|
+
},
|
|
57
|
+
source: {
|
|
58
|
+
// See https://github.com/storybookjs/storybook/issues/12596
|
|
59
|
+
excludeDecorators: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const styles = StyleSheet.create({
|
|
66
|
+
example: {
|
|
67
|
+
background: Color.offWhite,
|
|
68
|
+
padding: Spacing.medium_16,
|
|
69
|
+
},
|
|
70
|
+
setWidth: {
|
|
71
|
+
minWidth: 170,
|
|
72
|
+
width: "100%",
|
|
73
|
+
},
|
|
74
|
+
customDropdown: {
|
|
75
|
+
maxHeight: 200,
|
|
76
|
+
},
|
|
77
|
+
wrapper: {
|
|
78
|
+
height: "400px",
|
|
79
|
+
width: "600px",
|
|
80
|
+
},
|
|
81
|
+
centered: {
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
justifyContent: "center",
|
|
84
|
+
},
|
|
85
|
+
scrolledWrapper: {
|
|
86
|
+
height: 200,
|
|
87
|
+
overflow: "auto",
|
|
88
|
+
border: "1px solid grey",
|
|
89
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
90
|
+
margin: Spacing.xSmall_8,
|
|
91
|
+
padding: Spacing.medium_16,
|
|
92
|
+
},
|
|
93
|
+
scrollableArea: {
|
|
94
|
+
height: "200vh",
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Custom opener styles
|
|
98
|
+
*/
|
|
99
|
+
customOpener: {
|
|
100
|
+
borderLeft: `5px solid ${Color.blue}`,
|
|
101
|
+
borderRadius: Spacing.xxxSmall_4,
|
|
102
|
+
background: Color.lightBlue,
|
|
103
|
+
color: Color.white,
|
|
104
|
+
padding: Spacing.medium_16,
|
|
105
|
+
},
|
|
106
|
+
focused: {
|
|
107
|
+
color: Color.offWhite,
|
|
108
|
+
},
|
|
109
|
+
hovered: {
|
|
110
|
+
textDecoration: "underline",
|
|
111
|
+
color: Color.offWhite,
|
|
112
|
+
cursor: "pointer",
|
|
113
|
+
},
|
|
114
|
+
pressed: {
|
|
115
|
+
color: Color.blue,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const items = [
|
|
120
|
+
<OptionItem label="Mercury" value="1" />,
|
|
121
|
+
<OptionItem label="Venus" value="2" />,
|
|
122
|
+
<OptionItem label="Earth" value="3" disabled />,
|
|
123
|
+
<OptionItem label="Mars" value="4" />,
|
|
124
|
+
<OptionItem label="Jupiter" value="5" />,
|
|
125
|
+
<OptionItem label="Saturn" value="6" />,
|
|
126
|
+
<OptionItem label="Neptune" value="7" />,
|
|
127
|
+
<OptionItem label="Uranus" value="8" />,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const Template = (args) => {
|
|
131
|
+
const [selectedValues, setSelectedValues] = React.useState(
|
|
132
|
+
args.selectedValues,
|
|
133
|
+
);
|
|
134
|
+
const [opened, setOpened] = React.useState(args.opened);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
// Only update opened if the args.opened prop changes (using the
|
|
137
|
+
// controls panel).
|
|
138
|
+
setOpened(args.opened);
|
|
139
|
+
}, [args.opened]);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<View style={styles.wrapper}>
|
|
143
|
+
<MultiSelect
|
|
144
|
+
{...args}
|
|
145
|
+
onChange={setSelectedValues}
|
|
146
|
+
selectedValues={selectedValues}
|
|
147
|
+
opened={opened}
|
|
148
|
+
onToggle={setOpened}
|
|
149
|
+
>
|
|
150
|
+
{items}
|
|
151
|
+
</MultiSelect>
|
|
152
|
+
</View>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const Default: StoryComponentType = Template.bind({});
|
|
157
|
+
|
|
158
|
+
export const ControlledOpened: StoryComponentType = (args) => {
|
|
159
|
+
const [selectedValues, setSelectedValues] = React.useState([]);
|
|
160
|
+
const [opened, setOpened] = React.useState(args.opened);
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
// Only update opened if the args.opened prop changes (using the
|
|
163
|
+
// controls panel).
|
|
164
|
+
setOpened(args.opened);
|
|
165
|
+
}, [args.opened]);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<View style={styles.wrapper}>
|
|
169
|
+
<Checkbox label="Open" onChange={setOpened} checked={opened} />
|
|
170
|
+
<MultiSelect
|
|
171
|
+
{...args}
|
|
172
|
+
onChange={setSelectedValues}
|
|
173
|
+
selectedValue={selectedValues}
|
|
174
|
+
opened={opened}
|
|
175
|
+
onToggle={setOpened}
|
|
176
|
+
>
|
|
177
|
+
{items}
|
|
178
|
+
</MultiSelect>
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
ControlledOpened.args = {
|
|
184
|
+
opened: true,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
ControlledOpened.storyName = "Controlled (opened)";
|
|
188
|
+
|
|
189
|
+
ControlledOpened.parameters = {
|
|
190
|
+
docs: {
|
|
191
|
+
description: {
|
|
192
|
+
story:
|
|
193
|
+
"Sometimes you'll want to trigger a dropdown programmatically. This can be done by `MultiSelect` is a controlled component. The parent is responsible for managing the opening/closing of the dropdown when using this prop.\n\n" +
|
|
194
|
+
"This means that you'll also have to update `opened` to the value triggered by the `onToggle` prop.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
// Added to ensure that the dropdown menu is rendered using PopperJS.
|
|
198
|
+
chromatic: {delay: 400},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Custom MultiSelect labels
|
|
202
|
+
const dropdownLabels: $Shape<Labels> = {
|
|
203
|
+
noneSelected: "Solar system",
|
|
204
|
+
someSelected: (numSelectedValues) => `${numSelectedValues} planets`,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const CustomStyles: StoryComponentType = Template.bind({});
|
|
208
|
+
|
|
209
|
+
CustomStyles.args = {
|
|
210
|
+
labels: dropdownLabels,
|
|
211
|
+
dropdownStyle: styles.customDropdown,
|
|
212
|
+
style: styles.setWidth,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
CustomStyles.parameters = {
|
|
216
|
+
docs: {
|
|
217
|
+
description: {
|
|
218
|
+
story: "Sometimes, we may want to customize the dropdown style (for example, to limit the height of the list). For this purpose, we have the `dropdownStyle` prop.",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
chromatic: {
|
|
222
|
+
// we don't need screenshots because this story only tests behavior.
|
|
223
|
+
disableSnapshot: true,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const CustomStylesOpened: StoryComponentType = Template.bind({});
|
|
228
|
+
|
|
229
|
+
CustomStylesOpened.args = {
|
|
230
|
+
labels: dropdownLabels,
|
|
231
|
+
dropdownStyle: styles.customDropdown,
|
|
232
|
+
style: styles.setWidth,
|
|
233
|
+
opened: true,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
CustomStylesOpened.storyName = "Custom styles (opened)";
|
|
237
|
+
|
|
238
|
+
CustomStylesOpened.parameters = {
|
|
239
|
+
docs: {
|
|
240
|
+
description: {
|
|
241
|
+
story: "Here you can see an example of the previous dropdown opened",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* With shortcuts
|
|
248
|
+
*/
|
|
249
|
+
export const Shortcuts: StoryComponentType = Template.bind({});
|
|
250
|
+
|
|
251
|
+
Shortcuts.args = {
|
|
252
|
+
shortcuts: true,
|
|
253
|
+
opened: true,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
Shortcuts.parameters = {
|
|
257
|
+
docs: {
|
|
258
|
+
description: {
|
|
259
|
+
story: "This example starts with one item selected and has selection shortcuts for select all and select none. This one does not have a predefined placeholder.",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* In a Modal
|
|
266
|
+
*/
|
|
267
|
+
export const DropdownInModal: StoryComponentType = (args) => {
|
|
268
|
+
const [selectedValues, setSelectedValues] = React.useState([]);
|
|
269
|
+
const [opened, setOpened] = React.useState(true);
|
|
270
|
+
|
|
271
|
+
const modalContent = (
|
|
272
|
+
<View style={styles.scrollableArea}>
|
|
273
|
+
<View style={styles.scrolledWrapper}>
|
|
274
|
+
<View style={{minHeight: "100vh"}}>
|
|
275
|
+
<MultiSelect
|
|
276
|
+
{...args}
|
|
277
|
+
onChange={setSelectedValues}
|
|
278
|
+
isFilterable={true}
|
|
279
|
+
opened={opened}
|
|
280
|
+
onToggle={setOpened}
|
|
281
|
+
selectedValues={selectedValues}
|
|
282
|
+
>
|
|
283
|
+
{items}
|
|
284
|
+
</MultiSelect>
|
|
285
|
+
</View>
|
|
286
|
+
</View>
|
|
287
|
+
</View>
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const modal = (
|
|
291
|
+
<OnePaneDialog title="Dropdown in a Modal" content={modalContent} />
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<View style={styles.centered}>
|
|
296
|
+
<ModalLauncher modal={modal}>
|
|
297
|
+
{({openModal}) => (
|
|
298
|
+
<Button onClick={openModal}>Click here!</Button>
|
|
299
|
+
)}
|
|
300
|
+
</ModalLauncher>
|
|
301
|
+
</View>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
DropdownInModal.storyName = "Dropdown in a modal";
|
|
306
|
+
|
|
307
|
+
DropdownInModal.parameters = {
|
|
308
|
+
docs: {
|
|
309
|
+
description: {
|
|
310
|
+
story: "Sometimes we want to include Dropdowns inside a Modal, and these controls can be accessed only by scrolling down. This example help us to demonstrate that `MultiSelect` components can correctly be displayed within the visible scrolling area.",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
chromatic: {
|
|
314
|
+
// We don't need screenshots because this story can be tested after
|
|
315
|
+
// the modal is opened.
|
|
316
|
+
disableSnapshot: true,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Disabled
|
|
322
|
+
*/
|
|
323
|
+
export const Disabled: StoryComponentType = () => (
|
|
324
|
+
<MultiSelect disabled={true} onChange={() => {}}>
|
|
325
|
+
<OptionItem label="Mercury" value="1" />
|
|
326
|
+
<OptionItem label="Venus" value="2" />
|
|
327
|
+
</MultiSelect>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
Disabled.parameters = {
|
|
331
|
+
docs: {
|
|
332
|
+
storyDescription:
|
|
333
|
+
"`MultiSelect` can be disabled by passing `disabled={true}`. This can be useful when you want to disable a control temporarily.",
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* ImplicitAll enabled
|
|
339
|
+
*/
|
|
340
|
+
export const ImplicitAllEnabled: StoryComponentType = Template.bind({});
|
|
341
|
+
|
|
342
|
+
ImplicitAllEnabled.args = {
|
|
343
|
+
implicitAllEnabled: true,
|
|
344
|
+
labels: {
|
|
345
|
+
someSelected: (numSelectedValues) => `${numSelectedValues} fruits`,
|
|
346
|
+
allSelected: "All planets selected",
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
ImplicitAllEnabled.parameters = {
|
|
351
|
+
docs: {
|
|
352
|
+
description: {
|
|
353
|
+
story: `When nothing is selected, show the menu text as "All selected". Note that the actual selection logic doesn't change. (Only the menu text)`,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
chromatic: {
|
|
357
|
+
// We don't need screenshots b/c the dropdown is initially closed.
|
|
358
|
+
disableSnapshot: true,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Virtualized with search filter
|
|
364
|
+
*/
|
|
365
|
+
const fruits = ["banana", "strawberry", "pear", "orange"];
|
|
366
|
+
|
|
367
|
+
const optionItems = new Array(1000)
|
|
368
|
+
.fill(null)
|
|
369
|
+
.map((_, i) => (
|
|
370
|
+
<OptionItem
|
|
371
|
+
key={i}
|
|
372
|
+
value={(i + 1).toString()}
|
|
373
|
+
label={`Fruit # ${i + 1} ${fruits[i % fruits.length]}`}
|
|
374
|
+
/>
|
|
375
|
+
));
|
|
376
|
+
|
|
377
|
+
type Props = {|
|
|
378
|
+
opened?: boolean,
|
|
379
|
+
|};
|
|
380
|
+
|
|
381
|
+
function VirtualizedMultiSelect(props: Props) {
|
|
382
|
+
const [selectedValues, setSelectedValues] = React.useState([]);
|
|
383
|
+
const [opened, setOpened] = React.useState(props.opened || false);
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<View style={styles.wrapper}>
|
|
387
|
+
<MultiSelect
|
|
388
|
+
onChange={setSelectedValues}
|
|
389
|
+
shortcuts={true}
|
|
390
|
+
isFilterable={true}
|
|
391
|
+
opened={opened}
|
|
392
|
+
onToggle={setOpened}
|
|
393
|
+
selectedValues={selectedValues}
|
|
394
|
+
>
|
|
395
|
+
{optionItems}
|
|
396
|
+
</MultiSelect>
|
|
397
|
+
</View>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Virtualized MultiSelect
|
|
403
|
+
*/
|
|
404
|
+
export const VirtualizedFilterable: StoryComponentType = () => (
|
|
405
|
+
<VirtualizedMultiSelect opened={true} />
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
VirtualizedFilterable.storyName = "Virtualized (isFilterable)";
|
|
409
|
+
|
|
410
|
+
VirtualizedFilterable.parameters = {
|
|
411
|
+
docs: {
|
|
412
|
+
description: {
|
|
413
|
+
story: "When there are many options, you could use a search filter in the `MultiSelect`. The search filter will be performed toward the labels of the option items. Note that this example shows how we can add custom styles to the dropdown as well.",
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Custom opener
|
|
420
|
+
*/
|
|
421
|
+
export const CustomOpener: StoryComponentType = Template.bind({});
|
|
422
|
+
|
|
423
|
+
CustomOpener.args = {
|
|
424
|
+
selectedValue: "",
|
|
425
|
+
opener: ({focused, hovered, pressed, text}) => (
|
|
426
|
+
<HeadingLarge
|
|
427
|
+
onClick={() => {
|
|
428
|
+
// eslint-disable-next-line no-console
|
|
429
|
+
console.log("custom click!!!!!");
|
|
430
|
+
}}
|
|
431
|
+
style={[
|
|
432
|
+
styles.customOpener,
|
|
433
|
+
focused && styles.focused,
|
|
434
|
+
hovered && styles.hovered,
|
|
435
|
+
pressed && styles.pressed,
|
|
436
|
+
]}
|
|
437
|
+
>
|
|
438
|
+
{text}
|
|
439
|
+
</HeadingLarge>
|
|
440
|
+
),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
CustomOpener.storyName = "With custom opener";
|
|
444
|
+
|
|
445
|
+
CustomOpener.parameters = {
|
|
446
|
+
docs: {
|
|
447
|
+
description: {
|
|
448
|
+
story:
|
|
449
|
+
"In case you need to use a custom opener with the `MultiSelect`, you can use the opener property to achieve this. In this example, the opener prop accepts a function with the following arguments:\n" +
|
|
450
|
+
"- `eventState`: lets you customize the style for different states, such as pressed, hovered and focused.\n" +
|
|
451
|
+
"- `text`: Passes the menu label defined in the parent component. This value is passed using the placeholder prop set in the `MultiSelect` component.\n\n" +
|
|
452
|
+
"**Note:** If you need to use a custom ID for testing the opener, make sure to pass the testId prop inside the opener component/element.",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Custom labels
|
|
459
|
+
*/
|
|
460
|
+
const translatedItems = new Array(10)
|
|
461
|
+
.fill(null)
|
|
462
|
+
.map((_, i) => (
|
|
463
|
+
<OptionItem
|
|
464
|
+
key={i}
|
|
465
|
+
value={(i + 1).toString()}
|
|
466
|
+
label={`Escuela # ${i + 1}`}
|
|
467
|
+
/>
|
|
468
|
+
));
|
|
469
|
+
|
|
470
|
+
export const CustomLabels: StoryComponentType = () => {
|
|
471
|
+
const [selectedValues, setSelectedValues] = React.useState([]);
|
|
472
|
+
const [opened, setOpened] = React.useState(true);
|
|
473
|
+
|
|
474
|
+
const labels: $Shape<Labels> = {
|
|
475
|
+
clearSearch: "Limpiar busqueda",
|
|
476
|
+
filter: "Filtrar",
|
|
477
|
+
noResults: "Sin resultados",
|
|
478
|
+
selectAllLabel: (numOptions) => `Seleccionar todas (${numOptions})`,
|
|
479
|
+
selectNoneLabel: "No seleccionar ninguno",
|
|
480
|
+
noneSelected: "0 escuelas seleccionadas",
|
|
481
|
+
allSelected: "Todas las escuelas",
|
|
482
|
+
someSelected: (numSelectedValues) =>
|
|
483
|
+
`${numSelectedValues} escuelas seleccionadas`,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<View style={styles.wrapper}>
|
|
488
|
+
<MultiSelect
|
|
489
|
+
shortcuts={true}
|
|
490
|
+
isFilterable={true}
|
|
491
|
+
onChange={setSelectedValues}
|
|
492
|
+
selectedValues={selectedValues}
|
|
493
|
+
labels={labels}
|
|
494
|
+
opened={opened}
|
|
495
|
+
onToggle={setOpened}
|
|
496
|
+
>
|
|
497
|
+
{translatedItems}
|
|
498
|
+
</MultiSelect>
|
|
499
|
+
</View>
|
|
500
|
+
);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
CustomLabels.parameters = {
|
|
504
|
+
docs: {
|
|
505
|
+
description: {
|
|
506
|
+
story: "This example illustrates how you can pass custom labels to the MultiSelect component.",
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {Meta, Story, Canvas} from "@storybook/addon-docs";
|
|
2
|
+
|
|
3
|
+
import {OptionItem, SingleSelect} from "@khanacademy/wonder-blocks-dropdown";
|
|
4
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
5
|
+
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
|
|
6
|
+
|
|
7
|
+
<Meta
|
|
8
|
+
title="Dropdown / SingleSelect / Accessibility"
|
|
9
|
+
component={SingleSelect}
|
|
10
|
+
parameters={{
|
|
11
|
+
previewTabs: {
|
|
12
|
+
canvas: {hidden: true},
|
|
13
|
+
},
|
|
14
|
+
viewMode: "docs",
|
|
15
|
+
chromatic: {
|
|
16
|
+
// Disables chromatic testing for these stories.
|
|
17
|
+
disableSnapshot: true,
|
|
18
|
+
},
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
export const SingleSelectAccessibility = () => (
|
|
23
|
+
<View>
|
|
24
|
+
<LabelLarge
|
|
25
|
+
tag="label"
|
|
26
|
+
id="label-for-single-select"
|
|
27
|
+
htmlFor="unique-single-select"
|
|
28
|
+
>
|
|
29
|
+
Associated label element
|
|
30
|
+
</LabelLarge>
|
|
31
|
+
<SingleSelect
|
|
32
|
+
aria-labelledby="label-for-single-select"
|
|
33
|
+
id="unique-single-select"
|
|
34
|
+
placeholder="Accessible SingleSelect"
|
|
35
|
+
selectedValue="one"
|
|
36
|
+
>
|
|
37
|
+
<OptionItem label="First element" aria-label="First element, selected" value="one" />
|
|
38
|
+
<OptionItem label="Second element" aria-label="Second element, unselelected" value="two" />
|
|
39
|
+
</SingleSelect>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
# Accessibility
|
|
44
|
+
|
|
45
|
+
If you need to associate this component with another element (e.g. `<label>`),
|
|
46
|
+
make sure to pass the `aria-labelledby` and/or `id` props to the `SingleSelect` component.
|
|
47
|
+
This way, the `opener` will receive this value and it will associate both
|
|
48
|
+
elements.
|
|
49
|
+
|
|
50
|
+
Also, if you need screen readers to understand any relevant information on every
|
|
51
|
+
option item, you can use `aria-label` on each item. e.g. You can use it to let
|
|
52
|
+
screen readers know the current selected/unselected status of the item when it
|
|
53
|
+
receives focus.
|
|
54
|
+
|
|
55
|
+
<Canvas>
|
|
56
|
+
<Story name="Using aria attributes">
|
|
57
|
+
{SingleSelectAccessibility.bind({})}
|
|
58
|
+
</Story>
|
|
59
|
+
</Canvas>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
export default {
|
|
3
|
+
alignment: {
|
|
4
|
+
table: {
|
|
5
|
+
category: "Layout",
|
|
6
|
+
},
|
|
7
|
+
},
|
|
8
|
+
disabled: {
|
|
9
|
+
table: {
|
|
10
|
+
category: "States",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
isFilterable: {
|
|
14
|
+
table: {
|
|
15
|
+
category: "States",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
light: {
|
|
19
|
+
table: {
|
|
20
|
+
category: "States",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
opened: {
|
|
24
|
+
control: "boolean",
|
|
25
|
+
table: {
|
|
26
|
+
category: "States",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
onToggle: {
|
|
30
|
+
table: {
|
|
31
|
+
category: "Events",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
onChange: {
|
|
35
|
+
table: {
|
|
36
|
+
category: "Events",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
dropdownStyle: {
|
|
40
|
+
table: {
|
|
41
|
+
category: "Styling",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
style: {
|
|
45
|
+
table: {
|
|
46
|
+
category: "Styling",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
className: {
|
|
50
|
+
table: {
|
|
51
|
+
category: "Styling",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|