@khanacademy/wonder-blocks-dropdown 5.8.1 → 6.1.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/index.js CHANGED
@@ -61,13 +61,12 @@ var xIcon__default = /*#__PURE__*/_interopDefaultLegacy(xIcon);
61
61
  var IconButton__default = /*#__PURE__*/_interopDefaultLegacy(IconButton);
62
62
  var Pill__default = /*#__PURE__*/_interopDefaultLegacy(Pill);
63
63
 
64
- const keyCodes = {
65
- tab: 9,
66
- enter: 13,
67
- escape: 27,
68
- space: 32,
69
- up: 38,
70
- down: 40
64
+ const keys = {
65
+ escape: "Escape",
66
+ tab: "Tab",
67
+ space: " ",
68
+ up: "ArrowUp",
69
+ down: "ArrowDown"
71
70
  };
72
71
  const selectDropdownStyle = {
73
72
  marginTop: tokens.spacing.xSmall_8,
@@ -531,7 +530,9 @@ class DropdownOpener extends React__namespace.Component {
531
530
  opened,
532
531
  "aria-controls": ariaControls,
533
532
  "aria-haspopup": ariaHasPopUp,
534
- id
533
+ "aria-required": ariaRequired,
534
+ id,
535
+ onBlur
535
536
  } = this.props;
536
537
  const renderedChildren = this.props.children(_extends__default["default"]({}, eventState, {
537
538
  text,
@@ -540,16 +541,19 @@ class DropdownOpener extends React__namespace.Component {
540
541
  const childrenProps = renderedChildren.props;
541
542
  const childrenTestId = this.getTestIdFromProps(childrenProps);
542
543
  return React__namespace.cloneElement(renderedChildren, _extends__default["default"]({}, clickableChildrenProps, {
544
+ "aria-invalid": this.props.error,
543
545
  disabled,
544
546
  "aria-controls": ariaControls,
545
547
  id,
546
548
  "aria-expanded": opened ? "true" : "false",
547
549
  "aria-haspopup": ariaHasPopUp,
550
+ "aria-required": ariaRequired,
548
551
  onClick: childrenProps.onClick ? e => {
549
552
  childrenProps.onClick(e);
550
553
  clickableChildrenProps.onClick(e);
551
554
  } : clickableChildrenProps.onClick,
552
- "data-testid": childrenTestId || testId
555
+ "data-testid": childrenTestId || testId,
556
+ onBlur
553
557
  }));
554
558
  }
555
559
  render() {
@@ -861,13 +865,13 @@ class DropdownCore extends React__namespace.Component {
861
865
  }
862
866
  constructor(props) {
863
867
  super(props);
864
- this.focusedIndex = void 0;
865
- this.focusedOriginalIndex = void 0;
866
- this.itemsClicked = void 0;
867
868
  this.popperElement = void 0;
868
869
  this.virtualizedListRef = void 0;
869
870
  this.handleKeyDownDebounced = void 0;
870
871
  this.textSuggestion = void 0;
872
+ this.focusedIndex = -1;
873
+ this.focusedOriginalIndex = -1;
874
+ this.itemsClicked = false;
871
875
  this.searchFieldRef = React__namespace.createRef();
872
876
  this.handleInteract = event => {
873
877
  const {
@@ -887,39 +891,39 @@ class DropdownCore extends React__namespace.Component {
887
891
  open,
888
892
  searchText
889
893
  } = this.props;
890
- const keyCode = event.which || event.keyCode;
891
- if (enableTypeAhead && getStringForKey(event.key)) {
894
+ const key = event.key;
895
+ if (enableTypeAhead && getStringForKey(key)) {
892
896
  event.stopPropagation();
893
- this.textSuggestion += event.key;
897
+ this.textSuggestion += key;
894
898
  this.handleKeyDownDebounced(this.textSuggestion);
895
899
  }
896
900
  if (!open) {
897
- if (keyCode === keyCodes.down) {
901
+ if (key === keys.down) {
898
902
  event.preventDefault();
899
903
  onOpenChanged(true);
900
904
  return;
901
905
  }
902
906
  return;
903
907
  }
904
- switch (keyCode) {
905
- case keyCodes.tab:
908
+ switch (key) {
909
+ case keys.tab:
906
910
  if (this.isSearchFieldFocused() && searchText) {
907
911
  return;
908
912
  }
909
913
  this.restoreTabOrder();
910
914
  onOpenChanged(false);
911
915
  return;
912
- case keyCodes.space:
916
+ case keys.space:
913
917
  if (this.isSearchFieldFocused()) {
914
918
  return;
915
919
  }
916
920
  event.preventDefault();
917
921
  return;
918
- case keyCodes.up:
922
+ case keys.up:
919
923
  event.preventDefault();
920
924
  this.focusPreviousItem();
921
925
  return;
922
- case keyCodes.down:
926
+ case keys.down:
923
927
  event.preventDefault();
924
928
  this.focusNextItem();
925
929
  return;
@@ -930,15 +934,15 @@ class DropdownCore extends React__namespace.Component {
930
934
  onOpenChanged,
931
935
  open
932
936
  } = this.props;
933
- const keyCode = event.which || event.keyCode;
934
- switch (keyCode) {
935
- case keyCodes.space:
937
+ const key = event.key;
938
+ switch (key) {
939
+ case keys.space:
936
940
  if (this.isSearchFieldFocused()) {
937
941
  return;
938
942
  }
939
943
  event.preventDefault();
940
944
  return;
941
- case keyCodes.escape:
945
+ case keys.escape:
942
946
  if (open) {
943
947
  event.stopPropagation();
944
948
  this.restoreTabOrder();
@@ -1111,18 +1115,37 @@ class DropdownCore extends React__namespace.Component {
1111
1115
  }
1112
1116
  focusCurrentItem(onFocus) {
1113
1117
  const focusedItemRef = this.state.itemRefs[this.focusedIndex];
1114
- if (focusedItemRef) {
1115
- if (this.virtualizedListRef.current) {
1116
- this.virtualizedListRef.current.scrollToItem(focusedItemRef.originalIndex);
1118
+ if (!focusedItemRef) {
1119
+ return;
1120
+ }
1121
+ const {
1122
+ current: virtualizedList
1123
+ } = this.virtualizedListRef;
1124
+ if (virtualizedList) {
1125
+ virtualizedList.scrollToItem(focusedItemRef.originalIndex);
1126
+ }
1127
+ const focusNode = () => {
1128
+ if (!this.props.open) {
1129
+ return;
1130
+ }
1131
+ const currentFocusedItemRef = this.state.itemRefs[this.focusedIndex];
1132
+ const node = ReactDOM__namespace.findDOMNode(currentFocusedItemRef.ref.current);
1133
+ if (!node && this.shouldVirtualizeList()) {
1134
+ this.props.schedule.animationFrame(focusNode);
1135
+ return;
1117
1136
  }
1118
- const node = ReactDOM__namespace.findDOMNode(focusedItemRef.ref.current);
1119
1137
  if (node) {
1120
1138
  node.focus();
1121
- this.focusedOriginalIndex = focusedItemRef.originalIndex;
1139
+ this.focusedOriginalIndex = currentFocusedItemRef.originalIndex;
1122
1140
  if (onFocus) {
1123
1141
  onFocus(node);
1124
1142
  }
1125
1143
  }
1144
+ };
1145
+ if (this.shouldVirtualizeList()) {
1146
+ this.props.schedule.animationFrame(focusNode);
1147
+ } else {
1148
+ focusNode();
1126
1149
  }
1127
1150
  }
1128
1151
  focusSearchField() {
@@ -1142,7 +1165,7 @@ class DropdownCore extends React__namespace.Component {
1142
1165
  return this.focusSearchField();
1143
1166
  }
1144
1167
  this.focusedIndex = this.state.itemRefs.length - 1;
1145
- } else {
1168
+ } else if (!this.isSearchFieldFocused()) {
1146
1169
  this.focusedIndex -= 1;
1147
1170
  }
1148
1171
  this.scheduleToFocusCurrentItem();
@@ -1153,7 +1176,7 @@ class DropdownCore extends React__namespace.Component {
1153
1176
  return this.focusSearchField();
1154
1177
  }
1155
1178
  this.focusedIndex = 0;
1156
- } else {
1179
+ } else if (!this.isSearchFieldFocused()) {
1157
1180
  this.focusedIndex += 1;
1158
1181
  }
1159
1182
  this.scheduleToFocusCurrentItem();
@@ -1235,7 +1258,7 @@ class DropdownCore extends React__namespace.Component {
1235
1258
  const focusIndex = focusCounter - 1;
1236
1259
  return _extends__default["default"]({}, item, {
1237
1260
  role: populatedProps.role || itemRole,
1238
- ref: item.focusable ? this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null : null,
1261
+ ref: item.focusable && this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null,
1239
1262
  onClick: () => {
1240
1263
  this.handleItemClick(focusIndex, item);
1241
1264
  }
@@ -1696,7 +1719,7 @@ const styles$5 = aphrodite.StyleSheet.create({
1696
1719
  }
1697
1720
  });
1698
1721
 
1699
- const _excluded$2 = ["children", "disabled", "error", "id", "isPlaceholder", "light", "open", "testId", "onOpenChanged"];
1722
+ const _excluded$2 = ["children", "disabled", "error", "id", "isPlaceholder", "light", "open", "testId", "aria-required", "onBlur", "onOpenChanged"];
1700
1723
  const StyledButton = wonderBlocksCore.addStyle("button");
1701
1724
  class SelectOpener extends React__namespace.Component {
1702
1725
  constructor(props) {
@@ -1739,7 +1762,9 @@ class SelectOpener extends React__namespace.Component {
1739
1762
  isPlaceholder,
1740
1763
  light,
1741
1764
  open,
1742
- testId
1765
+ testId,
1766
+ "aria-required": ariaRequired,
1767
+ onBlur
1743
1768
  } = _this$props,
1744
1769
  sharedProps = _objectWithoutPropertiesLoose__default["default"](_this$props, _excluded$2);
1745
1770
  const stateStyles = _generateStyles(light, isPlaceholder, error);
@@ -1748,6 +1773,8 @@ class SelectOpener extends React__namespace.Component {
1748
1773
  return React__namespace.createElement(StyledButton, _extends__default["default"]({}, sharedProps, {
1749
1774
  "aria-disabled": disabled,
1750
1775
  "aria-expanded": open ? "true" : "false",
1776
+ "aria-invalid": error,
1777
+ "aria-required": ariaRequired,
1751
1778
  "aria-haspopup": "listbox",
1752
1779
  "data-testid": testId,
1753
1780
  id: id,
@@ -1755,7 +1782,8 @@ class SelectOpener extends React__namespace.Component {
1755
1782
  type: "button",
1756
1783
  onClick: !disabled ? this.handleClick : undefined,
1757
1784
  onKeyDown: !disabled ? this.handleKeyDown : undefined,
1758
- onKeyUp: !disabled ? this.handleKeyUp : undefined
1785
+ onKeyUp: !disabled ? this.handleKeyUp : undefined,
1786
+ onBlur: onBlur
1759
1787
  }), React__namespace.createElement(wonderBlocksTypography.LabelMedium, {
1760
1788
  style: styles$4.text
1761
1789
  }, children || "\u00A0"), React__namespace.createElement(wonderBlocksIcon.PhosphorIcon, {
@@ -1904,117 +1932,206 @@ const _generateStyles = (light, placeholder, error) => {
1904
1932
  return stateStyles[styleKey];
1905
1933
  };
1906
1934
 
1907
- const _excluded$1 = ["children", "error", "id", "light", "opener", "placeholder", "selectedValue", "testId", "showOpenerLabelAsText", "alignment", "autoFocus", "dropdownStyle", "enableTypeAhead", "isFilterable", "labels", "onChange", "onToggle", "opened", "style", "className", "aria-invalid", "aria-required"];
1908
- class SingleSelect extends React__namespace.Component {
1909
- constructor(props) {
1910
- super(props);
1911
- this.selectedIndex = void 0;
1912
- this.handleOpenChanged = opened => {
1913
- this.setState({
1914
- open: opened,
1915
- searchText: ""
1916
- });
1917
- if (this.props.onToggle) {
1918
- this.props.onToggle(opened);
1935
+ const defaultErrorMessage = "This field is required.";
1936
+ function hasValue(value) {
1937
+ return value ? value.length > 0 : false;
1938
+ }
1939
+ function useSelectValidation({
1940
+ value,
1941
+ disabled = false,
1942
+ validate,
1943
+ onValidate,
1944
+ required,
1945
+ open
1946
+ }) {
1947
+ const [errorMessage, setErrorMessage] = React__namespace.useState(() => validate && hasValue(value) && !disabled && validate(value) || null);
1948
+ const handleValidation = React__namespace.useCallback(newValue => {
1949
+ if (disabled) {
1950
+ return;
1951
+ }
1952
+ if (validate) {
1953
+ const error = newValue !== undefined && validate(newValue) || null;
1954
+ setErrorMessage(error);
1955
+ if (onValidate) {
1956
+ onValidate(error);
1919
1957
  }
1920
- };
1921
- this.handleToggle = selectedValue => {
1922
- if (selectedValue !== this.props.selectedValue) {
1923
- this.props.onChange(selectedValue);
1958
+ if (error) {
1959
+ return;
1924
1960
  }
1925
- if (this.state.open && this.state.openerElement) {
1926
- this.state.openerElement.focus();
1961
+ }
1962
+ if (required) {
1963
+ const requiredString = typeof required === "string" ? required : defaultErrorMessage;
1964
+ const error = hasValue(newValue) ? null : requiredString;
1965
+ setErrorMessage(error);
1966
+ if (onValidate) {
1967
+ onValidate(error);
1927
1968
  }
1928
- this.setState({
1929
- open: false
1930
- });
1931
- if (this.props.onToggle) {
1932
- this.props.onToggle(false);
1969
+ }
1970
+ }, [disabled, validate, setErrorMessage, onValidate, required]);
1971
+ wonderBlocksCore.useOnMountEffect(() => {
1972
+ if (hasValue(value)) {
1973
+ handleValidation(value);
1974
+ }
1975
+ });
1976
+ function onOpenerBlurValidation() {
1977
+ if (!open && required && !hasValue(value)) {
1978
+ handleValidation(value);
1979
+ }
1980
+ }
1981
+ const onDropdownClosedValidation = () => {
1982
+ if (required && !hasValue(value)) {
1983
+ handleValidation(value);
1984
+ }
1985
+ };
1986
+ const onSelectionValidation = newValue => {
1987
+ handleValidation(newValue);
1988
+ };
1989
+ const onSelectedValuesChangeValidation = () => {
1990
+ setErrorMessage(null);
1991
+ if (onValidate) {
1992
+ onValidate(null);
1993
+ }
1994
+ };
1995
+ return {
1996
+ errorMessage,
1997
+ onOpenerBlurValidation,
1998
+ onDropdownClosedValidation,
1999
+ onSelectionValidation,
2000
+ onSelectedValuesChangeValidation
2001
+ };
2002
+ }
2003
+
2004
+ const _excluded$1 = ["children", "error", "id", "opener", "light", "placeholder", "selectedValue", "testId", "alignment", "autoFocus", "dropdownStyle", "enableTypeAhead", "isFilterable", "labels", "onChange", "onToggle", "opened", "style", "className", "aria-invalid", "aria-required", "disabled", "dropdownId", "validate", "onValidate", "required", "showOpenerLabelAsText"];
2005
+ const SingleSelect = props => {
2006
+ const selectedIndex = React__namespace.useRef(0);
2007
+ const {
2008
+ children,
2009
+ error = false,
2010
+ id,
2011
+ opener,
2012
+ light = false,
2013
+ placeholder,
2014
+ selectedValue,
2015
+ testId,
2016
+ alignment = "left",
2017
+ autoFocus = true,
2018
+ dropdownStyle,
2019
+ enableTypeAhead = true,
2020
+ isFilterable,
2021
+ labels = {
2022
+ clearSearch: defaultLabels.clearSearch,
2023
+ filter: defaultLabels.filter,
2024
+ noResults: defaultLabels.noResults,
2025
+ someResults: defaultLabels.someSelected
2026
+ },
2027
+ onChange,
2028
+ onToggle,
2029
+ opened,
2030
+ style,
2031
+ className,
2032
+ "aria-invalid": ariaInvalid,
2033
+ "aria-required": ariaRequired,
2034
+ disabled = false,
2035
+ dropdownId,
2036
+ validate,
2037
+ onValidate,
2038
+ required,
2039
+ showOpenerLabelAsText = true
2040
+ } = props,
2041
+ sharedProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded$1);
2042
+ const [open, setOpen] = React__namespace.useState(false);
2043
+ const [searchText, setSearchText] = React__namespace.useState("");
2044
+ const [openerElement, setOpenerElement] = React__namespace.useState();
2045
+ const {
2046
+ errorMessage,
2047
+ onOpenerBlurValidation,
2048
+ onDropdownClosedValidation,
2049
+ onSelectionValidation
2050
+ } = useSelectValidation({
2051
+ value: selectedValue,
2052
+ disabled,
2053
+ validate,
2054
+ onValidate,
2055
+ required,
2056
+ open
2057
+ });
2058
+ const hasError = error || !!errorMessage;
2059
+ React__namespace.useEffect(() => {
2060
+ if (disabled) {
2061
+ setOpen(false);
2062
+ } else if (typeof opened === "boolean") {
2063
+ setOpen(opened);
2064
+ }
2065
+ }, [disabled, opened]);
2066
+ const handleOpenChanged = opened => {
2067
+ setOpen(opened);
2068
+ setSearchText("");
2069
+ if (onToggle) {
2070
+ onToggle(opened);
2071
+ }
2072
+ if (!opened) {
2073
+ onDropdownClosedValidation();
2074
+ }
2075
+ };
2076
+ const handleToggle = newSelectedValue => {
2077
+ if (newSelectedValue !== selectedValue) {
2078
+ onChange(newSelectedValue);
2079
+ }
2080
+ if (open && openerElement) {
2081
+ openerElement.focus();
2082
+ }
2083
+ setOpen(false);
2084
+ if (onToggle) {
2085
+ onToggle(false);
2086
+ }
2087
+ onSelectionValidation(newSelectedValue);
2088
+ };
2089
+ const mapOptionItemsToDropdownItems = children => {
2090
+ let indexCounter = 0;
2091
+ selectedIndex.current = 0;
2092
+ return children.map(option => {
2093
+ const {
2094
+ disabled,
2095
+ value
2096
+ } = option.props;
2097
+ const selected = selectedValue === value;
2098
+ if (selected) {
2099
+ selectedIndex.current = indexCounter;
1933
2100
  }
1934
- };
1935
- this.mapOptionItemsToDropdownItems = children => {
1936
- let indexCounter = 0;
1937
- this.selectedIndex = 0;
1938
- return children.map(option => {
1939
- const {
1940
- selectedValue
1941
- } = this.props;
1942
- const {
1943
- disabled,
1944
- value
1945
- } = option.props;
1946
- const selected = selectedValue === value;
1947
- if (selected) {
1948
- this.selectedIndex = indexCounter;
1949
- }
1950
- if (!disabled) {
1951
- indexCounter += 1;
2101
+ if (!disabled) {
2102
+ indexCounter += 1;
2103
+ }
2104
+ return {
2105
+ component: option,
2106
+ focusable: !disabled,
2107
+ populatedProps: {
2108
+ onToggle: handleToggle,
2109
+ selected: selected,
2110
+ variant: "check"
1952
2111
  }
1953
- return {
1954
- component: option,
1955
- focusable: !disabled,
1956
- populatedProps: {
1957
- onToggle: this.handleToggle,
1958
- selected: selected,
1959
- variant: "check"
1960
- }
1961
- };
1962
- });
1963
- };
1964
- this.handleSearchTextChanged = searchText => {
1965
- this.setState({
1966
- searchText
1967
- });
1968
- };
1969
- this.handleOpenerRef = node => {
1970
- const openerElement = ReactDOM__namespace.findDOMNode(node);
1971
- this.setState({
1972
- openerElement
1973
- });
1974
- };
1975
- this.handleClick = e => {
1976
- this.handleOpenChanged(!this.state.open);
1977
- };
1978
- this.selectedIndex = 0;
1979
- this.state = {
1980
- open: false,
1981
- searchText: ""
1982
- };
1983
- }
1984
- static getDerivedStateFromProps(props, state) {
1985
- return {
1986
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
1987
- };
1988
- }
1989
- filterChildren(children) {
1990
- const {
1991
- searchText
1992
- } = this.state;
2112
+ };
2113
+ });
2114
+ };
2115
+ const filterChildren = children => {
1993
2116
  const lowercasedSearchText = searchText.toLowerCase();
1994
2117
  return children.filter(({
1995
2118
  props
1996
2119
  }) => !searchText || getLabel(props).toLowerCase().indexOf(lowercasedSearchText) > -1);
1997
- }
1998
- getMenuItems(children) {
1999
- const {
2000
- isFilterable
2001
- } = this.props;
2002
- return this.mapOptionItemsToDropdownItems(isFilterable ? this.filterChildren(children) : children);
2003
- }
2004
- renderOpener(isDisabled, dropdownId) {
2005
- const _this$props = this.props,
2006
- {
2007
- children,
2008
- error,
2009
- id,
2010
- light,
2011
- opener,
2012
- placeholder,
2013
- selectedValue,
2014
- testId,
2015
- showOpenerLabelAsText
2016
- } = _this$props,
2017
- sharedProps = _objectWithoutPropertiesLoose__default["default"](_this$props, _excluded$1);
2120
+ };
2121
+ const getMenuItems = children => {
2122
+ return mapOptionItemsToDropdownItems(isFilterable ? filterChildren(children) : children);
2123
+ };
2124
+ const handleSearchTextChanged = searchText => {
2125
+ setSearchText(searchText);
2126
+ };
2127
+ const handleOpenerRef = node => {
2128
+ const openerElement = ReactDOM__namespace.findDOMNode(node);
2129
+ setOpenerElement(openerElement);
2130
+ };
2131
+ const handleClick = e => {
2132
+ handleOpenChanged(!open);
2133
+ };
2134
+ const renderOpener = (isDisabled, dropdownId) => {
2018
2135
  const items = React__namespace.Children.toArray(children);
2019
2136
  const selectedItem = items.find(option => option.props.value === selectedValue);
2020
2137
  const menuText = selectedItem ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) : placeholder;
@@ -2026,202 +2143,160 @@ class SingleSelect extends React__namespace.Component {
2026
2143
  id: uniqueOpenerId,
2027
2144
  "aria-controls": dropdownId,
2028
2145
  "aria-haspopup": "listbox",
2029
- onClick: this.handleClick,
2146
+ onClick: handleClick,
2030
2147
  disabled: isDisabled,
2031
- ref: this.handleOpenerRef,
2148
+ ref: handleOpenerRef,
2032
2149
  text: menuText,
2033
- opened: this.state.open
2150
+ opened: open,
2151
+ error: hasError,
2152
+ onBlur: onOpenerBlurValidation
2034
2153
  }, opener) : React__namespace.createElement(SelectOpener, _extends__default["default"]({}, sharedProps, {
2035
2154
  "aria-controls": dropdownId,
2036
2155
  disabled: isDisabled,
2037
2156
  id: uniqueOpenerId,
2038
- error: error,
2157
+ error: hasError,
2039
2158
  isPlaceholder: !selectedItem,
2040
2159
  light: light,
2041
- onOpenChanged: this.handleOpenChanged,
2042
- open: this.state.open,
2043
- ref: this.handleOpenerRef,
2044
- testId: testId
2160
+ onOpenChanged: handleOpenChanged,
2161
+ open: open,
2162
+ ref: handleOpenerRef,
2163
+ testId: testId,
2164
+ onBlur: onOpenerBlurValidation
2045
2165
  }), menuText);
2046
2166
  });
2047
2167
  return dropdownOpener;
2048
- }
2049
- render() {
2050
- const {
2051
- alignment,
2052
- autoFocus,
2053
- children,
2054
- className,
2168
+ };
2169
+ const allChildren = React__namespace.Children.toArray(children).filter(Boolean);
2170
+ const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2171
+ const items = getMenuItems(allChildren);
2172
+ const isDisabled = numEnabledOptions === 0 || disabled;
2173
+ return React__namespace.createElement(wonderBlocksCore.IDProvider, {
2174
+ id: dropdownId,
2175
+ scope: "single-select-dropdown"
2176
+ }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
2177
+ id: uniqueDropdownId,
2178
+ role: "listbox",
2179
+ selectionType: "single",
2180
+ alignment: alignment,
2181
+ autoFocus: autoFocus,
2182
+ enableTypeAhead: enableTypeAhead,
2183
+ dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2184
+ initialFocusedIndex: selectedIndex.current,
2185
+ items: items,
2186
+ light: light,
2187
+ onOpenChanged: handleOpenChanged,
2188
+ open: open,
2189
+ opener: renderOpener(isDisabled, uniqueDropdownId),
2190
+ openerElement: openerElement,
2191
+ style: style,
2192
+ className: className,
2193
+ isFilterable: isFilterable,
2194
+ onSearchTextChanged: isFilterable ? handleSearchTextChanged : undefined,
2195
+ searchText: isFilterable ? searchText : "",
2196
+ labels: labels,
2197
+ "aria-invalid": ariaInvalid,
2198
+ "aria-required": ariaRequired,
2199
+ disabled: isDisabled
2200
+ }));
2201
+ };
2202
+
2203
+ const _excluded = ["id", "light", "opener", "testId", "alignment", "dropdownStyle", "implicitAllEnabled", "isFilterable", "labels", "onChange", "onToggle", "opened", "selectedValues", "shortcuts", "style", "className", "aria-invalid", "aria-required", "disabled", "error", "children", "dropdownId", "showOpenerLabelAsText", "validate", "onValidate", "required"];
2204
+ const MultiSelect = props => {
2205
+ const {
2206
+ id,
2207
+ light = false,
2208
+ opener,
2209
+ testId,
2210
+ alignment = "left",
2055
2211
  dropdownStyle,
2056
- enableTypeAhead,
2212
+ implicitAllEnabled,
2057
2213
  isFilterable,
2058
- labels,
2059
- light,
2214
+ labels: propLabels,
2215
+ onChange,
2216
+ onToggle,
2217
+ opened,
2218
+ selectedValues = [],
2219
+ shortcuts = false,
2060
2220
  style,
2221
+ className,
2061
2222
  "aria-invalid": ariaInvalid,
2062
2223
  "aria-required": ariaRequired,
2063
- disabled,
2064
- dropdownId
2065
- } = this.props;
2066
- const {
2067
- searchText
2068
- } = this.state;
2069
- const allChildren = React__namespace.Children.toArray(children).filter(Boolean);
2070
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2071
- const items = this.getMenuItems(allChildren);
2072
- const isDisabled = numEnabledOptions === 0 || disabled;
2073
- return React__namespace.createElement(wonderBlocksCore.IDProvider, {
2074
- id: dropdownId,
2075
- scope: "single-select-dropdown"
2076
- }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
2077
- id: uniqueDropdownId,
2078
- role: "listbox",
2079
- selectionType: "single",
2080
- alignment: alignment,
2081
- autoFocus: autoFocus,
2082
- enableTypeAhead: enableTypeAhead,
2083
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2084
- initialFocusedIndex: this.selectedIndex,
2085
- items: items,
2086
- light: light,
2087
- onOpenChanged: this.handleOpenChanged,
2088
- open: this.state.open,
2089
- opener: this.renderOpener(isDisabled, uniqueDropdownId),
2090
- openerElement: this.state.openerElement,
2091
- style: style,
2092
- className: className,
2093
- isFilterable: isFilterable,
2094
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2095
- searchText: isFilterable ? searchText : "",
2096
- labels: labels,
2097
- "aria-invalid": ariaInvalid,
2098
- "aria-required": ariaRequired,
2099
- disabled: isDisabled
2100
- }));
2101
- }
2102
- }
2103
- SingleSelect.defaultProps = {
2104
- alignment: "left",
2105
- autoFocus: true,
2106
- disabled: false,
2107
- enableTypeAhead: true,
2108
- error: false,
2109
- light: false,
2110
- labels: {
2111
- clearSearch: defaultLabels.clearSearch,
2112
- filter: defaultLabels.filter,
2113
- noResults: defaultLabels.noResults,
2114
- someResults: defaultLabels.someSelected
2115
- },
2116
- showOpenerLabelAsText: true
2117
- };
2118
-
2119
- const _excluded = ["id", "light", "opener", "testId", "alignment", "dropdownStyle", "implicitAllEnabled", "isFilterable", "labels", "onChange", "onToggle", "opened", "selectedValues", "shortcuts", "style", "className", "aria-invalid", "aria-required", "showOpenerLabelAsText"];
2120
- class MultiSelect extends React__namespace.Component {
2121
- constructor(props) {
2122
- super(props);
2123
- this.labels = void 0;
2124
- this.handleOpenChanged = opened => {
2125
- this.setState({
2126
- open: opened,
2127
- searchText: "",
2128
- lastSelectedValues: this.props.selectedValues
2129
- });
2130
- if (this.props.onToggle) {
2131
- this.props.onToggle(opened);
2132
- }
2133
- };
2134
- this.handleToggle = selectedValue => {
2135
- const {
2136
- onChange,
2137
- selectedValues
2138
- } = this.props;
2139
- if (selectedValues.includes(selectedValue)) {
2140
- const index = selectedValues.indexOf(selectedValue);
2141
- const updatedSelection = [...selectedValues.slice(0, index), ...selectedValues.slice(index + 1)];
2142
- onChange(updatedSelection);
2224
+ disabled = false,
2225
+ error = false,
2226
+ children,
2227
+ dropdownId,
2228
+ showOpenerLabelAsText = true,
2229
+ validate,
2230
+ onValidate,
2231
+ required
2232
+ } = props,
2233
+ sharedProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded);
2234
+ const labels = _extends__default["default"]({}, defaultLabels, propLabels);
2235
+ const [open, setOpen] = React__namespace.useState(false);
2236
+ const [searchText, setSearchText] = React__namespace.useState("");
2237
+ const [lastSelectedValues, setLastSelectedValues] = React__namespace.useState([]);
2238
+ const [openerElement, setOpenerElement] = React__namespace.useState();
2239
+ const {
2240
+ errorMessage,
2241
+ onOpenerBlurValidation,
2242
+ onDropdownClosedValidation,
2243
+ onSelectionValidation,
2244
+ onSelectedValuesChangeValidation
2245
+ } = useSelectValidation({
2246
+ value: selectedValues,
2247
+ disabled,
2248
+ validate,
2249
+ onValidate,
2250
+ required,
2251
+ open
2252
+ });
2253
+ const hasError = error || !!errorMessage;
2254
+ React__namespace.useEffect(() => {
2255
+ if (disabled) {
2256
+ setOpen(false);
2257
+ } else if (typeof opened === "boolean") {
2258
+ setOpen(opened);
2259
+ }
2260
+ }, [disabled, opened]);
2261
+ const handleOpenChanged = opened => {
2262
+ setOpen(opened);
2263
+ setSearchText("");
2264
+ setLastSelectedValues(selectedValues);
2265
+ if (onToggle) {
2266
+ onToggle(opened);
2267
+ }
2268
+ if (!opened) {
2269
+ if (lastSelectedValues !== selectedValues) {
2270
+ onSelectionValidation(selectedValues);
2143
2271
  } else {
2144
- onChange([...selectedValues, selectedValue]);
2272
+ onDropdownClosedValidation();
2145
2273
  }
2146
- };
2147
- this.handleSelectAll = () => {
2148
- const {
2149
- children,
2150
- onChange
2151
- } = this.props;
2152
- const allChildren = React__namespace.Children.toArray(children);
2153
- const selected = allChildren.filter(option => !!option && !option.props.disabled).map(option => option.props.value);
2154
- onChange(selected);
2155
- };
2156
- this.handleSelectNone = () => {
2157
- const {
2158
- onChange
2159
- } = this.props;
2160
- onChange([]);
2161
- };
2162
- this.mapOptionItemToDropdownItem = option => {
2163
- const {
2164
- selectedValues
2165
- } = this.props;
2166
- const {
2167
- disabled,
2168
- value
2169
- } = option.props;
2170
- return {
2171
- component: option,
2172
- focusable: !disabled,
2173
- populatedProps: {
2174
- onToggle: this.handleToggle,
2175
- selected: selectedValues.includes(value),
2176
- variant: "checkbox"
2177
- }
2178
- };
2179
- };
2180
- this.handleOpenerRef = node => {
2181
- const openerElement = ReactDOM__namespace.findDOMNode(node);
2182
- this.setState({
2183
- openerElement
2184
- });
2185
- };
2186
- this.handleSearchTextChanged = searchText => {
2187
- this.setState({
2188
- searchText
2189
- });
2190
- };
2191
- this.handleClick = e => {
2192
- this.handleOpenChanged(!this.state.open);
2193
- };
2194
- this.state = {
2195
- open: false,
2196
- searchText: "",
2197
- lastSelectedValues: [],
2198
- labels: _extends__default["default"]({}, defaultLabels, props.labels)
2199
- };
2200
- this.labels = _extends__default["default"]({}, defaultLabels, props.labels);
2201
- }
2202
- static getDerivedStateFromProps(props, state) {
2203
- return {
2204
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
2205
- };
2206
- }
2207
- componentDidUpdate(prevProps) {
2208
- if (this.props.labels !== prevProps.labels) {
2209
- this.setState({
2210
- labels: _extends__default["default"]({}, this.state.labels, this.props.labels)
2211
- });
2212
2274
  }
2213
- }
2214
- getMenuText(children) {
2215
- const {
2216
- implicitAllEnabled,
2217
- selectedValues,
2218
- showOpenerLabelAsText
2219
- } = this.props;
2275
+ };
2276
+ const handleToggle = selectedValue => {
2277
+ if (selectedValues.includes(selectedValue)) {
2278
+ const index = selectedValues.indexOf(selectedValue);
2279
+ const updatedSelection = [...selectedValues.slice(0, index), ...selectedValues.slice(index + 1)];
2280
+ onChange(updatedSelection);
2281
+ } else {
2282
+ onChange([...selectedValues, selectedValue]);
2283
+ }
2284
+ onSelectedValuesChangeValidation();
2285
+ };
2286
+ const handleSelectAll = () => {
2287
+ const allChildren = React__namespace.Children.toArray(children);
2288
+ const selected = allChildren.filter(option => !!option && !option.props.disabled).map(option => option.props.value);
2289
+ onChange(selected);
2290
+ };
2291
+ const handleSelectNone = () => {
2292
+ onChange([]);
2293
+ };
2294
+ const getMenuText = children => {
2220
2295
  const {
2221
2296
  noneSelected,
2222
2297
  someSelected,
2223
2298
  allSelected
2224
- } = this.state.labels;
2299
+ } = labels;
2225
2300
  const numSelectedAll = children.filter(option => !option.props.disabled).length;
2226
2301
  const noSelectionText = implicitAllEnabled ? allSelected : noneSelected;
2227
2302
  switch (selectedValues.length) {
@@ -2243,24 +2318,20 @@ class MultiSelect extends React__namespace.Component {
2243
2318
  default:
2244
2319
  return someSelected(selectedValues.length);
2245
2320
  }
2246
- }
2247
- getShortcuts(numOptions) {
2248
- const {
2249
- selectedValues,
2250
- shortcuts
2251
- } = this.props;
2321
+ };
2322
+ const getShortcuts = numOptions => {
2252
2323
  const {
2253
2324
  selectAllLabel,
2254
2325
  selectNoneLabel
2255
- } = this.state.labels;
2256
- if (shortcuts && !this.state.searchText) {
2326
+ } = labels;
2327
+ if (shortcuts && !searchText) {
2257
2328
  const selectAllDisabled = numOptions === selectedValues.length;
2258
2329
  const selectAll = {
2259
2330
  component: React__namespace.createElement(ActionItem, {
2260
2331
  disabled: selectAllDisabled,
2261
2332
  label: selectAllLabel(numOptions),
2262
2333
  indent: true,
2263
- onClick: this.handleSelectAll
2334
+ onClick: handleSelectAll
2264
2335
  }),
2265
2336
  focusable: !selectAllDisabled,
2266
2337
  populatedProps: {}
@@ -2271,7 +2342,7 @@ class MultiSelect extends React__namespace.Component {
2271
2342
  disabled: selectNoneDisabled,
2272
2343
  label: selectNoneLabel,
2273
2344
  indent: true,
2274
- onClick: this.handleSelectNone
2345
+ onClick: handleSelectNone
2275
2346
  }),
2276
2347
  focusable: !selectNoneDisabled,
2277
2348
  populatedProps: {}
@@ -2287,18 +2358,11 @@ class MultiSelect extends React__namespace.Component {
2287
2358
  } else {
2288
2359
  return [];
2289
2360
  }
2290
- }
2291
- getMenuItems(children) {
2292
- const {
2293
- isFilterable
2294
- } = this.props;
2361
+ };
2362
+ const getMenuItems = children => {
2295
2363
  if (!isFilterable) {
2296
- return children.map(this.mapOptionItemToDropdownItem);
2364
+ return children.map(mapOptionItemToDropdownItem);
2297
2365
  }
2298
- const {
2299
- searchText,
2300
- lastSelectedValues
2301
- } = this.state;
2302
2366
  const lowercasedSearchText = searchText.toLowerCase();
2303
2367
  const filteredChildren = children.filter(({
2304
2368
  props
@@ -2312,7 +2376,7 @@ class MultiSelect extends React__namespace.Component {
2312
2376
  restOfTheChildren.push(child);
2313
2377
  }
2314
2378
  }
2315
- const lastSelectedItems = lastSelectedChildren.map(this.mapOptionItemToDropdownItem);
2379
+ const lastSelectedItems = lastSelectedChildren.map(mapOptionItemToDropdownItem);
2316
2380
  if (lastSelectedChildren.length && restOfTheChildren.length) {
2317
2381
  lastSelectedItems.push({
2318
2382
  component: React__namespace.createElement(SeparatorItem, {
@@ -2322,116 +2386,109 @@ class MultiSelect extends React__namespace.Component {
2322
2386
  populatedProps: {}
2323
2387
  });
2324
2388
  }
2325
- return [...lastSelectedItems, ...restOfTheChildren.map(this.mapOptionItemToDropdownItem)];
2326
- }
2327
- renderOpener(allChildren, isDisabled, dropdownId) {
2328
- const _this$props = this.props,
2329
- {
2330
- id,
2331
- light,
2332
- opener,
2333
- testId
2334
- } = _this$props,
2335
- sharedProps = _objectWithoutPropertiesLoose__default["default"](_this$props, _excluded);
2389
+ return [...lastSelectedItems, ...restOfTheChildren.map(mapOptionItemToDropdownItem)];
2390
+ };
2391
+ const mapOptionItemToDropdownItem = option => {
2392
+ const {
2393
+ disabled,
2394
+ value
2395
+ } = option.props;
2396
+ return {
2397
+ component: option,
2398
+ focusable: !disabled,
2399
+ populatedProps: {
2400
+ onToggle: handleToggle,
2401
+ selected: selectedValues.includes(value),
2402
+ variant: "checkbox"
2403
+ }
2404
+ };
2405
+ };
2406
+ const handleOpenerRef = node => {
2407
+ const openerElement = ReactDOM__namespace.findDOMNode(node);
2408
+ setOpenerElement(openerElement);
2409
+ };
2410
+ const handleSearchTextChanged = searchText => {
2411
+ setSearchText(searchText);
2412
+ };
2413
+ const handleClick = e => {
2414
+ handleOpenChanged(!open);
2415
+ };
2416
+ const renderOpener = (allChildren, isDisabled, dropdownId) => {
2336
2417
  const {
2337
2418
  noneSelected
2338
- } = this.state.labels;
2339
- const menuText = this.getMenuText(allChildren);
2419
+ } = labels;
2420
+ const menuText = getMenuText(allChildren);
2340
2421
  const dropdownOpener = React__namespace.createElement(wonderBlocksCore.IDProvider, {
2341
2422
  id: id,
2342
2423
  scope: "multi-select-opener"
2343
2424
  }, uniqueOpenerId => {
2344
2425
  return opener ? React__namespace.createElement(DropdownOpener, {
2345
2426
  id: uniqueOpenerId,
2427
+ error: hasError,
2346
2428
  "aria-controls": dropdownId,
2347
2429
  "aria-haspopup": "listbox",
2348
- onClick: this.handleClick,
2430
+ onClick: handleClick,
2431
+ onBlur: onOpenerBlurValidation,
2349
2432
  disabled: isDisabled,
2350
- ref: this.handleOpenerRef,
2433
+ ref: handleOpenerRef,
2351
2434
  text: menuText,
2352
- opened: this.state.open
2435
+ opened: open
2353
2436
  }, opener) : React__namespace.createElement(SelectOpener, _extends__default["default"]({}, sharedProps, {
2437
+ error: hasError,
2354
2438
  disabled: isDisabled,
2355
2439
  id: uniqueOpenerId,
2356
2440
  "aria-controls": dropdownId,
2357
2441
  isPlaceholder: menuText === noneSelected,
2358
2442
  light: light,
2359
- onOpenChanged: this.handleOpenChanged,
2360
- open: this.state.open,
2361
- ref: this.handleOpenerRef,
2443
+ onOpenChanged: handleOpenChanged,
2444
+ onBlur: onOpenerBlurValidation,
2445
+ open: open,
2446
+ ref: handleOpenerRef,
2362
2447
  testId: testId
2363
2448
  }), menuText);
2364
2449
  });
2365
2450
  return dropdownOpener;
2366
- }
2367
- render() {
2368
- const {
2369
- alignment,
2370
- light,
2371
- style,
2372
- className,
2373
- dropdownStyle,
2374
- children,
2375
- isFilterable,
2376
- "aria-invalid": ariaInvalid,
2377
- "aria-required": ariaRequired,
2378
- disabled,
2379
- dropdownId
2380
- } = this.props;
2381
- const {
2382
- open,
2383
- searchText
2384
- } = this.state;
2385
- const {
2451
+ };
2452
+ const {
2453
+ clearSearch,
2454
+ filter,
2455
+ noResults,
2456
+ someSelected
2457
+ } = labels;
2458
+ const allChildren = React__namespace.Children.toArray(children).filter(Boolean);
2459
+ const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2460
+ const filteredItems = getMenuItems(allChildren);
2461
+ const isDisabled = numEnabledOptions === 0 || disabled;
2462
+ return React__namespace.createElement(wonderBlocksCore.IDProvider, {
2463
+ id: dropdownId,
2464
+ scope: "multi-select-dropdown"
2465
+ }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
2466
+ id: uniqueDropdownId,
2467
+ role: "listbox",
2468
+ alignment: alignment,
2469
+ dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2470
+ isFilterable: isFilterable,
2471
+ items: [...getShortcuts(numEnabledOptions), ...filteredItems],
2472
+ light: light,
2473
+ onOpenChanged: handleOpenChanged,
2474
+ open: open,
2475
+ opener: renderOpener(allChildren, isDisabled, uniqueDropdownId),
2476
+ openerElement: openerElement,
2477
+ selectionType: "multi",
2478
+ style: style,
2479
+ className: className,
2480
+ onSearchTextChanged: isFilterable ? handleSearchTextChanged : undefined,
2481
+ searchText: isFilterable ? searchText : "",
2482
+ labels: {
2386
2483
  clearSearch,
2387
2484
  filter,
2388
2485
  noResults,
2389
- someSelected
2390
- } = this.state.labels;
2391
- const allChildren = React__namespace.Children.toArray(children).filter(Boolean);
2392
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2393
- const filteredItems = this.getMenuItems(allChildren);
2394
- const isDisabled = numEnabledOptions === 0 || disabled;
2395
- return React__namespace.createElement(wonderBlocksCore.IDProvider, {
2396
- id: dropdownId,
2397
- scope: "multi-select-dropdown"
2398
- }, uniqueDropdownId => React__namespace.createElement(DropdownCore$1, {
2399
- id: uniqueDropdownId,
2400
- role: "listbox",
2401
- alignment: alignment,
2402
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2403
- isFilterable: isFilterable,
2404
- items: [...this.getShortcuts(numEnabledOptions), ...filteredItems],
2405
- light: light,
2406
- onOpenChanged: this.handleOpenChanged,
2407
- open: open,
2408
- opener: this.renderOpener(allChildren, isDisabled, uniqueDropdownId),
2409
- openerElement: this.state.openerElement,
2410
- selectionType: "multi",
2411
- style: style,
2412
- className: className,
2413
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2414
- searchText: isFilterable ? searchText : "",
2415
- labels: {
2416
- clearSearch,
2417
- filter,
2418
- noResults,
2419
- someResults: someSelected
2420
- },
2421
- "aria-invalid": ariaInvalid,
2422
- "aria-required": ariaRequired,
2423
- disabled: isDisabled
2424
- }));
2425
- }
2426
- }
2427
- MultiSelect.defaultProps = {
2428
- alignment: "left",
2429
- disabled: false,
2430
- error: false,
2431
- light: false,
2432
- shortcuts: false,
2433
- selectedValues: [],
2434
- showOpenerLabelAsText: true
2486
+ someResults: someSelected
2487
+ },
2488
+ "aria-invalid": ariaInvalid,
2489
+ "aria-required": ariaRequired,
2490
+ disabled: isDisabled
2491
+ }));
2435
2492
  };
2436
2493
 
2437
2494
  function updateMultipleSelection(previousSelection, value = "") {