@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/es/index.js CHANGED
@@ -6,7 +6,7 @@ import * as tokens from '@khanacademy/wonder-blocks-tokens';
6
6
  import { spacing, color, mix, fade, font, border, semanticColor } from '@khanacademy/wonder-blocks-tokens';
7
7
  import { LabelMedium, LabelSmall, LabelLarge } from '@khanacademy/wonder-blocks-typography';
8
8
  import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
9
- import { View, addStyle, IDProvider, useUniqueIdWithMock } from '@khanacademy/wonder-blocks-core';
9
+ import { View, addStyle, IDProvider, useOnMountEffect, useUniqueIdWithMock } from '@khanacademy/wonder-blocks-core';
10
10
  import { Strut } from '@khanacademy/wonder-blocks-layout';
11
11
  import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
12
12
  import checkIcon from '@phosphor-icons/core/bold/check-bold.svg';
@@ -25,13 +25,12 @@ import { TextField } from '@khanacademy/wonder-blocks-form';
25
25
  import IconButton from '@khanacademy/wonder-blocks-icon-button';
26
26
  import Pill from '@khanacademy/wonder-blocks-pill';
27
27
 
28
- const keyCodes = {
29
- tab: 9,
30
- enter: 13,
31
- escape: 27,
32
- space: 32,
33
- up: 38,
34
- down: 40
28
+ const keys = {
29
+ escape: "Escape",
30
+ tab: "Tab",
31
+ space: " ",
32
+ up: "ArrowUp",
33
+ down: "ArrowDown"
35
34
  };
36
35
  const selectDropdownStyle = {
37
36
  marginTop: spacing.xSmall_8,
@@ -495,7 +494,9 @@ class DropdownOpener extends React.Component {
495
494
  opened,
496
495
  "aria-controls": ariaControls,
497
496
  "aria-haspopup": ariaHasPopUp,
498
- id
497
+ "aria-required": ariaRequired,
498
+ id,
499
+ onBlur
499
500
  } = this.props;
500
501
  const renderedChildren = this.props.children(_extends({}, eventState, {
501
502
  text,
@@ -504,16 +505,19 @@ class DropdownOpener extends React.Component {
504
505
  const childrenProps = renderedChildren.props;
505
506
  const childrenTestId = this.getTestIdFromProps(childrenProps);
506
507
  return React.cloneElement(renderedChildren, _extends({}, clickableChildrenProps, {
508
+ "aria-invalid": this.props.error,
507
509
  disabled,
508
510
  "aria-controls": ariaControls,
509
511
  id,
510
512
  "aria-expanded": opened ? "true" : "false",
511
513
  "aria-haspopup": ariaHasPopUp,
514
+ "aria-required": ariaRequired,
512
515
  onClick: childrenProps.onClick ? e => {
513
516
  childrenProps.onClick(e);
514
517
  clickableChildrenProps.onClick(e);
515
518
  } : clickableChildrenProps.onClick,
516
- "data-testid": childrenTestId || testId
519
+ "data-testid": childrenTestId || testId,
520
+ onBlur
517
521
  }));
518
522
  }
519
523
  render() {
@@ -825,13 +829,13 @@ class DropdownCore extends React.Component {
825
829
  }
826
830
  constructor(props) {
827
831
  super(props);
828
- this.focusedIndex = void 0;
829
- this.focusedOriginalIndex = void 0;
830
- this.itemsClicked = void 0;
831
832
  this.popperElement = void 0;
832
833
  this.virtualizedListRef = void 0;
833
834
  this.handleKeyDownDebounced = void 0;
834
835
  this.textSuggestion = void 0;
836
+ this.focusedIndex = -1;
837
+ this.focusedOriginalIndex = -1;
838
+ this.itemsClicked = false;
835
839
  this.searchFieldRef = React.createRef();
836
840
  this.handleInteract = event => {
837
841
  const {
@@ -851,39 +855,39 @@ class DropdownCore extends React.Component {
851
855
  open,
852
856
  searchText
853
857
  } = this.props;
854
- const keyCode = event.which || event.keyCode;
855
- if (enableTypeAhead && getStringForKey(event.key)) {
858
+ const key = event.key;
859
+ if (enableTypeAhead && getStringForKey(key)) {
856
860
  event.stopPropagation();
857
- this.textSuggestion += event.key;
861
+ this.textSuggestion += key;
858
862
  this.handleKeyDownDebounced(this.textSuggestion);
859
863
  }
860
864
  if (!open) {
861
- if (keyCode === keyCodes.down) {
865
+ if (key === keys.down) {
862
866
  event.preventDefault();
863
867
  onOpenChanged(true);
864
868
  return;
865
869
  }
866
870
  return;
867
871
  }
868
- switch (keyCode) {
869
- case keyCodes.tab:
872
+ switch (key) {
873
+ case keys.tab:
870
874
  if (this.isSearchFieldFocused() && searchText) {
871
875
  return;
872
876
  }
873
877
  this.restoreTabOrder();
874
878
  onOpenChanged(false);
875
879
  return;
876
- case keyCodes.space:
880
+ case keys.space:
877
881
  if (this.isSearchFieldFocused()) {
878
882
  return;
879
883
  }
880
884
  event.preventDefault();
881
885
  return;
882
- case keyCodes.up:
886
+ case keys.up:
883
887
  event.preventDefault();
884
888
  this.focusPreviousItem();
885
889
  return;
886
- case keyCodes.down:
890
+ case keys.down:
887
891
  event.preventDefault();
888
892
  this.focusNextItem();
889
893
  return;
@@ -894,15 +898,15 @@ class DropdownCore extends React.Component {
894
898
  onOpenChanged,
895
899
  open
896
900
  } = this.props;
897
- const keyCode = event.which || event.keyCode;
898
- switch (keyCode) {
899
- case keyCodes.space:
901
+ const key = event.key;
902
+ switch (key) {
903
+ case keys.space:
900
904
  if (this.isSearchFieldFocused()) {
901
905
  return;
902
906
  }
903
907
  event.preventDefault();
904
908
  return;
905
- case keyCodes.escape:
909
+ case keys.escape:
906
910
  if (open) {
907
911
  event.stopPropagation();
908
912
  this.restoreTabOrder();
@@ -1075,18 +1079,37 @@ class DropdownCore extends React.Component {
1075
1079
  }
1076
1080
  focusCurrentItem(onFocus) {
1077
1081
  const focusedItemRef = this.state.itemRefs[this.focusedIndex];
1078
- if (focusedItemRef) {
1079
- if (this.virtualizedListRef.current) {
1080
- this.virtualizedListRef.current.scrollToItem(focusedItemRef.originalIndex);
1082
+ if (!focusedItemRef) {
1083
+ return;
1084
+ }
1085
+ const {
1086
+ current: virtualizedList
1087
+ } = this.virtualizedListRef;
1088
+ if (virtualizedList) {
1089
+ virtualizedList.scrollToItem(focusedItemRef.originalIndex);
1090
+ }
1091
+ const focusNode = () => {
1092
+ if (!this.props.open) {
1093
+ return;
1094
+ }
1095
+ const currentFocusedItemRef = this.state.itemRefs[this.focusedIndex];
1096
+ const node = ReactDOM.findDOMNode(currentFocusedItemRef.ref.current);
1097
+ if (!node && this.shouldVirtualizeList()) {
1098
+ this.props.schedule.animationFrame(focusNode);
1099
+ return;
1081
1100
  }
1082
- const node = ReactDOM.findDOMNode(focusedItemRef.ref.current);
1083
1101
  if (node) {
1084
1102
  node.focus();
1085
- this.focusedOriginalIndex = focusedItemRef.originalIndex;
1103
+ this.focusedOriginalIndex = currentFocusedItemRef.originalIndex;
1086
1104
  if (onFocus) {
1087
1105
  onFocus(node);
1088
1106
  }
1089
1107
  }
1108
+ };
1109
+ if (this.shouldVirtualizeList()) {
1110
+ this.props.schedule.animationFrame(focusNode);
1111
+ } else {
1112
+ focusNode();
1090
1113
  }
1091
1114
  }
1092
1115
  focusSearchField() {
@@ -1106,7 +1129,7 @@ class DropdownCore extends React.Component {
1106
1129
  return this.focusSearchField();
1107
1130
  }
1108
1131
  this.focusedIndex = this.state.itemRefs.length - 1;
1109
- } else {
1132
+ } else if (!this.isSearchFieldFocused()) {
1110
1133
  this.focusedIndex -= 1;
1111
1134
  }
1112
1135
  this.scheduleToFocusCurrentItem();
@@ -1117,7 +1140,7 @@ class DropdownCore extends React.Component {
1117
1140
  return this.focusSearchField();
1118
1141
  }
1119
1142
  this.focusedIndex = 0;
1120
- } else {
1143
+ } else if (!this.isSearchFieldFocused()) {
1121
1144
  this.focusedIndex += 1;
1122
1145
  }
1123
1146
  this.scheduleToFocusCurrentItem();
@@ -1199,7 +1222,7 @@ class DropdownCore extends React.Component {
1199
1222
  const focusIndex = focusCounter - 1;
1200
1223
  return _extends({}, item, {
1201
1224
  role: populatedProps.role || itemRole,
1202
- ref: item.focusable ? this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null : null,
1225
+ ref: item.focusable && this.state.itemRefs[focusIndex] ? this.state.itemRefs[focusIndex].ref : null,
1203
1226
  onClick: () => {
1204
1227
  this.handleItemClick(focusIndex, item);
1205
1228
  }
@@ -1660,7 +1683,7 @@ const styles$5 = StyleSheet.create({
1660
1683
  }
1661
1684
  });
1662
1685
 
1663
- const _excluded$2 = ["children", "disabled", "error", "id", "isPlaceholder", "light", "open", "testId", "onOpenChanged"];
1686
+ const _excluded$2 = ["children", "disabled", "error", "id", "isPlaceholder", "light", "open", "testId", "aria-required", "onBlur", "onOpenChanged"];
1664
1687
  const StyledButton = addStyle("button");
1665
1688
  class SelectOpener extends React.Component {
1666
1689
  constructor(props) {
@@ -1703,7 +1726,9 @@ class SelectOpener extends React.Component {
1703
1726
  isPlaceholder,
1704
1727
  light,
1705
1728
  open,
1706
- testId
1729
+ testId,
1730
+ "aria-required": ariaRequired,
1731
+ onBlur
1707
1732
  } = _this$props,
1708
1733
  sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
1709
1734
  const stateStyles = _generateStyles(light, isPlaceholder, error);
@@ -1712,6 +1737,8 @@ class SelectOpener extends React.Component {
1712
1737
  return React.createElement(StyledButton, _extends({}, sharedProps, {
1713
1738
  "aria-disabled": disabled,
1714
1739
  "aria-expanded": open ? "true" : "false",
1740
+ "aria-invalid": error,
1741
+ "aria-required": ariaRequired,
1715
1742
  "aria-haspopup": "listbox",
1716
1743
  "data-testid": testId,
1717
1744
  id: id,
@@ -1719,7 +1746,8 @@ class SelectOpener extends React.Component {
1719
1746
  type: "button",
1720
1747
  onClick: !disabled ? this.handleClick : undefined,
1721
1748
  onKeyDown: !disabled ? this.handleKeyDown : undefined,
1722
- onKeyUp: !disabled ? this.handleKeyUp : undefined
1749
+ onKeyUp: !disabled ? this.handleKeyUp : undefined,
1750
+ onBlur: onBlur
1723
1751
  }), React.createElement(LabelMedium, {
1724
1752
  style: styles$4.text
1725
1753
  }, children || "\u00A0"), React.createElement(PhosphorIcon, {
@@ -1868,117 +1896,206 @@ const _generateStyles = (light, placeholder, error) => {
1868
1896
  return stateStyles[styleKey];
1869
1897
  };
1870
1898
 
1871
- 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"];
1872
- class SingleSelect extends React.Component {
1873
- constructor(props) {
1874
- super(props);
1875
- this.selectedIndex = void 0;
1876
- this.handleOpenChanged = opened => {
1877
- this.setState({
1878
- open: opened,
1879
- searchText: ""
1880
- });
1881
- if (this.props.onToggle) {
1882
- this.props.onToggle(opened);
1899
+ const defaultErrorMessage = "This field is required.";
1900
+ function hasValue(value) {
1901
+ return value ? value.length > 0 : false;
1902
+ }
1903
+ function useSelectValidation({
1904
+ value,
1905
+ disabled = false,
1906
+ validate,
1907
+ onValidate,
1908
+ required,
1909
+ open
1910
+ }) {
1911
+ const [errorMessage, setErrorMessage] = React.useState(() => validate && hasValue(value) && !disabled && validate(value) || null);
1912
+ const handleValidation = React.useCallback(newValue => {
1913
+ if (disabled) {
1914
+ return;
1915
+ }
1916
+ if (validate) {
1917
+ const error = newValue !== undefined && validate(newValue) || null;
1918
+ setErrorMessage(error);
1919
+ if (onValidate) {
1920
+ onValidate(error);
1883
1921
  }
1884
- };
1885
- this.handleToggle = selectedValue => {
1886
- if (selectedValue !== this.props.selectedValue) {
1887
- this.props.onChange(selectedValue);
1922
+ if (error) {
1923
+ return;
1888
1924
  }
1889
- if (this.state.open && this.state.openerElement) {
1890
- this.state.openerElement.focus();
1925
+ }
1926
+ if (required) {
1927
+ const requiredString = typeof required === "string" ? required : defaultErrorMessage;
1928
+ const error = hasValue(newValue) ? null : requiredString;
1929
+ setErrorMessage(error);
1930
+ if (onValidate) {
1931
+ onValidate(error);
1891
1932
  }
1892
- this.setState({
1893
- open: false
1894
- });
1895
- if (this.props.onToggle) {
1896
- this.props.onToggle(false);
1933
+ }
1934
+ }, [disabled, validate, setErrorMessage, onValidate, required]);
1935
+ useOnMountEffect(() => {
1936
+ if (hasValue(value)) {
1937
+ handleValidation(value);
1938
+ }
1939
+ });
1940
+ function onOpenerBlurValidation() {
1941
+ if (!open && required && !hasValue(value)) {
1942
+ handleValidation(value);
1943
+ }
1944
+ }
1945
+ const onDropdownClosedValidation = () => {
1946
+ if (required && !hasValue(value)) {
1947
+ handleValidation(value);
1948
+ }
1949
+ };
1950
+ const onSelectionValidation = newValue => {
1951
+ handleValidation(newValue);
1952
+ };
1953
+ const onSelectedValuesChangeValidation = () => {
1954
+ setErrorMessage(null);
1955
+ if (onValidate) {
1956
+ onValidate(null);
1957
+ }
1958
+ };
1959
+ return {
1960
+ errorMessage,
1961
+ onOpenerBlurValidation,
1962
+ onDropdownClosedValidation,
1963
+ onSelectionValidation,
1964
+ onSelectedValuesChangeValidation
1965
+ };
1966
+ }
1967
+
1968
+ 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"];
1969
+ const SingleSelect = props => {
1970
+ const selectedIndex = React.useRef(0);
1971
+ const {
1972
+ children,
1973
+ error = false,
1974
+ id,
1975
+ opener,
1976
+ light = false,
1977
+ placeholder,
1978
+ selectedValue,
1979
+ testId,
1980
+ alignment = "left",
1981
+ autoFocus = true,
1982
+ dropdownStyle,
1983
+ enableTypeAhead = true,
1984
+ isFilterable,
1985
+ labels = {
1986
+ clearSearch: defaultLabels.clearSearch,
1987
+ filter: defaultLabels.filter,
1988
+ noResults: defaultLabels.noResults,
1989
+ someResults: defaultLabels.someSelected
1990
+ },
1991
+ onChange,
1992
+ onToggle,
1993
+ opened,
1994
+ style,
1995
+ className,
1996
+ "aria-invalid": ariaInvalid,
1997
+ "aria-required": ariaRequired,
1998
+ disabled = false,
1999
+ dropdownId,
2000
+ validate,
2001
+ onValidate,
2002
+ required,
2003
+ showOpenerLabelAsText = true
2004
+ } = props,
2005
+ sharedProps = _objectWithoutPropertiesLoose(props, _excluded$1);
2006
+ const [open, setOpen] = React.useState(false);
2007
+ const [searchText, setSearchText] = React.useState("");
2008
+ const [openerElement, setOpenerElement] = React.useState();
2009
+ const {
2010
+ errorMessage,
2011
+ onOpenerBlurValidation,
2012
+ onDropdownClosedValidation,
2013
+ onSelectionValidation
2014
+ } = useSelectValidation({
2015
+ value: selectedValue,
2016
+ disabled,
2017
+ validate,
2018
+ onValidate,
2019
+ required,
2020
+ open
2021
+ });
2022
+ const hasError = error || !!errorMessage;
2023
+ React.useEffect(() => {
2024
+ if (disabled) {
2025
+ setOpen(false);
2026
+ } else if (typeof opened === "boolean") {
2027
+ setOpen(opened);
2028
+ }
2029
+ }, [disabled, opened]);
2030
+ const handleOpenChanged = opened => {
2031
+ setOpen(opened);
2032
+ setSearchText("");
2033
+ if (onToggle) {
2034
+ onToggle(opened);
2035
+ }
2036
+ if (!opened) {
2037
+ onDropdownClosedValidation();
2038
+ }
2039
+ };
2040
+ const handleToggle = newSelectedValue => {
2041
+ if (newSelectedValue !== selectedValue) {
2042
+ onChange(newSelectedValue);
2043
+ }
2044
+ if (open && openerElement) {
2045
+ openerElement.focus();
2046
+ }
2047
+ setOpen(false);
2048
+ if (onToggle) {
2049
+ onToggle(false);
2050
+ }
2051
+ onSelectionValidation(newSelectedValue);
2052
+ };
2053
+ const mapOptionItemsToDropdownItems = children => {
2054
+ let indexCounter = 0;
2055
+ selectedIndex.current = 0;
2056
+ return children.map(option => {
2057
+ const {
2058
+ disabled,
2059
+ value
2060
+ } = option.props;
2061
+ const selected = selectedValue === value;
2062
+ if (selected) {
2063
+ selectedIndex.current = indexCounter;
1897
2064
  }
1898
- };
1899
- this.mapOptionItemsToDropdownItems = children => {
1900
- let indexCounter = 0;
1901
- this.selectedIndex = 0;
1902
- return children.map(option => {
1903
- const {
1904
- selectedValue
1905
- } = this.props;
1906
- const {
1907
- disabled,
1908
- value
1909
- } = option.props;
1910
- const selected = selectedValue === value;
1911
- if (selected) {
1912
- this.selectedIndex = indexCounter;
1913
- }
1914
- if (!disabled) {
1915
- indexCounter += 1;
2065
+ if (!disabled) {
2066
+ indexCounter += 1;
2067
+ }
2068
+ return {
2069
+ component: option,
2070
+ focusable: !disabled,
2071
+ populatedProps: {
2072
+ onToggle: handleToggle,
2073
+ selected: selected,
2074
+ variant: "check"
1916
2075
  }
1917
- return {
1918
- component: option,
1919
- focusable: !disabled,
1920
- populatedProps: {
1921
- onToggle: this.handleToggle,
1922
- selected: selected,
1923
- variant: "check"
1924
- }
1925
- };
1926
- });
1927
- };
1928
- this.handleSearchTextChanged = searchText => {
1929
- this.setState({
1930
- searchText
1931
- });
1932
- };
1933
- this.handleOpenerRef = node => {
1934
- const openerElement = ReactDOM.findDOMNode(node);
1935
- this.setState({
1936
- openerElement
1937
- });
1938
- };
1939
- this.handleClick = e => {
1940
- this.handleOpenChanged(!this.state.open);
1941
- };
1942
- this.selectedIndex = 0;
1943
- this.state = {
1944
- open: false,
1945
- searchText: ""
1946
- };
1947
- }
1948
- static getDerivedStateFromProps(props, state) {
1949
- return {
1950
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
1951
- };
1952
- }
1953
- filterChildren(children) {
1954
- const {
1955
- searchText
1956
- } = this.state;
2076
+ };
2077
+ });
2078
+ };
2079
+ const filterChildren = children => {
1957
2080
  const lowercasedSearchText = searchText.toLowerCase();
1958
2081
  return children.filter(({
1959
2082
  props
1960
2083
  }) => !searchText || getLabel(props).toLowerCase().indexOf(lowercasedSearchText) > -1);
1961
- }
1962
- getMenuItems(children) {
1963
- const {
1964
- isFilterable
1965
- } = this.props;
1966
- return this.mapOptionItemsToDropdownItems(isFilterable ? this.filterChildren(children) : children);
1967
- }
1968
- renderOpener(isDisabled, dropdownId) {
1969
- const _this$props = this.props,
1970
- {
1971
- children,
1972
- error,
1973
- id,
1974
- light,
1975
- opener,
1976
- placeholder,
1977
- selectedValue,
1978
- testId,
1979
- showOpenerLabelAsText
1980
- } = _this$props,
1981
- sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$1);
2084
+ };
2085
+ const getMenuItems = children => {
2086
+ return mapOptionItemsToDropdownItems(isFilterable ? filterChildren(children) : children);
2087
+ };
2088
+ const handleSearchTextChanged = searchText => {
2089
+ setSearchText(searchText);
2090
+ };
2091
+ const handleOpenerRef = node => {
2092
+ const openerElement = ReactDOM.findDOMNode(node);
2093
+ setOpenerElement(openerElement);
2094
+ };
2095
+ const handleClick = e => {
2096
+ handleOpenChanged(!open);
2097
+ };
2098
+ const renderOpener = (isDisabled, dropdownId) => {
1982
2099
  const items = React.Children.toArray(children);
1983
2100
  const selectedItem = items.find(option => option.props.value === selectedValue);
1984
2101
  const menuText = selectedItem ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) : placeholder;
@@ -1990,202 +2107,160 @@ class SingleSelect extends React.Component {
1990
2107
  id: uniqueOpenerId,
1991
2108
  "aria-controls": dropdownId,
1992
2109
  "aria-haspopup": "listbox",
1993
- onClick: this.handleClick,
2110
+ onClick: handleClick,
1994
2111
  disabled: isDisabled,
1995
- ref: this.handleOpenerRef,
2112
+ ref: handleOpenerRef,
1996
2113
  text: menuText,
1997
- opened: this.state.open
2114
+ opened: open,
2115
+ error: hasError,
2116
+ onBlur: onOpenerBlurValidation
1998
2117
  }, opener) : React.createElement(SelectOpener, _extends({}, sharedProps, {
1999
2118
  "aria-controls": dropdownId,
2000
2119
  disabled: isDisabled,
2001
2120
  id: uniqueOpenerId,
2002
- error: error,
2121
+ error: hasError,
2003
2122
  isPlaceholder: !selectedItem,
2004
2123
  light: light,
2005
- onOpenChanged: this.handleOpenChanged,
2006
- open: this.state.open,
2007
- ref: this.handleOpenerRef,
2008
- testId: testId
2124
+ onOpenChanged: handleOpenChanged,
2125
+ open: open,
2126
+ ref: handleOpenerRef,
2127
+ testId: testId,
2128
+ onBlur: onOpenerBlurValidation
2009
2129
  }), menuText);
2010
2130
  });
2011
2131
  return dropdownOpener;
2012
- }
2013
- render() {
2014
- const {
2015
- alignment,
2016
- autoFocus,
2017
- children,
2018
- className,
2132
+ };
2133
+ const allChildren = React.Children.toArray(children).filter(Boolean);
2134
+ const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2135
+ const items = getMenuItems(allChildren);
2136
+ const isDisabled = numEnabledOptions === 0 || disabled;
2137
+ return React.createElement(IDProvider, {
2138
+ id: dropdownId,
2139
+ scope: "single-select-dropdown"
2140
+ }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2141
+ id: uniqueDropdownId,
2142
+ role: "listbox",
2143
+ selectionType: "single",
2144
+ alignment: alignment,
2145
+ autoFocus: autoFocus,
2146
+ enableTypeAhead: enableTypeAhead,
2147
+ dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2148
+ initialFocusedIndex: selectedIndex.current,
2149
+ items: items,
2150
+ light: light,
2151
+ onOpenChanged: handleOpenChanged,
2152
+ open: open,
2153
+ opener: renderOpener(isDisabled, uniqueDropdownId),
2154
+ openerElement: openerElement,
2155
+ style: style,
2156
+ className: className,
2157
+ isFilterable: isFilterable,
2158
+ onSearchTextChanged: isFilterable ? handleSearchTextChanged : undefined,
2159
+ searchText: isFilterable ? searchText : "",
2160
+ labels: labels,
2161
+ "aria-invalid": ariaInvalid,
2162
+ "aria-required": ariaRequired,
2163
+ disabled: isDisabled
2164
+ }));
2165
+ };
2166
+
2167
+ 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"];
2168
+ const MultiSelect = props => {
2169
+ const {
2170
+ id,
2171
+ light = false,
2172
+ opener,
2173
+ testId,
2174
+ alignment = "left",
2019
2175
  dropdownStyle,
2020
- enableTypeAhead,
2176
+ implicitAllEnabled,
2021
2177
  isFilterable,
2022
- labels,
2023
- light,
2178
+ labels: propLabels,
2179
+ onChange,
2180
+ onToggle,
2181
+ opened,
2182
+ selectedValues = [],
2183
+ shortcuts = false,
2024
2184
  style,
2185
+ className,
2025
2186
  "aria-invalid": ariaInvalid,
2026
2187
  "aria-required": ariaRequired,
2027
- disabled,
2028
- dropdownId
2029
- } = this.props;
2030
- const {
2031
- searchText
2032
- } = this.state;
2033
- const allChildren = React.Children.toArray(children).filter(Boolean);
2034
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2035
- const items = this.getMenuItems(allChildren);
2036
- const isDisabled = numEnabledOptions === 0 || disabled;
2037
- return React.createElement(IDProvider, {
2038
- id: dropdownId,
2039
- scope: "single-select-dropdown"
2040
- }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2041
- id: uniqueDropdownId,
2042
- role: "listbox",
2043
- selectionType: "single",
2044
- alignment: alignment,
2045
- autoFocus: autoFocus,
2046
- enableTypeAhead: enableTypeAhead,
2047
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2048
- initialFocusedIndex: this.selectedIndex,
2049
- items: items,
2050
- light: light,
2051
- onOpenChanged: this.handleOpenChanged,
2052
- open: this.state.open,
2053
- opener: this.renderOpener(isDisabled, uniqueDropdownId),
2054
- openerElement: this.state.openerElement,
2055
- style: style,
2056
- className: className,
2057
- isFilterable: isFilterable,
2058
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2059
- searchText: isFilterable ? searchText : "",
2060
- labels: labels,
2061
- "aria-invalid": ariaInvalid,
2062
- "aria-required": ariaRequired,
2063
- disabled: isDisabled
2064
- }));
2065
- }
2066
- }
2067
- SingleSelect.defaultProps = {
2068
- alignment: "left",
2069
- autoFocus: true,
2070
- disabled: false,
2071
- enableTypeAhead: true,
2072
- error: false,
2073
- light: false,
2074
- labels: {
2075
- clearSearch: defaultLabels.clearSearch,
2076
- filter: defaultLabels.filter,
2077
- noResults: defaultLabels.noResults,
2078
- someResults: defaultLabels.someSelected
2079
- },
2080
- showOpenerLabelAsText: true
2081
- };
2082
-
2083
- const _excluded = ["id", "light", "opener", "testId", "alignment", "dropdownStyle", "implicitAllEnabled", "isFilterable", "labels", "onChange", "onToggle", "opened", "selectedValues", "shortcuts", "style", "className", "aria-invalid", "aria-required", "showOpenerLabelAsText"];
2084
- class MultiSelect extends React.Component {
2085
- constructor(props) {
2086
- super(props);
2087
- this.labels = void 0;
2088
- this.handleOpenChanged = opened => {
2089
- this.setState({
2090
- open: opened,
2091
- searchText: "",
2092
- lastSelectedValues: this.props.selectedValues
2093
- });
2094
- if (this.props.onToggle) {
2095
- this.props.onToggle(opened);
2096
- }
2097
- };
2098
- this.handleToggle = selectedValue => {
2099
- const {
2100
- onChange,
2101
- selectedValues
2102
- } = this.props;
2103
- if (selectedValues.includes(selectedValue)) {
2104
- const index = selectedValues.indexOf(selectedValue);
2105
- const updatedSelection = [...selectedValues.slice(0, index), ...selectedValues.slice(index + 1)];
2106
- onChange(updatedSelection);
2188
+ disabled = false,
2189
+ error = false,
2190
+ children,
2191
+ dropdownId,
2192
+ showOpenerLabelAsText = true,
2193
+ validate,
2194
+ onValidate,
2195
+ required
2196
+ } = props,
2197
+ sharedProps = _objectWithoutPropertiesLoose(props, _excluded);
2198
+ const labels = _extends({}, defaultLabels, propLabels);
2199
+ const [open, setOpen] = React.useState(false);
2200
+ const [searchText, setSearchText] = React.useState("");
2201
+ const [lastSelectedValues, setLastSelectedValues] = React.useState([]);
2202
+ const [openerElement, setOpenerElement] = React.useState();
2203
+ const {
2204
+ errorMessage,
2205
+ onOpenerBlurValidation,
2206
+ onDropdownClosedValidation,
2207
+ onSelectionValidation,
2208
+ onSelectedValuesChangeValidation
2209
+ } = useSelectValidation({
2210
+ value: selectedValues,
2211
+ disabled,
2212
+ validate,
2213
+ onValidate,
2214
+ required,
2215
+ open
2216
+ });
2217
+ const hasError = error || !!errorMessage;
2218
+ React.useEffect(() => {
2219
+ if (disabled) {
2220
+ setOpen(false);
2221
+ } else if (typeof opened === "boolean") {
2222
+ setOpen(opened);
2223
+ }
2224
+ }, [disabled, opened]);
2225
+ const handleOpenChanged = opened => {
2226
+ setOpen(opened);
2227
+ setSearchText("");
2228
+ setLastSelectedValues(selectedValues);
2229
+ if (onToggle) {
2230
+ onToggle(opened);
2231
+ }
2232
+ if (!opened) {
2233
+ if (lastSelectedValues !== selectedValues) {
2234
+ onSelectionValidation(selectedValues);
2107
2235
  } else {
2108
- onChange([...selectedValues, selectedValue]);
2236
+ onDropdownClosedValidation();
2109
2237
  }
2110
- };
2111
- this.handleSelectAll = () => {
2112
- const {
2113
- children,
2114
- onChange
2115
- } = this.props;
2116
- const allChildren = React.Children.toArray(children);
2117
- const selected = allChildren.filter(option => !!option && !option.props.disabled).map(option => option.props.value);
2118
- onChange(selected);
2119
- };
2120
- this.handleSelectNone = () => {
2121
- const {
2122
- onChange
2123
- } = this.props;
2124
- onChange([]);
2125
- };
2126
- this.mapOptionItemToDropdownItem = option => {
2127
- const {
2128
- selectedValues
2129
- } = this.props;
2130
- const {
2131
- disabled,
2132
- value
2133
- } = option.props;
2134
- return {
2135
- component: option,
2136
- focusable: !disabled,
2137
- populatedProps: {
2138
- onToggle: this.handleToggle,
2139
- selected: selectedValues.includes(value),
2140
- variant: "checkbox"
2141
- }
2142
- };
2143
- };
2144
- this.handleOpenerRef = node => {
2145
- const openerElement = ReactDOM.findDOMNode(node);
2146
- this.setState({
2147
- openerElement
2148
- });
2149
- };
2150
- this.handleSearchTextChanged = searchText => {
2151
- this.setState({
2152
- searchText
2153
- });
2154
- };
2155
- this.handleClick = e => {
2156
- this.handleOpenChanged(!this.state.open);
2157
- };
2158
- this.state = {
2159
- open: false,
2160
- searchText: "",
2161
- lastSelectedValues: [],
2162
- labels: _extends({}, defaultLabels, props.labels)
2163
- };
2164
- this.labels = _extends({}, defaultLabels, props.labels);
2165
- }
2166
- static getDerivedStateFromProps(props, state) {
2167
- return {
2168
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
2169
- };
2170
- }
2171
- componentDidUpdate(prevProps) {
2172
- if (this.props.labels !== prevProps.labels) {
2173
- this.setState({
2174
- labels: _extends({}, this.state.labels, this.props.labels)
2175
- });
2176
2238
  }
2177
- }
2178
- getMenuText(children) {
2179
- const {
2180
- implicitAllEnabled,
2181
- selectedValues,
2182
- showOpenerLabelAsText
2183
- } = this.props;
2239
+ };
2240
+ const handleToggle = selectedValue => {
2241
+ if (selectedValues.includes(selectedValue)) {
2242
+ const index = selectedValues.indexOf(selectedValue);
2243
+ const updatedSelection = [...selectedValues.slice(0, index), ...selectedValues.slice(index + 1)];
2244
+ onChange(updatedSelection);
2245
+ } else {
2246
+ onChange([...selectedValues, selectedValue]);
2247
+ }
2248
+ onSelectedValuesChangeValidation();
2249
+ };
2250
+ const handleSelectAll = () => {
2251
+ const allChildren = React.Children.toArray(children);
2252
+ const selected = allChildren.filter(option => !!option && !option.props.disabled).map(option => option.props.value);
2253
+ onChange(selected);
2254
+ };
2255
+ const handleSelectNone = () => {
2256
+ onChange([]);
2257
+ };
2258
+ const getMenuText = children => {
2184
2259
  const {
2185
2260
  noneSelected,
2186
2261
  someSelected,
2187
2262
  allSelected
2188
- } = this.state.labels;
2263
+ } = labels;
2189
2264
  const numSelectedAll = children.filter(option => !option.props.disabled).length;
2190
2265
  const noSelectionText = implicitAllEnabled ? allSelected : noneSelected;
2191
2266
  switch (selectedValues.length) {
@@ -2207,24 +2282,20 @@ class MultiSelect extends React.Component {
2207
2282
  default:
2208
2283
  return someSelected(selectedValues.length);
2209
2284
  }
2210
- }
2211
- getShortcuts(numOptions) {
2212
- const {
2213
- selectedValues,
2214
- shortcuts
2215
- } = this.props;
2285
+ };
2286
+ const getShortcuts = numOptions => {
2216
2287
  const {
2217
2288
  selectAllLabel,
2218
2289
  selectNoneLabel
2219
- } = this.state.labels;
2220
- if (shortcuts && !this.state.searchText) {
2290
+ } = labels;
2291
+ if (shortcuts && !searchText) {
2221
2292
  const selectAllDisabled = numOptions === selectedValues.length;
2222
2293
  const selectAll = {
2223
2294
  component: React.createElement(ActionItem, {
2224
2295
  disabled: selectAllDisabled,
2225
2296
  label: selectAllLabel(numOptions),
2226
2297
  indent: true,
2227
- onClick: this.handleSelectAll
2298
+ onClick: handleSelectAll
2228
2299
  }),
2229
2300
  focusable: !selectAllDisabled,
2230
2301
  populatedProps: {}
@@ -2235,7 +2306,7 @@ class MultiSelect extends React.Component {
2235
2306
  disabled: selectNoneDisabled,
2236
2307
  label: selectNoneLabel,
2237
2308
  indent: true,
2238
- onClick: this.handleSelectNone
2309
+ onClick: handleSelectNone
2239
2310
  }),
2240
2311
  focusable: !selectNoneDisabled,
2241
2312
  populatedProps: {}
@@ -2251,18 +2322,11 @@ class MultiSelect extends React.Component {
2251
2322
  } else {
2252
2323
  return [];
2253
2324
  }
2254
- }
2255
- getMenuItems(children) {
2256
- const {
2257
- isFilterable
2258
- } = this.props;
2325
+ };
2326
+ const getMenuItems = children => {
2259
2327
  if (!isFilterable) {
2260
- return children.map(this.mapOptionItemToDropdownItem);
2328
+ return children.map(mapOptionItemToDropdownItem);
2261
2329
  }
2262
- const {
2263
- searchText,
2264
- lastSelectedValues
2265
- } = this.state;
2266
2330
  const lowercasedSearchText = searchText.toLowerCase();
2267
2331
  const filteredChildren = children.filter(({
2268
2332
  props
@@ -2276,7 +2340,7 @@ class MultiSelect extends React.Component {
2276
2340
  restOfTheChildren.push(child);
2277
2341
  }
2278
2342
  }
2279
- const lastSelectedItems = lastSelectedChildren.map(this.mapOptionItemToDropdownItem);
2343
+ const lastSelectedItems = lastSelectedChildren.map(mapOptionItemToDropdownItem);
2280
2344
  if (lastSelectedChildren.length && restOfTheChildren.length) {
2281
2345
  lastSelectedItems.push({
2282
2346
  component: React.createElement(SeparatorItem, {
@@ -2286,116 +2350,109 @@ class MultiSelect extends React.Component {
2286
2350
  populatedProps: {}
2287
2351
  });
2288
2352
  }
2289
- return [...lastSelectedItems, ...restOfTheChildren.map(this.mapOptionItemToDropdownItem)];
2290
- }
2291
- renderOpener(allChildren, isDisabled, dropdownId) {
2292
- const _this$props = this.props,
2293
- {
2294
- id,
2295
- light,
2296
- opener,
2297
- testId
2298
- } = _this$props,
2299
- sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded);
2353
+ return [...lastSelectedItems, ...restOfTheChildren.map(mapOptionItemToDropdownItem)];
2354
+ };
2355
+ const mapOptionItemToDropdownItem = option => {
2356
+ const {
2357
+ disabled,
2358
+ value
2359
+ } = option.props;
2360
+ return {
2361
+ component: option,
2362
+ focusable: !disabled,
2363
+ populatedProps: {
2364
+ onToggle: handleToggle,
2365
+ selected: selectedValues.includes(value),
2366
+ variant: "checkbox"
2367
+ }
2368
+ };
2369
+ };
2370
+ const handleOpenerRef = node => {
2371
+ const openerElement = ReactDOM.findDOMNode(node);
2372
+ setOpenerElement(openerElement);
2373
+ };
2374
+ const handleSearchTextChanged = searchText => {
2375
+ setSearchText(searchText);
2376
+ };
2377
+ const handleClick = e => {
2378
+ handleOpenChanged(!open);
2379
+ };
2380
+ const renderOpener = (allChildren, isDisabled, dropdownId) => {
2300
2381
  const {
2301
2382
  noneSelected
2302
- } = this.state.labels;
2303
- const menuText = this.getMenuText(allChildren);
2383
+ } = labels;
2384
+ const menuText = getMenuText(allChildren);
2304
2385
  const dropdownOpener = React.createElement(IDProvider, {
2305
2386
  id: id,
2306
2387
  scope: "multi-select-opener"
2307
2388
  }, uniqueOpenerId => {
2308
2389
  return opener ? React.createElement(DropdownOpener, {
2309
2390
  id: uniqueOpenerId,
2391
+ error: hasError,
2310
2392
  "aria-controls": dropdownId,
2311
2393
  "aria-haspopup": "listbox",
2312
- onClick: this.handleClick,
2394
+ onClick: handleClick,
2395
+ onBlur: onOpenerBlurValidation,
2313
2396
  disabled: isDisabled,
2314
- ref: this.handleOpenerRef,
2397
+ ref: handleOpenerRef,
2315
2398
  text: menuText,
2316
- opened: this.state.open
2399
+ opened: open
2317
2400
  }, opener) : React.createElement(SelectOpener, _extends({}, sharedProps, {
2401
+ error: hasError,
2318
2402
  disabled: isDisabled,
2319
2403
  id: uniqueOpenerId,
2320
2404
  "aria-controls": dropdownId,
2321
2405
  isPlaceholder: menuText === noneSelected,
2322
2406
  light: light,
2323
- onOpenChanged: this.handleOpenChanged,
2324
- open: this.state.open,
2325
- ref: this.handleOpenerRef,
2407
+ onOpenChanged: handleOpenChanged,
2408
+ onBlur: onOpenerBlurValidation,
2409
+ open: open,
2410
+ ref: handleOpenerRef,
2326
2411
  testId: testId
2327
2412
  }), menuText);
2328
2413
  });
2329
2414
  return dropdownOpener;
2330
- }
2331
- render() {
2332
- const {
2333
- alignment,
2334
- light,
2335
- style,
2336
- className,
2337
- dropdownStyle,
2338
- children,
2339
- isFilterable,
2340
- "aria-invalid": ariaInvalid,
2341
- "aria-required": ariaRequired,
2342
- disabled,
2343
- dropdownId
2344
- } = this.props;
2345
- const {
2346
- open,
2347
- searchText
2348
- } = this.state;
2349
- const {
2415
+ };
2416
+ const {
2417
+ clearSearch,
2418
+ filter,
2419
+ noResults,
2420
+ someSelected
2421
+ } = labels;
2422
+ const allChildren = React.Children.toArray(children).filter(Boolean);
2423
+ const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2424
+ const filteredItems = getMenuItems(allChildren);
2425
+ const isDisabled = numEnabledOptions === 0 || disabled;
2426
+ return React.createElement(IDProvider, {
2427
+ id: dropdownId,
2428
+ scope: "multi-select-dropdown"
2429
+ }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2430
+ id: uniqueDropdownId,
2431
+ role: "listbox",
2432
+ alignment: alignment,
2433
+ dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2434
+ isFilterable: isFilterable,
2435
+ items: [...getShortcuts(numEnabledOptions), ...filteredItems],
2436
+ light: light,
2437
+ onOpenChanged: handleOpenChanged,
2438
+ open: open,
2439
+ opener: renderOpener(allChildren, isDisabled, uniqueDropdownId),
2440
+ openerElement: openerElement,
2441
+ selectionType: "multi",
2442
+ style: style,
2443
+ className: className,
2444
+ onSearchTextChanged: isFilterable ? handleSearchTextChanged : undefined,
2445
+ searchText: isFilterable ? searchText : "",
2446
+ labels: {
2350
2447
  clearSearch,
2351
2448
  filter,
2352
2449
  noResults,
2353
- someSelected
2354
- } = this.state.labels;
2355
- const allChildren = React.Children.toArray(children).filter(Boolean);
2356
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2357
- const filteredItems = this.getMenuItems(allChildren);
2358
- const isDisabled = numEnabledOptions === 0 || disabled;
2359
- return React.createElement(IDProvider, {
2360
- id: dropdownId,
2361
- scope: "multi-select-dropdown"
2362
- }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2363
- id: uniqueDropdownId,
2364
- role: "listbox",
2365
- alignment: alignment,
2366
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2367
- isFilterable: isFilterable,
2368
- items: [...this.getShortcuts(numEnabledOptions), ...filteredItems],
2369
- light: light,
2370
- onOpenChanged: this.handleOpenChanged,
2371
- open: open,
2372
- opener: this.renderOpener(allChildren, isDisabled, uniqueDropdownId),
2373
- openerElement: this.state.openerElement,
2374
- selectionType: "multi",
2375
- style: style,
2376
- className: className,
2377
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2378
- searchText: isFilterable ? searchText : "",
2379
- labels: {
2380
- clearSearch,
2381
- filter,
2382
- noResults,
2383
- someResults: someSelected
2384
- },
2385
- "aria-invalid": ariaInvalid,
2386
- "aria-required": ariaRequired,
2387
- disabled: isDisabled
2388
- }));
2389
- }
2390
- }
2391
- MultiSelect.defaultProps = {
2392
- alignment: "left",
2393
- disabled: false,
2394
- error: false,
2395
- light: false,
2396
- shortcuts: false,
2397
- selectedValues: [],
2398
- showOpenerLabelAsText: true
2450
+ someResults: someSelected
2451
+ },
2452
+ "aria-invalid": ariaInvalid,
2453
+ "aria-required": ariaRequired,
2454
+ disabled: isDisabled
2455
+ }));
2399
2456
  };
2400
2457
 
2401
2458
  function updateMultipleSelection(previousSelection, value = "") {