@mtes-mct/monitor-ui 6.3.2 → 6.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [6.3.2](https://github.com/MTES-MCT/monitor-ui/compare/v6.3.1...v6.3.2) (2023-05-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **libs:** make CustomSearch fully functional ([931dda0](https://github.com/MTES-MCT/monitor-ui/commit/931dda03e79ba3710d9ae0d14a45305e0c57b819))
7
+
1
8
  ## [6.3.1](https://github.com/MTES-MCT/monitor-ui/compare/v6.3.0...v6.3.1) (2023-05-24)
2
9
 
3
10
 
@@ -6,6 +6,8 @@ export type MultiSelectProps<OptionValue extends OptionValueType = string> = Omi
6
6
  /** Used to pass something else than `window.document` as a base container to attach global events listeners. */
7
7
  baseContainer?: Document | HTMLDivElement | null | undefined;
8
8
  customSearch?: CustomSearch<Option<OptionValue>> | undefined;
9
+ /** Minimum search query length required to trigger custom search filtering. */
10
+ customSearchMinQueryLength?: number | undefined;
9
11
  error?: string | undefined;
10
12
  isErrorMessageHidden?: boolean | undefined;
11
13
  isLabelHidden?: boolean | undefined;
@@ -18,4 +20,4 @@ export type MultiSelectProps<OptionValue extends OptionValueType = string> = Omi
18
20
  options: Option<OptionValue>[];
19
21
  value?: OptionValue[] | undefined;
20
22
  };
21
- export declare function MultiSelect<OptionValue extends OptionValueType = string>({ baseContainer, customSearch, disabled, error, isErrorMessageHidden, isLabelHidden, isLight, isUndefinedWhenDisabled, label, onChange, options, optionValueKey, searchable, value, ...originalProps }: MultiSelectProps<OptionValue>): JSX.Element;
23
+ export declare function MultiSelect<OptionValue extends OptionValueType = string>({ baseContainer, customSearch, customSearchMinQueryLength, disabled, error, isErrorMessageHidden, isLabelHidden, isLight, isUndefinedWhenDisabled, label, onChange, options, optionValueKey, searchable, value, ...originalProps }: MultiSelectProps<OptionValue>): JSX.Element;
@@ -6,6 +6,8 @@ export type SelectProps<OptionValue extends OptionValueType = string> = Omit<Sel
6
6
  /** Used to pass something else than `window.document` as a base container to attach global events listeners. */
7
7
  baseContainer?: Document | HTMLDivElement | null | undefined;
8
8
  customSearch?: CustomSearch<Option<OptionValue>> | undefined;
9
+ /** Minimum search query length required to trigger custom search filtering. */
10
+ customSearchMinQueryLength?: number | undefined;
9
11
  error?: string | undefined;
10
12
  isCleanable?: boolean | undefined;
11
13
  isErrorMessageHidden?: boolean | undefined;
@@ -19,4 +21,4 @@ export type SelectProps<OptionValue extends OptionValueType = string> = Omit<Sel
19
21
  options: Option<OptionValue>[];
20
22
  value?: OptionValue | undefined;
21
23
  };
22
- export declare function Select<OptionValue extends OptionValueType = string>({ baseContainer, customSearch, disabled, error, isCleanable, isErrorMessageHidden, isLabelHidden, isLight, isUndefinedWhenDisabled, label, onChange, options, optionValueKey, searchable, value, ...originalProps }: SelectProps<OptionValue>): JSX.Element;
24
+ export declare function Select<OptionValue extends OptionValueType = string>({ baseContainer, customSearch, customSearchMinQueryLength, disabled, error, isCleanable, isErrorMessageHidden, isLabelHidden, isLight, isUndefinedWhenDisabled, label, onChange, options, optionValueKey, searchable, value, ...originalProps }: SelectProps<OptionValue>): JSX.Element;
package/index.js CHANGED
@@ -25681,47 +25681,19 @@ class CustomSearch {
25681
25681
  }
25682
25682
  }
25683
25683
 
25684
- function MultiSelect({ baseContainer, customSearch, disabled = false, error, isErrorMessageHidden = false, isLabelHidden = false, isLight = false, isUndefinedWhenDisabled = false, label, onChange, options, optionValueKey, searchable = false, value, ...originalProps }) {
25684
+ function MultiSelect({ baseContainer, customSearch, customSearchMinQueryLength = 1, disabled = false, error, isErrorMessageHidden = false, isLabelHidden = false, isLight = false, isUndefinedWhenDisabled = false, label, onChange, options, optionValueKey, searchable = false, value, ...originalProps }) {
25685
25685
  // eslint-disable-next-line no-null/no-null
25686
25686
  const boxRef = useRef(null);
25687
- /**
25688
- * Current list of option labels found by `CustomSearch.find()` for the current select search query
25689
- *
25690
- * @description
25691
- * `undefined` means that search query is empty, thus all labels should be a match.
25692
- */
25693
- const customSearchLabelMatchesRef = useRef(undefined);
25694
25687
  /** Instance of `CustomSearch` */
25695
25688
  const customSearchRef = useRef(customSearch);
25696
- /** Last search query (only used when `customSearch` prop is set) */
25697
- const previousSearchQueryRef = useRef('');
25698
- const [isOpen, setIsOpen] = useState(false);
25699
25689
  const controlledError = useMemo(() => normalizeString(error), [error]);
25700
25690
  const rsuiteData = useMemo(() => getRsuiteDataFromOptions(options, optionValueKey), [options, optionValueKey]);
25701
25691
  const hasError = useMemo(() => Boolean(controlledError), [controlledError]);
25702
25692
  const key = useKey([disabled, originalProps.name, value]);
25703
25693
  const selectedRsuiteValue = useMemo(() => (value || []).map(valueItem => getRsuiteValueFromOptionValue(valueItem, optionValueKey)), [optionValueKey, value]);
25704
- const searchBy = useMemo(() =>
25705
- // Since this function is called by a `.filter()` in Rsuite,
25706
- // we first prepare `CustomSearch` results in `handleSearch()` (called each time the search query changes),
25707
- // and we use the `customSearchLabelMatches` ref-stored results, in the form of option labels,
25708
- // to check if the current option label is part of these results.
25709
- // Note that options label are expected to be unique in order for this pattern to work.
25710
- customSearchRef.current
25711
- ? (query, _label, item) => {
25712
- if (!customSearchRef.current) {
25713
- throw new Error('`customSearchRef.current` is undefined.');
25714
- }
25715
- // Since this function will be called xN times, N being the number of options,
25716
- // we only want to update found option labels once each time the search query changes.
25717
- if (query !== previousSearchQueryRef.current) {
25718
- const nextCustomSearchLabelMatches = query.trim().length > 0 ? customSearchRef.current.find(query).map(option => option.label) : undefined;
25719
- customSearchLabelMatchesRef.current = nextCustomSearchLabelMatches;
25720
- previousSearchQueryRef.current = query;
25721
- }
25722
- return customSearchLabelMatchesRef.current ? customSearchLabelMatchesRef.current.includes(item.label) : true;
25723
- }
25724
- : undefined, []);
25694
+ // Only used when `customSearch` prop is set
25695
+ const [controlledRsuiteData, setControlledRsuiteData] = useState(customSearch ? rsuiteData : undefined);
25696
+ const [isOpen, setIsOpen] = useState(false);
25725
25697
  const { forceUpdate } = useForceUpdate();
25726
25698
  const close = useCallback(() => {
25727
25699
  setIsOpen(false);
@@ -25738,8 +25710,18 @@ function MultiSelect({ baseContainer, customSearch, disabled = false, error, isE
25738
25710
  }
25739
25711
  const nextValue = nextOptionRsuiteValues ? getOptionValuesFromRsuiteDataValues(nextOptionRsuiteValues) : [];
25740
25712
  const normalizedNextValue = nextValue.length > 0 ? nextValue : undefined;
25713
+ setControlledRsuiteData(rsuiteData);
25741
25714
  onChange(normalizedNextValue);
25742
- }, [getOptionValuesFromRsuiteDataValues, onChange]);
25715
+ }, [getOptionValuesFromRsuiteDataValues, onChange, rsuiteData]);
25716
+ const handleSearch = useCallback((nextQuery) => {
25717
+ if (!customSearchRef.current || nextQuery.trim().length < customSearchMinQueryLength) {
25718
+ return;
25719
+ }
25720
+ const nextControlledRsuiteData = nextQuery.trim().length >= customSearchMinQueryLength
25721
+ ? getRsuiteDataFromOptions(customSearchRef.current.find(nextQuery), optionValueKey)
25722
+ : rsuiteData;
25723
+ setControlledRsuiteData(nextControlledRsuiteData);
25724
+ }, [customSearchMinQueryLength, optionValueKey, rsuiteData]);
25743
25725
  const renderMenuItem = useCallback((node) => jsx("span", { title: String(node), children: String(node) }), []);
25744
25726
  const toggle = useCallback((event) => {
25745
25727
  let targetElement = event.target;
@@ -25759,7 +25741,13 @@ function MultiSelect({ baseContainer, customSearch, disabled = false, error, isE
25759
25741
  useEffect(() => {
25760
25742
  forceUpdate();
25761
25743
  }, [forceUpdate]);
25762
- return (jsxs(Field$2, { className: "Field-MultiSelect", children: [jsx(Label, { disabled: disabled, hasError: hasError, htmlFor: originalProps.name, isHidden: isLabelHidden, children: label }), jsx(Box$4, { ref: boxRef, "$hasError": hasError, "$isActive": isOpen, "$isLight": isLight, onClick: toggle, children: boxRef.current && (jsx(TagPicker, { container: boxRef.current, data: rsuiteData, disabled: disabled, id: originalProps.name, onChange: handleChange, onClick: toggle, open: isOpen, renderMenuItem: renderMenuItem, searchable: !!customSearch || searchable, searchBy: searchBy, value: selectedRsuiteValue, ...originalProps }, key)) }), !isErrorMessageHidden && hasError && jsx(FieldError, { children: controlledError })] }));
25744
+ return (jsxs(Field$2, { className: "Field-MultiSelect", children: [jsx(Label, { disabled: disabled, hasError: hasError, htmlFor: originalProps.name, isHidden: isLabelHidden, children: label }), jsx(Box$4, { ref: boxRef, "$hasError": hasError, "$isActive": isOpen, "$isLight": isLight, onClick: toggle, children: boxRef.current && (jsx(TagPicker, { container: boxRef.current,
25745
+ // When we use a custom search, we use `controlledRsuiteData` to provide the matching options (data),
25746
+ // when we don't, we don't need to control that and just pass the non-internally-controlled `rsuiteData`
25747
+ data: controlledRsuiteData || rsuiteData, disabled: disabled, id: originalProps.name, onChange: handleChange, onClick: toggle, onSearch: handleSearch, open: isOpen, renderMenuItem: renderMenuItem, searchable: !!customSearch || searchable,
25748
+ // When we use a custom search, we use `controlledRsuiteData` to provide the matching options (data),
25749
+ // that's why we send this "always true" filter to disable Rsuite TagPicker internal search filtering
25750
+ searchBy: (customSearch ? () => true : undefined), value: selectedRsuiteValue, ...originalProps }, key)) }), !isErrorMessageHidden && hasError && jsx(FieldError, { children: controlledError })] }));
25763
25751
  }
25764
25752
  const Box$4 = styled.div `
25765
25753
  position: relative;
@@ -34943,48 +34931,20 @@ const StyledFieldset = styled(Fieldset) `
34943
34931
  }
34944
34932
  `;
34945
34933
 
34946
- function Select({ baseContainer, customSearch, disabled = false, error, isCleanable = true, isErrorMessageHidden = false, isLabelHidden = false, isLight = false, isUndefinedWhenDisabled = false, label, onChange, options, optionValueKey, searchable = false, value, ...originalProps }) {
34934
+ function Select({ baseContainer, customSearch, customSearchMinQueryLength = 1, disabled = false, error, isCleanable = true, isErrorMessageHidden = false, isLabelHidden = false, isLight = false, isUndefinedWhenDisabled = false, label, onChange, options, optionValueKey, searchable = false, value, ...originalProps }) {
34947
34935
  // eslint-disable-next-line no-null/no-null
34948
34936
  const boxRef = useRef(null);
34949
- /**
34950
- * Current list of option labels found by `CustomSearch.find()` for the current select search query
34951
- *
34952
- * @description
34953
- * `undefined` means that search query is empty, thus all labels should be a match.
34954
- */
34955
- const customSearchLabelMatchesRef = useRef(undefined);
34956
34937
  /** Instance of `CustomSearch` */
34957
34938
  const customSearchRef = useRef(customSearch);
34958
- /** Last search query (only used when `customSearch` prop is set) */
34959
- const previousSearchQueryRef = useRef('');
34960
- const [isOpen, setIsOpen] = useState(false);
34961
34939
  const { forceUpdate } = useForceUpdate();
34962
34940
  const controlledError = useMemo(() => normalizeString(error), [error]);
34963
34941
  const rsuiteData = useMemo(() => getRsuiteDataFromOptions(options, optionValueKey), [options, optionValueKey]);
34964
34942
  const hasError = useMemo(() => Boolean(controlledError), [controlledError]);
34965
34943
  const key = useKey([disabled, originalProps.name, value]);
34966
34944
  const selectedRsuiteValue = useMemo(() => getRsuiteValueFromOptionValue(value, optionValueKey), [value, optionValueKey]);
34967
- const searchBy = useMemo(() =>
34968
- // Since this function is called by a `.filter()` in Rsuite,
34969
- // we first prepare `CustomSearch` results in `handleSearch()` (called each time the search query changes),
34970
- // and we use the `customSearchLabelMatches` ref-stored results, in the form of option labels,
34971
- // to check if the current option label is part of these results.
34972
- // Note that options label are expected to be unique in order for this pattern to work.
34973
- customSearchRef.current
34974
- ? (query, _label, item) => {
34975
- if (!customSearchRef.current) {
34976
- throw new Error('`customSearchRef.current` is undefined.');
34977
- }
34978
- // Since this function will be called xN times, N being the number of options,
34979
- // we only want to update found option labels once each time the search query changes.
34980
- if (query !== previousSearchQueryRef.current) {
34981
- const nextCustomSearchLabelMatches = query.trim().length > 0 ? customSearchRef.current.find(query).map(option => option.label) : undefined;
34982
- customSearchLabelMatchesRef.current = nextCustomSearchLabelMatches;
34983
- previousSearchQueryRef.current = query;
34984
- }
34985
- return customSearchLabelMatchesRef.current ? customSearchLabelMatchesRef.current.includes(item.label) : true;
34986
- }
34987
- : undefined, []);
34945
+ // Only used when `customSearch` prop is set
34946
+ const [controlledRsuiteData, setControlledRsuiteData] = useState(customSearch ? rsuiteData : undefined);
34947
+ const [isOpen, setIsOpen] = useState(false);
34988
34948
  const close = useCallback(() => {
34989
34949
  setIsOpen(false);
34990
34950
  }, []);
@@ -34994,6 +34954,15 @@ function Select({ baseContainer, customSearch, disabled = false, error, isCleana
34994
34954
  }
34995
34955
  onChange(undefined);
34996
34956
  }, [onChange]);
34957
+ const handleSearch = useCallback((nextQuery) => {
34958
+ if (!customSearchRef.current || nextQuery.trim().length < customSearchMinQueryLength) {
34959
+ return;
34960
+ }
34961
+ const nextControlledRsuiteData = nextQuery.trim().length >= customSearchMinQueryLength
34962
+ ? getRsuiteDataFromOptions(customSearchRef.current.find(nextQuery), optionValueKey)
34963
+ : rsuiteData;
34964
+ setControlledRsuiteData(nextControlledRsuiteData);
34965
+ }, [customSearchMinQueryLength, optionValueKey, rsuiteData]);
34997
34966
  const handleSelect = useCallback((_, selectedItem) => {
34998
34967
  close();
34999
34968
  if (onChange) {
@@ -35021,10 +34990,16 @@ function Select({ baseContainer, customSearch, disabled = false, error, isCleana
35021
34990
  useEffect(() => {
35022
34991
  forceUpdate();
35023
34992
  }, [forceUpdate]);
35024
- return (jsxs(Field$2, { className: "Field-Select", children: [jsx(Label, { disabled: disabled, hasError: hasError, htmlFor: originalProps.name, isHidden: isLabelHidden, children: label }), jsx(Box, { ref: boxRef, onClick: toggle, children: boxRef.current && (jsx(StyledSelectPicker, { "$isLight": isLight, cleanable: isCleanable, container: boxRef.current, data: rsuiteData, disabled: disabled, id: originalProps.name, onClean: handleClean,
34993
+ return (jsxs(Field$2, { className: "Field-Select", children: [jsx(Label, { disabled: disabled, hasError: hasError, htmlFor: originalProps.name, isHidden: isLabelHidden, children: label }), jsx(Box, { ref: boxRef, onClick: toggle, children: boxRef.current && (jsx(StyledSelectPicker, { "$isLight": isLight, cleanable: isCleanable, container: boxRef.current,
34994
+ // When we use a custom search, we use `controlledRsuiteData` to provide the matching options (data),
34995
+ // when we don't, we don't need to control that and just pass the non-internally-controlled `rsuiteData`
34996
+ data: controlledRsuiteData || rsuiteData, disabled: disabled, id: originalProps.name, onClean: handleClean, onSearch: handleSearch,
35025
34997
  // `as any` because we customized `ItemDataType` type by adding `optionValue`,
35026
34998
  // which generates an optional vs required type conflict
35027
- onSelect: handleSelect, open: isOpen, renderMenuItem: renderMenuItem, searchable: !!customSearch || searchable, searchBy: searchBy, value: selectedRsuiteValue, ...originalProps }, key)) }), !isErrorMessageHidden && hasError && jsx(FieldError, { children: controlledError })] }));
34999
+ onSelect: handleSelect, open: isOpen, renderMenuItem: renderMenuItem, searchable: !!customSearch || searchable,
35000
+ // When we use a custom search, we use `controlledRsuiteData` to provide the matching options (data),
35001
+ // that's why we send this "always true" filter to disable Rsuite SelectPicker internal search filtering
35002
+ searchBy: (customSearch ? () => true : undefined), value: selectedRsuiteValue, ...originalProps }, key)) }), !isErrorMessageHidden && hasError && jsx(FieldError, { children: controlledError })] }));
35028
35003
  }
35029
35004
  const StyledSelectPicker = styled(SelectPicker) `
35030
35005
  > .rs-picker-toggle {