@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-dropdown
|
|
2
2
|
|
|
3
|
+
## 5.3.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f7ff9a77: SingleSelect and MultiSelect: Disable keyboard interactions to open the select if the `disabled` prop is set to `true`. Prevent select from being in an open state if
|
|
8
|
+
`disabled` prop is `true`.
|
|
9
|
+
|
|
10
|
+
## 5.3.6
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- cc2d8e86: - Update styling for variants of `SingleSelect` and `MultiSelect`. This addresses some styling edge cases around error/disabled states, focus/hover/active states, and light mode. The `not-allowed` cursor is also applied to the disabled state.
|
|
15
|
+
- Styling in the `SelectOpener` (`SingleSelect` and `MultiSelect`) is now applied using css pseudo-classes (`:focus-visible`, `:hover`, `:active`) instead of applying styles using js.
|
|
16
|
+
- Replaced internal use of `ClickableBehaviour` in `SelectOpener` for normalized keyboard interaction behaviour across browsers.
|
|
17
|
+
- 13f49f85: Allow `SingleSelect` and `MultiSelect` to be focusable when disabled. These components now have `aria-disabled` when the `disabled` prop is `true`. This makes it so screenreaders will continue to communicate that the component is disabled, while allowing focus on the disabled component. Focus styling is also added to the disabled state.
|
|
18
|
+
- Updated dependencies [47a758b6]
|
|
19
|
+
- @khanacademy/wonder-blocks-layout@2.1.0
|
|
20
|
+
- @khanacademy/wonder-blocks-cell@3.3.7
|
|
21
|
+
- @khanacademy/wonder-blocks-modal@5.1.4
|
|
22
|
+
- @khanacademy/wonder-blocks-search-field@2.2.15
|
|
23
|
+
|
|
3
24
|
## 5.3.5
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
|
@@ -77,6 +77,10 @@ declare const _default: React.ComponentType<Readonly<{
|
|
|
77
77
|
* top. The items will be filtered by the input.
|
|
78
78
|
*/
|
|
79
79
|
isFilterable?: boolean | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the dropdown and it's interactions should be disabled.
|
|
82
|
+
*/
|
|
83
|
+
disabled?: boolean | undefined;
|
|
80
84
|
/**
|
|
81
85
|
* Whether this menu should be left-aligned or right-aligned with the
|
|
82
86
|
* opener component. Defaults to left-aligned.
|
|
@@ -49,13 +49,25 @@ type DefaultProps = {
|
|
|
49
49
|
light: SelectOpenerProps["light"];
|
|
50
50
|
isPlaceholder: SelectOpenerProps["isPlaceholder"];
|
|
51
51
|
};
|
|
52
|
+
type SelectOpenerState = {
|
|
53
|
+
/**
|
|
54
|
+
* We only keep track of the pressed state to apply styling for when the select
|
|
55
|
+
* opener is pressed using Enter/Space. Other states (active, hover, focus)
|
|
56
|
+
* are not tracked because we use css pseudo-classes to handle those styles
|
|
57
|
+
* instead. Note: `:active` styling is only applied on clicks across browsers,
|
|
58
|
+
* and not on keyboard interaction.
|
|
59
|
+
*/
|
|
60
|
+
pressed: boolean;
|
|
61
|
+
};
|
|
52
62
|
/**
|
|
53
63
|
* An opener that opens select boxes.
|
|
54
64
|
*/
|
|
55
|
-
export default class SelectOpener extends React.Component<SelectOpenerProps> {
|
|
65
|
+
export default class SelectOpener extends React.Component<SelectOpenerProps, SelectOpenerState> {
|
|
56
66
|
static defaultProps: DefaultProps;
|
|
67
|
+
constructor(props: SelectOpenerProps);
|
|
57
68
|
handleClick: (e: React.SyntheticEvent) => void;
|
|
58
|
-
|
|
69
|
+
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
70
|
+
handleKeyUp: (e: React.KeyboardEvent) => void;
|
|
59
71
|
render(): React.ReactNode;
|
|
60
72
|
}
|
|
61
73
|
export {};
|
package/dist/es/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Strut } from '@khanacademy/wonder-blocks-layout';
|
|
|
9
9
|
import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
|
|
10
10
|
import checkIcon from '@phosphor-icons/core/bold/check-bold.svg';
|
|
11
11
|
import * as ReactDOM from 'react-dom';
|
|
12
|
-
import { ClickableBehavior
|
|
12
|
+
import { ClickableBehavior } from '@khanacademy/wonder-blocks-clickable';
|
|
13
13
|
import SearchField from '@khanacademy/wonder-blocks-search-field';
|
|
14
14
|
import { withActionScheduler } from '@khanacademy/wonder-blocks-timing';
|
|
15
15
|
import { VariableSizeList } from 'react-window';
|
|
@@ -17,7 +17,6 @@ import { Popper } from 'react-popper';
|
|
|
17
17
|
import { maybeGetPortalMountedModalHostElement } from '@khanacademy/wonder-blocks-modal';
|
|
18
18
|
import { detectOverflow } from '@popperjs/core';
|
|
19
19
|
import caretDownIcon from '@phosphor-icons/core/bold/caret-down-bold.svg';
|
|
20
|
-
import { __RouterContext } from 'react-router';
|
|
21
20
|
|
|
22
21
|
function _extends() {
|
|
23
22
|
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
@@ -1275,11 +1274,12 @@ class DropdownCore extends React.Component {
|
|
|
1275
1274
|
open,
|
|
1276
1275
|
opener,
|
|
1277
1276
|
style,
|
|
1278
|
-
className
|
|
1277
|
+
className,
|
|
1278
|
+
disabled
|
|
1279
1279
|
} = this.props;
|
|
1280
1280
|
return React.createElement(View, {
|
|
1281
|
-
onKeyDown: this.handleKeyDown,
|
|
1282
|
-
onKeyUp: this.handleKeyUp,
|
|
1281
|
+
onKeyDown: !disabled ? this.handleKeyDown : undefined,
|
|
1282
|
+
onKeyUp: !disabled ? this.handleKeyUp : undefined,
|
|
1283
1283
|
style: [styles$4.menuWrapper, style],
|
|
1284
1284
|
className: className
|
|
1285
1285
|
}, this.renderLiveRegion(), opener, open && this.renderDropdown());
|
|
@@ -1630,16 +1630,37 @@ const styles$2 = StyleSheet.create({
|
|
|
1630
1630
|
const _excluded$2 = ["children", "disabled", "error", "id", "isPlaceholder", "light", "open", "testId", "onOpenChanged"];
|
|
1631
1631
|
const StyledButton = addStyle("button");
|
|
1632
1632
|
class SelectOpener extends React.Component {
|
|
1633
|
-
constructor(
|
|
1634
|
-
super(
|
|
1633
|
+
constructor(props) {
|
|
1634
|
+
super(props);
|
|
1635
1635
|
this.handleClick = e => {
|
|
1636
1636
|
const {
|
|
1637
1637
|
open
|
|
1638
1638
|
} = this.props;
|
|
1639
1639
|
this.props.onOpenChanged(!open);
|
|
1640
1640
|
};
|
|
1641
|
+
this.handleKeyDown = e => {
|
|
1642
|
+
const keyCode = e.key;
|
|
1643
|
+
if (keyCode === "Enter" || keyCode === " ") {
|
|
1644
|
+
this.setState({
|
|
1645
|
+
pressed: true
|
|
1646
|
+
});
|
|
1647
|
+
e.preventDefault();
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
this.handleKeyUp = e => {
|
|
1651
|
+
const keyCode = e.key;
|
|
1652
|
+
if (keyCode === "Enter" || keyCode === " ") {
|
|
1653
|
+
this.setState({
|
|
1654
|
+
pressed: false
|
|
1655
|
+
});
|
|
1656
|
+
this.handleClick(e);
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
this.state = {
|
|
1660
|
+
pressed: false
|
|
1661
|
+
};
|
|
1641
1662
|
}
|
|
1642
|
-
|
|
1663
|
+
render() {
|
|
1643
1664
|
const _this$props = this.props,
|
|
1644
1665
|
{
|
|
1645
1666
|
children,
|
|
@@ -1652,40 +1673,29 @@ class SelectOpener extends React.Component {
|
|
|
1652
1673
|
testId
|
|
1653
1674
|
} = _this$props,
|
|
1654
1675
|
sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
}, children || "\u00A0"), React.createElement(PhosphorIcon, {
|
|
1679
|
-
icon: caretDownIcon,
|
|
1680
|
-
color: iconColor,
|
|
1681
|
-
size: "small",
|
|
1682
|
-
style: styles$1.caret,
|
|
1683
|
-
"aria-hidden": "true"
|
|
1684
|
-
}));
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
render() {
|
|
1688
|
-
return React.createElement(__RouterContext.Consumer, null, router => this.renderClickableBehavior(router));
|
|
1676
|
+
const stateStyles = _generateStyles(light, isPlaceholder, error);
|
|
1677
|
+
const iconColor = light ? disabled || error ? "currentColor" : tokens.color.white : disabled ? tokens.color.offBlack32 : tokens.color.offBlack64;
|
|
1678
|
+
const style = [styles$1.shared, stateStyles.default, disabled && stateStyles.disabled, !disabled && this.state.pressed && stateStyles.pressed];
|
|
1679
|
+
return React.createElement(StyledButton, _extends({}, sharedProps, {
|
|
1680
|
+
"aria-disabled": disabled,
|
|
1681
|
+
"aria-expanded": open ? "true" : "false",
|
|
1682
|
+
"aria-haspopup": "listbox",
|
|
1683
|
+
"data-testid": testId,
|
|
1684
|
+
id: id,
|
|
1685
|
+
style: style,
|
|
1686
|
+
type: "button",
|
|
1687
|
+
onClick: !disabled ? this.handleClick : undefined,
|
|
1688
|
+
onKeyDown: !disabled ? this.handleKeyDown : undefined,
|
|
1689
|
+
onKeyUp: !disabled ? this.handleKeyUp : undefined
|
|
1690
|
+
}), React.createElement(LabelMedium, {
|
|
1691
|
+
style: styles$1.text
|
|
1692
|
+
}, children || "\u00A0"), React.createElement(PhosphorIcon, {
|
|
1693
|
+
icon: caretDownIcon,
|
|
1694
|
+
color: iconColor,
|
|
1695
|
+
size: "small",
|
|
1696
|
+
style: styles$1.caret,
|
|
1697
|
+
"aria-hidden": "true"
|
|
1698
|
+
}));
|
|
1689
1699
|
}
|
|
1690
1700
|
}
|
|
1691
1701
|
SelectOpener.defaultProps = {
|
|
@@ -1734,61 +1744,91 @@ const _generateStyles = (light, placeholder, error) => {
|
|
|
1734
1744
|
}
|
|
1735
1745
|
let newStyles = {};
|
|
1736
1746
|
if (light) {
|
|
1747
|
+
const focusHoverStyling = {
|
|
1748
|
+
borderColor: error ? tokens.color.red : tokens.color.white,
|
|
1749
|
+
borderWidth: tokens.spacing.xxxxSmall_2,
|
|
1750
|
+
paddingLeft: adjustedPaddingLeft,
|
|
1751
|
+
paddingRight: adjustedPaddingRight
|
|
1752
|
+
};
|
|
1753
|
+
const activePressedStyling = {
|
|
1754
|
+
paddingLeft: adjustedPaddingLeft,
|
|
1755
|
+
paddingRight: adjustedPaddingRight,
|
|
1756
|
+
borderColor: error ? tokens.color.red : tokens.color.fadedBlue,
|
|
1757
|
+
borderWidth: tokens.border.width.thin,
|
|
1758
|
+
color: error ? tokens.color.offBlack64 : placeholder ? mix(tokens.color.white32, tokens.color.blue) : tokens.color.fadedBlue,
|
|
1759
|
+
backgroundColor: error ? tokens.color.fadedRed : tokens.color.activeBlue
|
|
1760
|
+
};
|
|
1737
1761
|
newStyles = {
|
|
1738
1762
|
default: {
|
|
1739
1763
|
background: error ? tokens.color.fadedRed8 : "transparent",
|
|
1740
|
-
color: placeholder ? tokens.color.white50 : tokens.color.white,
|
|
1764
|
+
color: error ? tokens.color.offBlack64 : placeholder ? tokens.color.white50 : tokens.color.white,
|
|
1741
1765
|
borderColor: error ? tokens.color.red : tokens.color.white50,
|
|
1742
|
-
borderWidth: tokens.border.width.hairline
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
borderWidth: tokens.border.width.thin,
|
|
1755
|
-
color: placeholder ? mix(tokens.color.white32, tokens.color.blue) : tokens.color.fadedBlue,
|
|
1756
|
-
backgroundColor: error ? tokens.color.fadedRed : tokens.color.activeBlue
|
|
1766
|
+
borderWidth: tokens.border.width.hairline,
|
|
1767
|
+
":hover:not([aria-disabled=true])": focusHoverStyling,
|
|
1768
|
+
["@media not (hover: hover)"]: {
|
|
1769
|
+
":hover:not([aria-disabled=true])": {
|
|
1770
|
+
borderColor: error ? tokens.color.red : tokens.color.white50,
|
|
1771
|
+
borderWidth: tokens.border.width.hairline,
|
|
1772
|
+
paddingLeft: tokens.spacing.medium_16,
|
|
1773
|
+
paddingRight: tokens.spacing.small_12
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
":focus-visible:not([aria-disabled=true])": focusHoverStyling,
|
|
1777
|
+
":active:not([aria-disabled=true])": activePressedStyling
|
|
1757
1778
|
},
|
|
1758
1779
|
disabled: {
|
|
1759
1780
|
background: "transparent",
|
|
1760
1781
|
borderColor: mix(tokens.color.white32, tokens.color.blue),
|
|
1761
1782
|
color: mix(tokens.color.white32, tokens.color.blue),
|
|
1762
|
-
cursor: "
|
|
1763
|
-
|
|
1783
|
+
cursor: "not-allowed",
|
|
1784
|
+
":focus-visible": {
|
|
1785
|
+
boxShadow: `0 0 0 1px ${tokens.color.offBlack32}, 0 0 0 3px ${tokens.color.fadedBlue}`
|
|
1786
|
+
}
|
|
1787
|
+
},
|
|
1788
|
+
pressed: activePressedStyling
|
|
1764
1789
|
};
|
|
1765
1790
|
} else {
|
|
1791
|
+
const focusHoverStyling = {
|
|
1792
|
+
borderColor: error ? tokens.color.red : tokens.color.blue,
|
|
1793
|
+
borderWidth: tokens.border.width.thin,
|
|
1794
|
+
paddingLeft: adjustedPaddingLeft,
|
|
1795
|
+
paddingRight: adjustedPaddingRight
|
|
1796
|
+
};
|
|
1797
|
+
const activePressedStyling = {
|
|
1798
|
+
background: error ? tokens.color.fadedRed : tokens.color.fadedBlue,
|
|
1799
|
+
borderColor: error ? tokens.color.red : tokens.color.activeBlue,
|
|
1800
|
+
borderWidth: tokens.border.width.thin,
|
|
1801
|
+
paddingLeft: adjustedPaddingLeft,
|
|
1802
|
+
paddingRight: adjustedPaddingRight
|
|
1803
|
+
};
|
|
1766
1804
|
newStyles = {
|
|
1767
1805
|
default: {
|
|
1768
1806
|
background: error ? tokens.color.fadedRed8 : tokens.color.white,
|
|
1769
1807
|
borderColor: error ? tokens.color.red : tokens.color.offBlack16,
|
|
1770
1808
|
borderWidth: tokens.border.width.hairline,
|
|
1771
|
-
color: placeholder ? tokens.color.offBlack64 : tokens.color.offBlack
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
paddingLeft: adjustedPaddingLeft,
|
|
1784
|
-
paddingRight: adjustedPaddingRight
|
|
1809
|
+
color: placeholder ? tokens.color.offBlack64 : tokens.color.offBlack,
|
|
1810
|
+
":hover:not([aria-disabled=true])": focusHoverStyling,
|
|
1811
|
+
["@media not (hover: hover)"]: {
|
|
1812
|
+
":hover:not([aria-disabled=true])": {
|
|
1813
|
+
borderColor: error ? tokens.color.red : tokens.color.offBlack16,
|
|
1814
|
+
borderWidth: tokens.border.width.hairline,
|
|
1815
|
+
paddingLeft: tokens.spacing.medium_16,
|
|
1816
|
+
paddingRight: tokens.spacing.small_12
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
":focus-visible:not([aria-disabled=true])": focusHoverStyling,
|
|
1820
|
+
":active:not([aria-disabled=true])": activePressedStyling
|
|
1785
1821
|
},
|
|
1786
1822
|
disabled: {
|
|
1787
1823
|
background: tokens.color.offWhite,
|
|
1788
1824
|
borderColor: tokens.color.offBlack16,
|
|
1789
1825
|
color: tokens.color.offBlack64,
|
|
1790
|
-
cursor: "
|
|
1791
|
-
|
|
1826
|
+
cursor: "not-allowed",
|
|
1827
|
+
":focus-visible": {
|
|
1828
|
+
boxShadow: `0 0 0 1px ${tokens.color.white}, 0 0 0 3px ${tokens.color.offBlack32}`
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
pressed: activePressedStyling
|
|
1792
1832
|
};
|
|
1793
1833
|
}
|
|
1794
1834
|
stateStyles[styleKey] = StyleSheet.create(newStyles);
|
|
@@ -1874,7 +1914,7 @@ class SingleSelect extends React.Component {
|
|
|
1874
1914
|
}
|
|
1875
1915
|
static getDerivedStateFromProps(props, state) {
|
|
1876
1916
|
return {
|
|
1877
|
-
open: typeof props.opened === "boolean" ? props.opened : state.open
|
|
1917
|
+
open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
|
|
1878
1918
|
};
|
|
1879
1919
|
}
|
|
1880
1920
|
filterChildren(children) {
|
|
@@ -1941,7 +1981,8 @@ class SingleSelect extends React.Component {
|
|
|
1941
1981
|
light,
|
|
1942
1982
|
style,
|
|
1943
1983
|
"aria-invalid": ariaInvalid,
|
|
1944
|
-
"aria-required": ariaRequired
|
|
1984
|
+
"aria-required": ariaRequired,
|
|
1985
|
+
disabled
|
|
1945
1986
|
} = this.props;
|
|
1946
1987
|
const {
|
|
1947
1988
|
searchText
|
|
@@ -1970,7 +2011,8 @@ class SingleSelect extends React.Component {
|
|
|
1970
2011
|
searchText: isFilterable ? searchText : "",
|
|
1971
2012
|
labels: labels,
|
|
1972
2013
|
"aria-invalid": ariaInvalid,
|
|
1973
|
-
"aria-required": ariaRequired
|
|
2014
|
+
"aria-required": ariaRequired,
|
|
2015
|
+
disabled: disabled
|
|
1974
2016
|
});
|
|
1975
2017
|
}
|
|
1976
2018
|
}
|
|
@@ -2074,7 +2116,7 @@ class MultiSelect extends React.Component {
|
|
|
2074
2116
|
}
|
|
2075
2117
|
static getDerivedStateFromProps(props, state) {
|
|
2076
2118
|
return {
|
|
2077
|
-
open: typeof props.opened === "boolean" ? props.opened : state.open
|
|
2119
|
+
open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
|
|
2078
2120
|
};
|
|
2079
2121
|
}
|
|
2080
2122
|
componentDidUpdate(prevProps) {
|
|
@@ -2239,7 +2281,8 @@ class MultiSelect extends React.Component {
|
|
|
2239
2281
|
children,
|
|
2240
2282
|
isFilterable,
|
|
2241
2283
|
"aria-invalid": ariaInvalid,
|
|
2242
|
-
"aria-required": ariaRequired
|
|
2284
|
+
"aria-required": ariaRequired,
|
|
2285
|
+
disabled
|
|
2243
2286
|
} = this.props;
|
|
2244
2287
|
const {
|
|
2245
2288
|
open,
|
|
@@ -2278,7 +2321,8 @@ class MultiSelect extends React.Component {
|
|
|
2278
2321
|
someResults: someSelected
|
|
2279
2322
|
},
|
|
2280
2323
|
"aria-invalid": ariaInvalid,
|
|
2281
|
-
"aria-required": ariaRequired
|
|
2324
|
+
"aria-required": ariaRequired,
|
|
2325
|
+
disabled: disabled
|
|
2282
2326
|
});
|
|
2283
2327
|
}
|
|
2284
2328
|
}
|