@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/es/index.js +92 -162
  3. package/dist/index.js +285 -374
  4. package/package.json +6 -6
  5. package/src/components/__docs__/action-menu.argtypes.js +44 -0
  6. package/src/components/__docs__/action-menu.stories.js +435 -0
  7. package/src/components/__docs__/base-select.argtypes.js +54 -0
  8. package/src/components/__docs__/multi-select.stories.js +509 -0
  9. package/src/components/__docs__/single-select.accessibility.stories.mdx +59 -0
  10. package/src/components/__docs__/single-select.argtypes.js +54 -0
  11. package/src/components/__docs__/single-select.stories.js +464 -0
  12. package/src/components/__tests__/dropdown-core-virtualized.test.js +0 -15
  13. package/src/components/__tests__/dropdown-core.test.js +114 -208
  14. package/src/components/__tests__/multi-select.test.js +1 -3
  15. package/src/components/__tests__/single-select.test.js +15 -47
  16. package/src/components/action-menu.js +11 -0
  17. package/src/components/dropdown-core-virtualized.js +0 -5
  18. package/src/components/dropdown-core.js +140 -126
  19. package/src/components/multi-select.js +17 -33
  20. package/src/components/single-select.js +15 -30
  21. package/src/util/__tests__/dropdown-menu-styles.test.js +0 -26
  22. package/src/util/constants.js +0 -11
  23. package/src/util/dropdown-menu-styles.js +0 -5
  24. package/src/util/types.js +2 -5
  25. package/src/components/__tests__/search-text-input.test.js +0 -212
  26. package/src/components/action-menu.stories.js +0 -48
  27. package/src/components/multi-select.stories.js +0 -124
  28. package/src/components/search-text-input.js +0 -115
  29. package/src/components/single-select.stories.js +0 -247
@@ -0,0 +1,464 @@
1
+ // @flow
2
+ import * as React from "react";
3
+ import {StyleSheet} from "aphrodite";
4
+
5
+ import Button from "@khanacademy/wonder-blocks-button";
6
+ import Color from "@khanacademy/wonder-blocks-color";
7
+ import {View} from "@khanacademy/wonder-blocks-core";
8
+ import {Strut} from "@khanacademy/wonder-blocks-layout";
9
+ import {OnePaneDialog, ModalLauncher} from "@khanacademy/wonder-blocks-modal";
10
+ import Spacing from "@khanacademy/wonder-blocks-spacing";
11
+ import {Body, HeadingLarge} from "@khanacademy/wonder-blocks-typography";
12
+
13
+ import type {StoryComponentType} from "@storybook/react";
14
+
15
+ import {
16
+ SingleSelect,
17
+ OptionItem,
18
+ SeparatorItem,
19
+ } from "@khanacademy/wonder-blocks-dropdown";
20
+ import ComponentInfo from "../../../../../.storybook/components/component-info.js";
21
+ import {name, version} from "../../../package.json";
22
+ import singleSelectArgtypes from "./base-select.argtypes.js";
23
+
24
+ export default {
25
+ title: "Dropdown / SingleSelect",
26
+ component: SingleSelect,
27
+ subcomponents: {OptionItem, SeparatorItem},
28
+ argTypes: singleSelectArgtypes,
29
+ args: {
30
+ isFilterable: true,
31
+ opened: false,
32
+ disabled: false,
33
+ light: false,
34
+ placeholder: "Choose a fruit",
35
+ selectedValue: "",
36
+ },
37
+ decorators: [
38
+ (Story: StoryComponentType): React.Element<typeof View> => (
39
+ <View style={styles.example}>
40
+ <Story />
41
+ </View>
42
+ ),
43
+ ],
44
+ parameters: {
45
+ componentSubtitle: ((
46
+ <ComponentInfo name={name} version={version} />
47
+ ): any),
48
+ docs: {
49
+ description: {
50
+ component: null,
51
+ },
52
+ source: {
53
+ // See https://github.com/storybookjs/storybook/issues/12596
54
+ excludeDecorators: true,
55
+ },
56
+ },
57
+ },
58
+ };
59
+
60
+ const styles = StyleSheet.create({
61
+ example: {
62
+ background: Color.offWhite,
63
+ padding: Spacing.medium_16,
64
+ },
65
+ rowRight: {
66
+ flexDirection: "row",
67
+ justifyContent: "flex-end",
68
+ },
69
+ row: {
70
+ flexDirection: "row",
71
+ alignItems: "center",
72
+ justifyContent: "space-between",
73
+ },
74
+ dropdown: {
75
+ maxHeight: 200,
76
+ },
77
+ /**
78
+ * Custom opener styles
79
+ */
80
+ customOpener: {
81
+ borderLeft: `5px solid ${Color.blue}`,
82
+ borderRadius: Spacing.xxxSmall_4,
83
+ background: Color.lightBlue,
84
+ color: Color.white,
85
+ padding: Spacing.medium_16,
86
+ },
87
+ focused: {
88
+ color: Color.offWhite,
89
+ },
90
+ hovered: {
91
+ textDecoration: "underline",
92
+ color: Color.offWhite,
93
+ cursor: "pointer",
94
+ },
95
+ pressed: {
96
+ color: Color.blue,
97
+ },
98
+
99
+ fullBleed: {
100
+ width: "100%",
101
+ },
102
+ wrapper: {
103
+ height: "500px",
104
+ width: "600px",
105
+ },
106
+ centered: {
107
+ alignItems: "center",
108
+ justifyContent: "center",
109
+ height: `calc(100vh - 16px)`,
110
+ },
111
+ scrollableArea: {
112
+ height: "200vh",
113
+ },
114
+ /**
115
+ * Dark
116
+ */
117
+ darkBackgroundWrapper: {
118
+ flexDirection: "row",
119
+ justifyContent: "flex-end",
120
+ backgroundColor: Color.darkBlue,
121
+ width: "100%",
122
+ height: 200,
123
+ paddingRight: Spacing.medium_16,
124
+ paddingTop: Spacing.medium_16,
125
+ },
126
+ });
127
+
128
+ const items = [
129
+ <OptionItem label="Banana" value="banana" />,
130
+ <OptionItem label="Strawberry" value="strawberry" disabled />,
131
+ <OptionItem label="Pear" value="pear" />,
132
+ <OptionItem label="Orange" value="orange" />,
133
+ <OptionItem label="Watermelon" value="watermelon" />,
134
+ <OptionItem label="Apple" value="apple" />,
135
+ <OptionItem label="Grape" value="grape" />,
136
+ <OptionItem label="Lemon" value="lemon" />,
137
+ <OptionItem label="Mango" value="mango" />,
138
+ ];
139
+
140
+ const Template = (args) => {
141
+ const [selectedValue, setSelectedValue] = React.useState(
142
+ args.selectedValue,
143
+ );
144
+ const [opened, setOpened] = React.useState(args.opened);
145
+ React.useEffect(() => {
146
+ // Only update opened if the args.opened prop changes (using the
147
+ // controls panel).
148
+ setOpened(args.opened);
149
+ }, [args.opened]);
150
+
151
+ return (
152
+ <SingleSelect
153
+ {...args}
154
+ onChange={setSelectedValue}
155
+ selectedValue={selectedValue}
156
+ opened={opened}
157
+ onToggle={setOpened}
158
+ >
159
+ {items}
160
+ </SingleSelect>
161
+ );
162
+ };
163
+
164
+ export const Default: StoryComponentType = Template.bind({});
165
+
166
+ /**
167
+ * Controlled SingleSelect
168
+ */
169
+ export const ControlledOpened: StoryComponentType = (args) => {
170
+ const [selectedValue, setSelectedValue] = React.useState("pear");
171
+ const [opened, setOpened] = React.useState(args.opened);
172
+ React.useEffect(() => {
173
+ // Only update opened if the args.opened prop changes (using the
174
+ // controls panel).
175
+ setOpened(args.opened);
176
+ }, [args.opened]);
177
+
178
+ return (
179
+ <View style={styles.wrapper}>
180
+ <SingleSelect
181
+ {...args}
182
+ onChange={setSelectedValue}
183
+ selectedValue={selectedValue}
184
+ opened={opened}
185
+ onToggle={setOpened}
186
+ >
187
+ {items}
188
+ </SingleSelect>
189
+ </View>
190
+ );
191
+ };
192
+
193
+ ControlledOpened.args = {
194
+ opened: true,
195
+ };
196
+
197
+ ControlledOpened.storyName = "Controlled (opened)";
198
+
199
+ ControlledOpened.parameters = {
200
+ docs: {
201
+ description: {
202
+ story:
203
+ "Sometimes you'll want to trigger a dropdown programmatically. This can be done by setting a value to the `opened` prop (`true` or `false`). In this situation the `SingleSelect` is a controlled component. The parent is responsible for managing the opening/closing of the dropdown when using this prop.\n\n" +
204
+ "This means that you'll also have to update `opened` to the value triggered by the `onToggle` prop.",
205
+ },
206
+ },
207
+ // Added to ensure that the dropdown menu is rendered using PopperJS.
208
+ chromatic: {delay: 400},
209
+ };
210
+
211
+ /**
212
+ * Disabled
213
+ */
214
+ export const Disabled: StoryComponentType = (args) => (
215
+ <SingleSelect
216
+ {...args}
217
+ onChange={() => {}}
218
+ selectedValue=""
219
+ disabled={true}
220
+ >
221
+ {items}
222
+ </SingleSelect>
223
+ );
224
+
225
+ Disabled.parameters = {
226
+ docs: {
227
+ description: {
228
+ story: "This select is disabled and cannot be interacted with.",
229
+ },
230
+ },
231
+ };
232
+
233
+ /**
234
+ * On dark background, right-aligned
235
+ */
236
+ export const Light: StoryComponentType = (args) => {
237
+ const [selectedValue, setSelectedValue] = React.useState("pear");
238
+
239
+ return (
240
+ <View style={styles.row}>
241
+ <View style={styles.darkBackgroundWrapper}>
242
+ <SingleSelect
243
+ alignment="right"
244
+ light={true}
245
+ onChange={setSelectedValue}
246
+ placeholder="Choose a drink"
247
+ selectedValue={selectedValue}
248
+ >
249
+ <OptionItem
250
+ label="Regular milk tea with boba"
251
+ value="regular"
252
+ />
253
+ <OptionItem
254
+ label="Wintermelon milk tea with boba"
255
+ value="wintermelon"
256
+ />
257
+ <OptionItem
258
+ label="Taro milk tea, half sugar"
259
+ value="taro"
260
+ />
261
+ </SingleSelect>
262
+ </View>
263
+ </View>
264
+ );
265
+ };
266
+
267
+ Light.parameters = {
268
+ docs: {
269
+ description: {
270
+ story: "This single select is on a dark background and is also right-aligned.",
271
+ },
272
+ },
273
+ };
274
+
275
+ const fruits = ["banana", "strawberry", "pear", "orange"];
276
+
277
+ const optionItems = new Array(1000)
278
+ .fill(null)
279
+ .map((_, i) => (
280
+ <OptionItem
281
+ key={i}
282
+ value={(i + 1).toString()}
283
+ label={`Fruit # ${i + 1} ${fruits[i % fruits.length]}`}
284
+ />
285
+ ));
286
+
287
+ type Props = {|
288
+ selectedValue?: ?string,
289
+ opened?: boolean,
290
+ |};
291
+
292
+ function VirtualizedSingleSelect(props: Props) {
293
+ const [selectedValue, setSelectedValue] = React.useState(
294
+ props.selectedValue,
295
+ );
296
+ const [opened, setOpened] = React.useState(props.opened || false);
297
+
298
+ return (
299
+ <View style={styles.wrapper}>
300
+ <SingleSelect
301
+ onChange={setSelectedValue}
302
+ isFilterable={true}
303
+ opened={opened}
304
+ onToggle={setOpened}
305
+ placeholder="Select a fruit"
306
+ selectedValue={selectedValue}
307
+ dropdownStyle={styles.fullBleed}
308
+ style={styles.fullBleed}
309
+ >
310
+ {optionItems}
311
+ </SingleSelect>
312
+ </View>
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Virtualized SingleSelect
318
+ */
319
+ export const VirtualizedFilterable: StoryComponentType = () => (
320
+ <VirtualizedSingleSelect />
321
+ );
322
+
323
+ VirtualizedFilterable.storyName = "Virtualized (isFilterable)";
324
+
325
+ VirtualizedFilterable.parameters = {
326
+ docs: {
327
+ description: {
328
+ story: "When there are many options, you could use a search filter in the SingleSelect. 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.",
329
+ },
330
+ },
331
+ chromatic: {
332
+ // we don't need screenshots because this story only tests behavior.
333
+ disableSnapshot: true,
334
+ },
335
+ };
336
+
337
+ export const VirtualizedOpened: StoryComponentType = () => (
338
+ <VirtualizedSingleSelect opened={true} />
339
+ );
340
+
341
+ VirtualizedOpened.storyName = "Virtualized (opened)";
342
+
343
+ VirtualizedOpened.parameters = {
344
+ docs: {
345
+ description: {
346
+ story: "This example shows how to use the `opened` prop to open the dropdown.",
347
+ },
348
+ },
349
+ };
350
+
351
+ export const VirtualizedOpenedNoSelection: StoryComponentType = () => (
352
+ <VirtualizedSingleSelect opened={true} selectedValue={null} />
353
+ );
354
+
355
+ VirtualizedOpenedNoSelection.storyName = "Virtualized (opened, no selection)";
356
+
357
+ VirtualizedOpenedNoSelection.parameters = {
358
+ docs: {
359
+ description: {
360
+ story: "This example shows how the focus is set to the search field if there's no current selection.",
361
+ },
362
+ },
363
+ };
364
+
365
+ /**
366
+ * Inside a modal
367
+ */
368
+ export const DropdownInModal: StoryComponentType = () => {
369
+ const [value, setValue] = React.useState(null);
370
+ const [opened, setOpened] = React.useState(true);
371
+
372
+ const modalContent = (
373
+ <View style={styles.scrollableArea}>
374
+ <View>
375
+ <Body>
376
+ Sometimes we want to include Dropdowns inside a Modal, and
377
+ these controls can be accessed only by scrolling down. This
378
+ example help us to demonstrate that SingleSelect components
379
+ can correctly be displayed within the visible scrolling
380
+ area.
381
+ </Body>
382
+ <Strut size={Spacing.large_24} />
383
+ <SingleSelect
384
+ onChange={(selected) => setValue(selected)}
385
+ isFilterable={true}
386
+ opened={opened}
387
+ onToggle={(opened) => setOpened(opened)}
388
+ placeholder="Select a fruit"
389
+ selectedValue={value}
390
+ >
391
+ {optionItems}
392
+ </SingleSelect>
393
+ </View>
394
+ </View>
395
+ );
396
+
397
+ const modal = (
398
+ <OnePaneDialog title="Dropdown in a Modal" content={modalContent} />
399
+ );
400
+
401
+ return (
402
+ <View style={styles.centered}>
403
+ <ModalLauncher modal={modal}>
404
+ {({openModal}) => (
405
+ <Button onClick={openModal}>Click here!</Button>
406
+ )}
407
+ </ModalLauncher>
408
+ </View>
409
+ );
410
+ };
411
+
412
+ DropdownInModal.storyName = "Dropdown in a modal";
413
+
414
+ DropdownInModal.parameters = {
415
+ docs: {
416
+ description: {
417
+ 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 `SingleSelect` components can correctly be displayed within the visible scrolling area.",
418
+ },
419
+ },
420
+ chromatic: {
421
+ // We don't need screenshots because this story can be tested after
422
+ // the modal is opened.
423
+ disableSnapshot: true,
424
+ },
425
+ };
426
+
427
+ /**
428
+ * Custom opener
429
+ */
430
+ export const CustomOpener: StoryComponentType = Template.bind({});
431
+
432
+ CustomOpener.args = {
433
+ selectedValue: "",
434
+ opener: ({focused, hovered, pressed, text}) => (
435
+ <HeadingLarge
436
+ onClick={() => {
437
+ // eslint-disable-next-line no-console
438
+ console.log("custom click!!!!!");
439
+ }}
440
+ style={[
441
+ styles.customOpener,
442
+ focused && styles.focused,
443
+ hovered && styles.hovered,
444
+ pressed && styles.pressed,
445
+ ]}
446
+ >
447
+ {text}
448
+ </HeadingLarge>
449
+ ),
450
+ };
451
+
452
+ CustomOpener.storyName = "With custom opener";
453
+
454
+ CustomOpener.parameters = {
455
+ docs: {
456
+ description: {
457
+ story:
458
+ "In case you need to use a custom opener with the `SingleSelect`, you can use the opener property to achieve this. In this example, the opener prop accepts a function with the following arguments:\n" +
459
+ "- `eventState`: lets you customize the style for different states, such as pressed, hovered and focused.\n" +
460
+ "- `text`: Passes the menu label defined in the parent component. This value is passed using the placeholder prop set in the `SingleSelect` component.\n\n" +
461
+ "**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.",
462
+ },
463
+ },
464
+ };
@@ -5,7 +5,6 @@ import {render, screen} from "@testing-library/react";
5
5
  import OptionItem from "../option-item.js";
6
6
  import SeparatorItem from "../separator-item.js";
7
7
  import DropdownCoreVirtualized from "../dropdown-core-virtualized.js";
8
- import SearchTextInput from "../search-text-input.js";
9
8
 
10
9
  describe("DropdownCoreVirtualized", () => {
11
10
  it("should sort the items on first load", () => {
@@ -22,13 +21,6 @@ describe("DropdownCoreVirtualized", () => {
22
21
  }));
23
22
 
24
23
  const initialItems = [
25
- {
26
- component: (
27
- <SearchTextInput onChange={jest.fn()} searchText="" />
28
- ),
29
- focusable: true,
30
- populatedProps: {},
31
- },
32
24
  {
33
25
  component: <SeparatorItem />,
34
26
  focusable: false,
@@ -68,13 +60,6 @@ describe("DropdownCoreVirtualized", () => {
68
60
  }));
69
61
 
70
62
  const initialItems = [
71
- {
72
- component: (
73
- <SearchTextInput onChange={jest.fn()} searchText="" />
74
- ),
75
- focusable: true,
76
- populatedProps: {},
77
- },
78
63
  {
79
64
  component: <SeparatorItem />,
80
65
  focusable: false,