@khanacademy/wonder-blocks-dropdown 6.0.0 → 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() {
@@ -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();
@@ -1679,7 +1683,7 @@ const styles$5 = StyleSheet.create({
1679
1683
  }
1680
1684
  });
1681
1685
 
1682
- 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"];
1683
1687
  const StyledButton = addStyle("button");
1684
1688
  class SelectOpener extends React.Component {
1685
1689
  constructor(props) {
@@ -1722,7 +1726,9 @@ class SelectOpener extends React.Component {
1722
1726
  isPlaceholder,
1723
1727
  light,
1724
1728
  open,
1725
- testId
1729
+ testId,
1730
+ "aria-required": ariaRequired,
1731
+ onBlur
1726
1732
  } = _this$props,
1727
1733
  sharedProps = _objectWithoutPropertiesLoose(_this$props, _excluded$2);
1728
1734
  const stateStyles = _generateStyles(light, isPlaceholder, error);
@@ -1731,6 +1737,8 @@ class SelectOpener extends React.Component {
1731
1737
  return React.createElement(StyledButton, _extends({}, sharedProps, {
1732
1738
  "aria-disabled": disabled,
1733
1739
  "aria-expanded": open ? "true" : "false",
1740
+ "aria-invalid": error,
1741
+ "aria-required": ariaRequired,
1734
1742
  "aria-haspopup": "listbox",
1735
1743
  "data-testid": testId,
1736
1744
  id: id,
@@ -1738,7 +1746,8 @@ class SelectOpener extends React.Component {
1738
1746
  type: "button",
1739
1747
  onClick: !disabled ? this.handleClick : undefined,
1740
1748
  onKeyDown: !disabled ? this.handleKeyDown : undefined,
1741
- onKeyUp: !disabled ? this.handleKeyUp : undefined
1749
+ onKeyUp: !disabled ? this.handleKeyUp : undefined,
1750
+ onBlur: onBlur
1742
1751
  }), React.createElement(LabelMedium, {
1743
1752
  style: styles$4.text
1744
1753
  }, children || "\u00A0"), React.createElement(PhosphorIcon, {
@@ -1887,117 +1896,206 @@ const _generateStyles = (light, placeholder, error) => {
1887
1896
  return stateStyles[styleKey];
1888
1897
  };
1889
1898
 
1890
- 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"];
1891
- class SingleSelect extends React.Component {
1892
- constructor(props) {
1893
- super(props);
1894
- this.selectedIndex = void 0;
1895
- this.handleOpenChanged = opened => {
1896
- this.setState({
1897
- open: opened,
1898
- searchText: ""
1899
- });
1900
- if (this.props.onToggle) {
1901
- 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);
1902
1921
  }
1903
- };
1904
- this.handleToggle = selectedValue => {
1905
- if (selectedValue !== this.props.selectedValue) {
1906
- this.props.onChange(selectedValue);
1922
+ if (error) {
1923
+ return;
1907
1924
  }
1908
- if (this.state.open && this.state.openerElement) {
1909
- 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);
1910
1932
  }
1911
- this.setState({
1912
- open: false
1913
- });
1914
- if (this.props.onToggle) {
1915
- 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;
1916
2064
  }
1917
- };
1918
- this.mapOptionItemsToDropdownItems = children => {
1919
- let indexCounter = 0;
1920
- this.selectedIndex = 0;
1921
- return children.map(option => {
1922
- const {
1923
- selectedValue
1924
- } = this.props;
1925
- const {
1926
- disabled,
1927
- value
1928
- } = option.props;
1929
- const selected = selectedValue === value;
1930
- if (selected) {
1931
- this.selectedIndex = indexCounter;
1932
- }
1933
- if (!disabled) {
1934
- 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"
1935
2075
  }
1936
- return {
1937
- component: option,
1938
- focusable: !disabled,
1939
- populatedProps: {
1940
- onToggle: this.handleToggle,
1941
- selected: selected,
1942
- variant: "check"
1943
- }
1944
- };
1945
- });
1946
- };
1947
- this.handleSearchTextChanged = searchText => {
1948
- this.setState({
1949
- searchText
1950
- });
1951
- };
1952
- this.handleOpenerRef = node => {
1953
- const openerElement = ReactDOM.findDOMNode(node);
1954
- this.setState({
1955
- openerElement
1956
- });
1957
- };
1958
- this.handleClick = e => {
1959
- this.handleOpenChanged(!this.state.open);
1960
- };
1961
- this.selectedIndex = 0;
1962
- this.state = {
1963
- open: false,
1964
- searchText: ""
1965
- };
1966
- }
1967
- static getDerivedStateFromProps(props, state) {
1968
- return {
1969
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
1970
- };
1971
- }
1972
- filterChildren(children) {
1973
- const {
1974
- searchText
1975
- } = this.state;
2076
+ };
2077
+ });
2078
+ };
2079
+ const filterChildren = children => {
1976
2080
  const lowercasedSearchText = searchText.toLowerCase();
1977
2081
  return children.filter(({
1978
2082
  props
1979
2083
  }) => !searchText || getLabel(props).toLowerCase().indexOf(lowercasedSearchText) > -1);
1980
- }
1981
- getMenuItems(children) {
1982
- const {
1983
- isFilterable
1984
- } = this.props;
1985
- return this.mapOptionItemsToDropdownItems(isFilterable ? this.filterChildren(children) : children);
1986
- }
1987
- renderOpener(isDisabled, dropdownId) {
1988
- const _this$props = this.props,
1989
- {
1990
- children,
1991
- error,
1992
- id,
1993
- light,
1994
- opener,
1995
- placeholder,
1996
- selectedValue,
1997
- testId,
1998
- showOpenerLabelAsText
1999
- } = _this$props,
2000
- 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) => {
2001
2099
  const items = React.Children.toArray(children);
2002
2100
  const selectedItem = items.find(option => option.props.value === selectedValue);
2003
2101
  const menuText = selectedItem ? getSelectOpenerLabel(showOpenerLabelAsText, selectedItem.props) : placeholder;
@@ -2009,202 +2107,160 @@ class SingleSelect extends React.Component {
2009
2107
  id: uniqueOpenerId,
2010
2108
  "aria-controls": dropdownId,
2011
2109
  "aria-haspopup": "listbox",
2012
- onClick: this.handleClick,
2110
+ onClick: handleClick,
2013
2111
  disabled: isDisabled,
2014
- ref: this.handleOpenerRef,
2112
+ ref: handleOpenerRef,
2015
2113
  text: menuText,
2016
- opened: this.state.open
2114
+ opened: open,
2115
+ error: hasError,
2116
+ onBlur: onOpenerBlurValidation
2017
2117
  }, opener) : React.createElement(SelectOpener, _extends({}, sharedProps, {
2018
2118
  "aria-controls": dropdownId,
2019
2119
  disabled: isDisabled,
2020
2120
  id: uniqueOpenerId,
2021
- error: error,
2121
+ error: hasError,
2022
2122
  isPlaceholder: !selectedItem,
2023
2123
  light: light,
2024
- onOpenChanged: this.handleOpenChanged,
2025
- open: this.state.open,
2026
- ref: this.handleOpenerRef,
2027
- testId: testId
2124
+ onOpenChanged: handleOpenChanged,
2125
+ open: open,
2126
+ ref: handleOpenerRef,
2127
+ testId: testId,
2128
+ onBlur: onOpenerBlurValidation
2028
2129
  }), menuText);
2029
2130
  });
2030
2131
  return dropdownOpener;
2031
- }
2032
- render() {
2033
- const {
2034
- alignment,
2035
- autoFocus,
2036
- children,
2037
- 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",
2038
2175
  dropdownStyle,
2039
- enableTypeAhead,
2176
+ implicitAllEnabled,
2040
2177
  isFilterable,
2041
- labels,
2042
- light,
2178
+ labels: propLabels,
2179
+ onChange,
2180
+ onToggle,
2181
+ opened,
2182
+ selectedValues = [],
2183
+ shortcuts = false,
2043
2184
  style,
2185
+ className,
2044
2186
  "aria-invalid": ariaInvalid,
2045
2187
  "aria-required": ariaRequired,
2046
- disabled,
2047
- dropdownId
2048
- } = this.props;
2049
- const {
2050
- searchText
2051
- } = this.state;
2052
- const allChildren = React.Children.toArray(children).filter(Boolean);
2053
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2054
- const items = this.getMenuItems(allChildren);
2055
- const isDisabled = numEnabledOptions === 0 || disabled;
2056
- return React.createElement(IDProvider, {
2057
- id: dropdownId,
2058
- scope: "single-select-dropdown"
2059
- }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2060
- id: uniqueDropdownId,
2061
- role: "listbox",
2062
- selectionType: "single",
2063
- alignment: alignment,
2064
- autoFocus: autoFocus,
2065
- enableTypeAhead: enableTypeAhead,
2066
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2067
- initialFocusedIndex: this.selectedIndex,
2068
- items: items,
2069
- light: light,
2070
- onOpenChanged: this.handleOpenChanged,
2071
- open: this.state.open,
2072
- opener: this.renderOpener(isDisabled, uniqueDropdownId),
2073
- openerElement: this.state.openerElement,
2074
- style: style,
2075
- className: className,
2076
- isFilterable: isFilterable,
2077
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2078
- searchText: isFilterable ? searchText : "",
2079
- labels: labels,
2080
- "aria-invalid": ariaInvalid,
2081
- "aria-required": ariaRequired,
2082
- disabled: isDisabled
2083
- }));
2084
- }
2085
- }
2086
- SingleSelect.defaultProps = {
2087
- alignment: "left",
2088
- autoFocus: true,
2089
- disabled: false,
2090
- enableTypeAhead: true,
2091
- error: false,
2092
- light: false,
2093
- labels: {
2094
- clearSearch: defaultLabels.clearSearch,
2095
- filter: defaultLabels.filter,
2096
- noResults: defaultLabels.noResults,
2097
- someResults: defaultLabels.someSelected
2098
- },
2099
- showOpenerLabelAsText: true
2100
- };
2101
-
2102
- const _excluded = ["id", "light", "opener", "testId", "alignment", "dropdownStyle", "implicitAllEnabled", "isFilterable", "labels", "onChange", "onToggle", "opened", "selectedValues", "shortcuts", "style", "className", "aria-invalid", "aria-required", "showOpenerLabelAsText"];
2103
- class MultiSelect extends React.Component {
2104
- constructor(props) {
2105
- super(props);
2106
- this.labels = void 0;
2107
- this.handleOpenChanged = opened => {
2108
- this.setState({
2109
- open: opened,
2110
- searchText: "",
2111
- lastSelectedValues: this.props.selectedValues
2112
- });
2113
- if (this.props.onToggle) {
2114
- this.props.onToggle(opened);
2115
- }
2116
- };
2117
- this.handleToggle = selectedValue => {
2118
- const {
2119
- onChange,
2120
- selectedValues
2121
- } = this.props;
2122
- if (selectedValues.includes(selectedValue)) {
2123
- const index = selectedValues.indexOf(selectedValue);
2124
- const updatedSelection = [...selectedValues.slice(0, index), ...selectedValues.slice(index + 1)];
2125
- 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);
2126
2235
  } else {
2127
- onChange([...selectedValues, selectedValue]);
2236
+ onDropdownClosedValidation();
2128
2237
  }
2129
- };
2130
- this.handleSelectAll = () => {
2131
- const {
2132
- children,
2133
- onChange
2134
- } = this.props;
2135
- const allChildren = React.Children.toArray(children);
2136
- const selected = allChildren.filter(option => !!option && !option.props.disabled).map(option => option.props.value);
2137
- onChange(selected);
2138
- };
2139
- this.handleSelectNone = () => {
2140
- const {
2141
- onChange
2142
- } = this.props;
2143
- onChange([]);
2144
- };
2145
- this.mapOptionItemToDropdownItem = option => {
2146
- const {
2147
- selectedValues
2148
- } = this.props;
2149
- const {
2150
- disabled,
2151
- value
2152
- } = option.props;
2153
- return {
2154
- component: option,
2155
- focusable: !disabled,
2156
- populatedProps: {
2157
- onToggle: this.handleToggle,
2158
- selected: selectedValues.includes(value),
2159
- variant: "checkbox"
2160
- }
2161
- };
2162
- };
2163
- this.handleOpenerRef = node => {
2164
- const openerElement = ReactDOM.findDOMNode(node);
2165
- this.setState({
2166
- openerElement
2167
- });
2168
- };
2169
- this.handleSearchTextChanged = searchText => {
2170
- this.setState({
2171
- searchText
2172
- });
2173
- };
2174
- this.handleClick = e => {
2175
- this.handleOpenChanged(!this.state.open);
2176
- };
2177
- this.state = {
2178
- open: false,
2179
- searchText: "",
2180
- lastSelectedValues: [],
2181
- labels: _extends({}, defaultLabels, props.labels)
2182
- };
2183
- this.labels = _extends({}, defaultLabels, props.labels);
2184
- }
2185
- static getDerivedStateFromProps(props, state) {
2186
- return {
2187
- open: props.disabled ? false : typeof props.opened === "boolean" ? props.opened : state.open
2188
- };
2189
- }
2190
- componentDidUpdate(prevProps) {
2191
- if (this.props.labels !== prevProps.labels) {
2192
- this.setState({
2193
- labels: _extends({}, this.state.labels, this.props.labels)
2194
- });
2195
2238
  }
2196
- }
2197
- getMenuText(children) {
2198
- const {
2199
- implicitAllEnabled,
2200
- selectedValues,
2201
- showOpenerLabelAsText
2202
- } = 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 => {
2203
2259
  const {
2204
2260
  noneSelected,
2205
2261
  someSelected,
2206
2262
  allSelected
2207
- } = this.state.labels;
2263
+ } = labels;
2208
2264
  const numSelectedAll = children.filter(option => !option.props.disabled).length;
2209
2265
  const noSelectionText = implicitAllEnabled ? allSelected : noneSelected;
2210
2266
  switch (selectedValues.length) {
@@ -2226,24 +2282,20 @@ class MultiSelect extends React.Component {
2226
2282
  default:
2227
2283
  return someSelected(selectedValues.length);
2228
2284
  }
2229
- }
2230
- getShortcuts(numOptions) {
2231
- const {
2232
- selectedValues,
2233
- shortcuts
2234
- } = this.props;
2285
+ };
2286
+ const getShortcuts = numOptions => {
2235
2287
  const {
2236
2288
  selectAllLabel,
2237
2289
  selectNoneLabel
2238
- } = this.state.labels;
2239
- if (shortcuts && !this.state.searchText) {
2290
+ } = labels;
2291
+ if (shortcuts && !searchText) {
2240
2292
  const selectAllDisabled = numOptions === selectedValues.length;
2241
2293
  const selectAll = {
2242
2294
  component: React.createElement(ActionItem, {
2243
2295
  disabled: selectAllDisabled,
2244
2296
  label: selectAllLabel(numOptions),
2245
2297
  indent: true,
2246
- onClick: this.handleSelectAll
2298
+ onClick: handleSelectAll
2247
2299
  }),
2248
2300
  focusable: !selectAllDisabled,
2249
2301
  populatedProps: {}
@@ -2254,7 +2306,7 @@ class MultiSelect extends React.Component {
2254
2306
  disabled: selectNoneDisabled,
2255
2307
  label: selectNoneLabel,
2256
2308
  indent: true,
2257
- onClick: this.handleSelectNone
2309
+ onClick: handleSelectNone
2258
2310
  }),
2259
2311
  focusable: !selectNoneDisabled,
2260
2312
  populatedProps: {}
@@ -2270,18 +2322,11 @@ class MultiSelect extends React.Component {
2270
2322
  } else {
2271
2323
  return [];
2272
2324
  }
2273
- }
2274
- getMenuItems(children) {
2275
- const {
2276
- isFilterable
2277
- } = this.props;
2325
+ };
2326
+ const getMenuItems = children => {
2278
2327
  if (!isFilterable) {
2279
- return children.map(this.mapOptionItemToDropdownItem);
2328
+ return children.map(mapOptionItemToDropdownItem);
2280
2329
  }
2281
- const {
2282
- searchText,
2283
- lastSelectedValues
2284
- } = this.state;
2285
2330
  const lowercasedSearchText = searchText.toLowerCase();
2286
2331
  const filteredChildren = children.filter(({
2287
2332
  props
@@ -2295,7 +2340,7 @@ class MultiSelect extends React.Component {
2295
2340
  restOfTheChildren.push(child);
2296
2341
  }
2297
2342
  }
2298
- const lastSelectedItems = lastSelectedChildren.map(this.mapOptionItemToDropdownItem);
2343
+ const lastSelectedItems = lastSelectedChildren.map(mapOptionItemToDropdownItem);
2299
2344
  if (lastSelectedChildren.length && restOfTheChildren.length) {
2300
2345
  lastSelectedItems.push({
2301
2346
  component: React.createElement(SeparatorItem, {
@@ -2305,116 +2350,109 @@ class MultiSelect extends React.Component {
2305
2350
  populatedProps: {}
2306
2351
  });
2307
2352
  }
2308
- return [...lastSelectedItems, ...restOfTheChildren.map(this.mapOptionItemToDropdownItem)];
2309
- }
2310
- renderOpener(allChildren, isDisabled, dropdownId) {
2311
- const _this$props = this.props,
2312
- {
2313
- id,
2314
- light,
2315
- opener,
2316
- testId
2317
- } = _this$props,
2318
- 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) => {
2319
2381
  const {
2320
2382
  noneSelected
2321
- } = this.state.labels;
2322
- const menuText = this.getMenuText(allChildren);
2383
+ } = labels;
2384
+ const menuText = getMenuText(allChildren);
2323
2385
  const dropdownOpener = React.createElement(IDProvider, {
2324
2386
  id: id,
2325
2387
  scope: "multi-select-opener"
2326
2388
  }, uniqueOpenerId => {
2327
2389
  return opener ? React.createElement(DropdownOpener, {
2328
2390
  id: uniqueOpenerId,
2391
+ error: hasError,
2329
2392
  "aria-controls": dropdownId,
2330
2393
  "aria-haspopup": "listbox",
2331
- onClick: this.handleClick,
2394
+ onClick: handleClick,
2395
+ onBlur: onOpenerBlurValidation,
2332
2396
  disabled: isDisabled,
2333
- ref: this.handleOpenerRef,
2397
+ ref: handleOpenerRef,
2334
2398
  text: menuText,
2335
- opened: this.state.open
2399
+ opened: open
2336
2400
  }, opener) : React.createElement(SelectOpener, _extends({}, sharedProps, {
2401
+ error: hasError,
2337
2402
  disabled: isDisabled,
2338
2403
  id: uniqueOpenerId,
2339
2404
  "aria-controls": dropdownId,
2340
2405
  isPlaceholder: menuText === noneSelected,
2341
2406
  light: light,
2342
- onOpenChanged: this.handleOpenChanged,
2343
- open: this.state.open,
2344
- ref: this.handleOpenerRef,
2407
+ onOpenChanged: handleOpenChanged,
2408
+ onBlur: onOpenerBlurValidation,
2409
+ open: open,
2410
+ ref: handleOpenerRef,
2345
2411
  testId: testId
2346
2412
  }), menuText);
2347
2413
  });
2348
2414
  return dropdownOpener;
2349
- }
2350
- render() {
2351
- const {
2352
- alignment,
2353
- light,
2354
- style,
2355
- className,
2356
- dropdownStyle,
2357
- children,
2358
- isFilterable,
2359
- "aria-invalid": ariaInvalid,
2360
- "aria-required": ariaRequired,
2361
- disabled,
2362
- dropdownId
2363
- } = this.props;
2364
- const {
2365
- open,
2366
- searchText
2367
- } = this.state;
2368
- 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: {
2369
2447
  clearSearch,
2370
2448
  filter,
2371
2449
  noResults,
2372
- someSelected
2373
- } = this.state.labels;
2374
- const allChildren = React.Children.toArray(children).filter(Boolean);
2375
- const numEnabledOptions = allChildren.filter(option => !option.props.disabled).length;
2376
- const filteredItems = this.getMenuItems(allChildren);
2377
- const isDisabled = numEnabledOptions === 0 || disabled;
2378
- return React.createElement(IDProvider, {
2379
- id: dropdownId,
2380
- scope: "multi-select-dropdown"
2381
- }, uniqueDropdownId => React.createElement(DropdownCore$1, {
2382
- id: uniqueDropdownId,
2383
- role: "listbox",
2384
- alignment: alignment,
2385
- dropdownStyle: [isFilterable && filterableDropdownStyle, selectDropdownStyle, dropdownStyle],
2386
- isFilterable: isFilterable,
2387
- items: [...this.getShortcuts(numEnabledOptions), ...filteredItems],
2388
- light: light,
2389
- onOpenChanged: this.handleOpenChanged,
2390
- open: open,
2391
- opener: this.renderOpener(allChildren, isDisabled, uniqueDropdownId),
2392
- openerElement: this.state.openerElement,
2393
- selectionType: "multi",
2394
- style: style,
2395
- className: className,
2396
- onSearchTextChanged: isFilterable ? this.handleSearchTextChanged : undefined,
2397
- searchText: isFilterable ? searchText : "",
2398
- labels: {
2399
- clearSearch,
2400
- filter,
2401
- noResults,
2402
- someResults: someSelected
2403
- },
2404
- "aria-invalid": ariaInvalid,
2405
- "aria-required": ariaRequired,
2406
- disabled: isDisabled
2407
- }));
2408
- }
2409
- }
2410
- MultiSelect.defaultProps = {
2411
- alignment: "left",
2412
- disabled: false,
2413
- error: false,
2414
- light: false,
2415
- shortcuts: false,
2416
- selectedValues: [],
2417
- showOpenerLabelAsText: true
2450
+ someResults: someSelected
2451
+ },
2452
+ "aria-invalid": ariaInvalid,
2453
+ "aria-required": ariaRequired,
2454
+ disabled: isDisabled
2455
+ }));
2418
2456
  };
2419
2457
 
2420
2458
  function updateMultipleSelection(previousSelection, value = "") {