@khanacademy/wonder-blocks-dropdown 5.3.5 → 5.3.7
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 +21 -0
- package/dist/components/dropdown-core.d.ts +4 -0
- package/dist/components/select-opener.d.ts +14 -2
- package/dist/es/index.js +126 -82
- package/dist/index.js +125 -81
- package/package.json +5 -5
- package/src/components/__tests__/dropdown-core.test.tsx +76 -0
- package/src/components/__tests__/multi-select.test.tsx +161 -25
- package/src/components/__tests__/select-opener.test.tsx +220 -0
- package/src/components/__tests__/single-select.test.tsx +181 -28
- package/src/components/dropdown-core.tsx +7 -3
- package/src/components/multi-select.tsx +8 -1
- package/src/components/select-opener.tsx +165 -106
- package/src/components/single-select.tsx +8 -1
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import {StyleSheet} from "aphrodite";
|
|
3
|
-
import {__RouterContext} from "react-router";
|
|
4
3
|
|
|
5
4
|
import type {AriaProps} from "@khanacademy/wonder-blocks-core";
|
|
6
5
|
|
|
7
6
|
import {mix} from "@khanacademy/wonder-blocks-tokens";
|
|
8
7
|
import {addStyle} from "@khanacademy/wonder-blocks-core";
|
|
9
|
-
import {getClickableBehavior} from "@khanacademy/wonder-blocks-clickable";
|
|
10
8
|
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
|
|
11
9
|
import {LabelMedium} from "@khanacademy/wonder-blocks-typography";
|
|
12
10
|
import * as tokens from "@khanacademy/wonder-blocks-tokens";
|
|
@@ -66,10 +64,24 @@ type DefaultProps = {
|
|
|
66
64
|
isPlaceholder: SelectOpenerProps["isPlaceholder"];
|
|
67
65
|
};
|
|
68
66
|
|
|
67
|
+
type SelectOpenerState = {
|
|
68
|
+
/**
|
|
69
|
+
* We only keep track of the pressed state to apply styling for when the select
|
|
70
|
+
* opener is pressed using Enter/Space. Other states (active, hover, focus)
|
|
71
|
+
* are not tracked because we use css pseudo-classes to handle those styles
|
|
72
|
+
* instead. Note: `:active` styling is only applied on clicks across browsers,
|
|
73
|
+
* and not on keyboard interaction.
|
|
74
|
+
*/
|
|
75
|
+
pressed: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
69
78
|
/**
|
|
70
79
|
* An opener that opens select boxes.
|
|
71
80
|
*/
|
|
72
|
-
export default class SelectOpener extends React.Component<
|
|
81
|
+
export default class SelectOpener extends React.Component<
|
|
82
|
+
SelectOpenerProps,
|
|
83
|
+
SelectOpenerState
|
|
84
|
+
> {
|
|
73
85
|
static defaultProps: DefaultProps = {
|
|
74
86
|
disabled: false,
|
|
75
87
|
error: false,
|
|
@@ -77,12 +89,41 @@ export default class SelectOpener extends React.Component<SelectOpenerProps> {
|
|
|
77
89
|
isPlaceholder: false,
|
|
78
90
|
};
|
|
79
91
|
|
|
92
|
+
constructor(props: SelectOpenerProps) {
|
|
93
|
+
super(props);
|
|
94
|
+
|
|
95
|
+
this.state = {
|
|
96
|
+
pressed: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
handleClick: (e: React.SyntheticEvent) => void = (e) => {
|
|
81
101
|
const {open} = this.props;
|
|
82
102
|
this.props.onOpenChanged(!open);
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
handleKeyDown: (e: React.KeyboardEvent) => void = (e) => {
|
|
106
|
+
const keyCode = e.key;
|
|
107
|
+
// Prevent default behavior for Enter key. Without this, the select
|
|
108
|
+
// is only open while the Enter key is pressed.
|
|
109
|
+
// Prevent default behavior for Space key. Without this, Safari stays in
|
|
110
|
+
// active state visually
|
|
111
|
+
if (keyCode === "Enter" || keyCode === " ") {
|
|
112
|
+
this.setState({pressed: true});
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
handleKeyUp: (e: React.KeyboardEvent) => void = (e) => {
|
|
118
|
+
const keyCode = e.key;
|
|
119
|
+
// On key up for Enter and Space, trigger the click handler
|
|
120
|
+
if (keyCode === "Enter" || keyCode === " ") {
|
|
121
|
+
this.setState({pressed: false});
|
|
122
|
+
this.handleClick(e);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
render(): React.ReactNode {
|
|
86
127
|
const {
|
|
87
128
|
children,
|
|
88
129
|
disabled,
|
|
@@ -97,74 +138,52 @@ export default class SelectOpener extends React.Component<SelectOpenerProps> {
|
|
|
97
138
|
...sharedProps
|
|
98
139
|
} = this.props;
|
|
99
140
|
|
|
100
|
-
const
|
|
141
|
+
const stateStyles = _generateStyles(light, isPlaceholder, error);
|
|
101
142
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// The icon colors are kind of fickle. This is just logic
|
|
113
|
-
// based on the zeplin design.
|
|
114
|
-
const iconColor = light
|
|
115
|
-
? disabled || pressed
|
|
116
|
-
? "currentColor"
|
|
117
|
-
: tokens.color.white
|
|
118
|
-
: disabled
|
|
119
|
-
? tokens.color.offBlack32
|
|
120
|
-
: tokens.color.offBlack64;
|
|
143
|
+
// The icon colors are kind of fickle. This is just logic
|
|
144
|
+
// based on the zeplin design.
|
|
145
|
+
const iconColor = light
|
|
146
|
+
? disabled || error
|
|
147
|
+
? "currentColor"
|
|
148
|
+
: tokens.color.white
|
|
149
|
+
: disabled
|
|
150
|
+
? tokens.color.offBlack32
|
|
151
|
+
: tokens.color.offBlack64;
|
|
121
152
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
? stateStyles.active
|
|
129
|
-
: (hovered || focused) && stateStyles.focus),
|
|
130
|
-
];
|
|
153
|
+
const style = [
|
|
154
|
+
styles.shared,
|
|
155
|
+
stateStyles.default,
|
|
156
|
+
disabled && stateStyles.disabled,
|
|
157
|
+
!disabled && this.state.pressed && stateStyles.pressed,
|
|
158
|
+
];
|
|
131
159
|
|
|
132
|
-
return (
|
|
133
|
-
<StyledButton
|
|
134
|
-
{...sharedProps}
|
|
135
|
-
aria-expanded={open ? "true" : "false"}
|
|
136
|
-
aria-haspopup="listbox"
|
|
137
|
-
data-testid={testId}
|
|
138
|
-
disabled={disabled}
|
|
139
|
-
id={id}
|
|
140
|
-
style={style}
|
|
141
|
-
type="button"
|
|
142
|
-
{...childrenProps}
|
|
143
|
-
>
|
|
144
|
-
<LabelMedium style={styles.text}>
|
|
145
|
-
{/* Note(tamarab): Prevents unwanted vertical
|
|
146
|
-
shift for empty selection */}
|
|
147
|
-
{children || "\u00A0"}
|
|
148
|
-
</LabelMedium>
|
|
149
|
-
<PhosphorIcon
|
|
150
|
-
icon={caretDownIcon}
|
|
151
|
-
color={iconColor}
|
|
152
|
-
size="small"
|
|
153
|
-
style={styles.caret}
|
|
154
|
-
aria-hidden="true"
|
|
155
|
-
/>
|
|
156
|
-
</StyledButton>
|
|
157
|
-
);
|
|
158
|
-
}}
|
|
159
|
-
</ClickableBehavior>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
render(): React.ReactNode {
|
|
164
160
|
return (
|
|
165
|
-
<
|
|
166
|
-
{
|
|
167
|
-
|
|
161
|
+
<StyledButton
|
|
162
|
+
{...sharedProps}
|
|
163
|
+
aria-disabled={disabled}
|
|
164
|
+
aria-expanded={open ? "true" : "false"}
|
|
165
|
+
aria-haspopup="listbox"
|
|
166
|
+
data-testid={testId}
|
|
167
|
+
id={id}
|
|
168
|
+
style={style}
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={!disabled ? this.handleClick : undefined}
|
|
171
|
+
onKeyDown={!disabled ? this.handleKeyDown : undefined}
|
|
172
|
+
onKeyUp={!disabled ? this.handleKeyUp : undefined}
|
|
173
|
+
>
|
|
174
|
+
<LabelMedium style={styles.text}>
|
|
175
|
+
{/* Note(tamarab): Prevents unwanted vertical
|
|
176
|
+
shift for empty selection */}
|
|
177
|
+
{children || "\u00A0"}
|
|
178
|
+
</LabelMedium>
|
|
179
|
+
<PhosphorIcon
|
|
180
|
+
icon={caretDownIcon}
|
|
181
|
+
color={iconColor}
|
|
182
|
+
size="small"
|
|
183
|
+
style={styles.caret}
|
|
184
|
+
aria-hidden="true"
|
|
185
|
+
/>
|
|
186
|
+
</StyledButton>
|
|
168
187
|
);
|
|
169
188
|
}
|
|
170
189
|
}
|
|
@@ -229,41 +248,77 @@ const _generateStyles = (
|
|
|
229
248
|
|
|
230
249
|
let newStyles: Record<string, any> = {};
|
|
231
250
|
if (light) {
|
|
251
|
+
const focusHoverStyling = {
|
|
252
|
+
borderColor: error ? tokens.color.red : tokens.color.white,
|
|
253
|
+
borderWidth: tokens.spacing.xxxxSmall_2,
|
|
254
|
+
paddingLeft: adjustedPaddingLeft,
|
|
255
|
+
paddingRight: adjustedPaddingRight,
|
|
256
|
+
};
|
|
257
|
+
const activePressedStyling = {
|
|
258
|
+
paddingLeft: adjustedPaddingLeft,
|
|
259
|
+
paddingRight: adjustedPaddingRight,
|
|
260
|
+
borderColor: error ? tokens.color.red : tokens.color.fadedBlue,
|
|
261
|
+
borderWidth: tokens.border.width.thin,
|
|
262
|
+
color: error
|
|
263
|
+
? tokens.color.offBlack64
|
|
264
|
+
: placeholder
|
|
265
|
+
? mix(tokens.color.white32, tokens.color.blue)
|
|
266
|
+
: tokens.color.fadedBlue,
|
|
267
|
+
backgroundColor: error
|
|
268
|
+
? tokens.color.fadedRed
|
|
269
|
+
: tokens.color.activeBlue,
|
|
270
|
+
};
|
|
232
271
|
newStyles = {
|
|
233
272
|
default: {
|
|
234
273
|
background: error ? tokens.color.fadedRed8 : "transparent",
|
|
235
|
-
color:
|
|
274
|
+
color: error
|
|
275
|
+
? tokens.color.offBlack64
|
|
276
|
+
: placeholder
|
|
277
|
+
? tokens.color.white50
|
|
278
|
+
: tokens.color.white,
|
|
236
279
|
borderColor: error ? tokens.color.red : tokens.color.white50,
|
|
237
280
|
borderWidth: tokens.border.width.hairline,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
? mix(tokens.color.white32, tokens.color.blue)
|
|
254
|
-
: tokens.color.fadedBlue,
|
|
255
|
-
backgroundColor: error
|
|
256
|
-
? tokens.color.fadedRed
|
|
257
|
-
: tokens.color.activeBlue,
|
|
281
|
+
":hover:not([aria-disabled=true])": focusHoverStyling,
|
|
282
|
+
// Allow hover styles on non-touch devices only. This prevents an
|
|
283
|
+
// issue with hover being sticky on touch devices (e.g. mobile).
|
|
284
|
+
["@media not (hover: hover)"]: {
|
|
285
|
+
":hover:not([aria-disabled=true])": {
|
|
286
|
+
borderColor: error
|
|
287
|
+
? tokens.color.red
|
|
288
|
+
: tokens.color.white50,
|
|
289
|
+
borderWidth: tokens.border.width.hairline,
|
|
290
|
+
paddingLeft: tokens.spacing.medium_16,
|
|
291
|
+
paddingRight: tokens.spacing.small_12,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
":focus-visible:not([aria-disabled=true])": focusHoverStyling,
|
|
295
|
+
":active:not([aria-disabled=true])": activePressedStyling,
|
|
258
296
|
},
|
|
259
297
|
disabled: {
|
|
260
298
|
background: "transparent",
|
|
261
299
|
borderColor: mix(tokens.color.white32, tokens.color.blue),
|
|
262
300
|
color: mix(tokens.color.white32, tokens.color.blue),
|
|
263
|
-
cursor: "
|
|
301
|
+
cursor: "not-allowed",
|
|
302
|
+
":focus-visible": {
|
|
303
|
+
boxShadow: `0 0 0 1px ${tokens.color.offBlack32}, 0 0 0 3px ${tokens.color.fadedBlue}`,
|
|
304
|
+
},
|
|
264
305
|
},
|
|
306
|
+
pressed: activePressedStyling,
|
|
265
307
|
};
|
|
266
308
|
} else {
|
|
309
|
+
const focusHoverStyling = {
|
|
310
|
+
borderColor: error ? tokens.color.red : tokens.color.blue,
|
|
311
|
+
borderWidth: tokens.border.width.thin,
|
|
312
|
+
paddingLeft: adjustedPaddingLeft,
|
|
313
|
+
paddingRight: adjustedPaddingRight,
|
|
314
|
+
};
|
|
315
|
+
const activePressedStyling = {
|
|
316
|
+
background: error ? tokens.color.fadedRed : tokens.color.fadedBlue,
|
|
317
|
+
borderColor: error ? tokens.color.red : tokens.color.activeBlue,
|
|
318
|
+
borderWidth: tokens.border.width.thin,
|
|
319
|
+
paddingLeft: adjustedPaddingLeft,
|
|
320
|
+
paddingRight: adjustedPaddingRight,
|
|
321
|
+
};
|
|
267
322
|
newStyles = {
|
|
268
323
|
default: {
|
|
269
324
|
background: error ? tokens.color.fadedRed8 : tokens.color.white,
|
|
@@ -272,28 +327,32 @@ const _generateStyles = (
|
|
|
272
327
|
color: placeholder
|
|
273
328
|
? tokens.color.offBlack64
|
|
274
329
|
: tokens.color.offBlack,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
330
|
+
":hover:not([aria-disabled=true])": focusHoverStyling,
|
|
331
|
+
// Allow hover styles on non-touch devices only. This prevents an
|
|
332
|
+
// issue with hover being sticky on touch devices (e.g. mobile).
|
|
333
|
+
["@media not (hover: hover)"]: {
|
|
334
|
+
":hover:not([aria-disabled=true])": {
|
|
335
|
+
borderColor: error
|
|
336
|
+
? tokens.color.red
|
|
337
|
+
: tokens.color.offBlack16,
|
|
338
|
+
borderWidth: tokens.border.width.hairline,
|
|
339
|
+
paddingLeft: tokens.spacing.medium_16,
|
|
340
|
+
paddingRight: tokens.spacing.small_12,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
":focus-visible:not([aria-disabled=true])": focusHoverStyling,
|
|
344
|
+
":active:not([aria-disabled=true])": activePressedStyling,
|
|
290
345
|
},
|
|
291
346
|
disabled: {
|
|
292
347
|
background: tokens.color.offWhite,
|
|
293
348
|
borderColor: tokens.color.offBlack16,
|
|
294
349
|
color: tokens.color.offBlack64,
|
|
295
|
-
cursor: "
|
|
350
|
+
cursor: "not-allowed",
|
|
351
|
+
":focus-visible": {
|
|
352
|
+
boxShadow: `0 0 0 1px ${tokens.color.white}, 0 0 0 3px ${tokens.color.offBlack32}`,
|
|
353
|
+
},
|
|
296
354
|
},
|
|
355
|
+
pressed: activePressedStyling,
|
|
297
356
|
};
|
|
298
357
|
}
|
|
299
358
|
|
|
@@ -252,7 +252,12 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
252
252
|
state: State,
|
|
253
253
|
): Partial<State> | null {
|
|
254
254
|
return {
|
|
255
|
-
open
|
|
255
|
+
// open should always be false if select is disabled
|
|
256
|
+
open: props.disabled
|
|
257
|
+
? false
|
|
258
|
+
: typeof props.opened === "boolean"
|
|
259
|
+
? props.opened
|
|
260
|
+
: state.open,
|
|
256
261
|
};
|
|
257
262
|
}
|
|
258
263
|
|
|
@@ -448,6 +453,7 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
448
453
|
style,
|
|
449
454
|
"aria-invalid": ariaInvalid,
|
|
450
455
|
"aria-required": ariaRequired,
|
|
456
|
+
disabled,
|
|
451
457
|
} = this.props;
|
|
452
458
|
const {searchText} = this.state;
|
|
453
459
|
const allChildren = React.Children.toArray(children).filter(Boolean);
|
|
@@ -484,6 +490,7 @@ export default class SingleSelect extends React.Component<Props, State> {
|
|
|
484
490
|
labels={labels}
|
|
485
491
|
aria-invalid={ariaInvalid}
|
|
486
492
|
aria-required={ariaRequired}
|
|
493
|
+
disabled={disabled}
|
|
487
494
|
/>
|
|
488
495
|
);
|
|
489
496
|
}
|