sbwb-ds 3.36.2 → 3.37.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.
@@ -1,7 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  import { SelectOption } from '../../../../../models/select-option';
3
3
  import { AutocompleteProps } from '..';
4
- export declare const useAutocomplete: (handleOption: (value: SelectOption | SelectOption[] | undefined) => void, { value, options, size, variant, }: Pick<AutocompleteProps, 'value' | 'options' | 'size' | 'variant'>) => {
4
+ export declare const useAutocomplete: (handleOption: (value: SelectOption | SelectOption[] | undefined) => void, { value, options, size, variant, clearSearchOnSelect, }: Pick<AutocompleteProps, 'value' | 'options' | 'size' | 'variant' | 'clearSearchOnSelect'>) => {
5
5
  isOpen: boolean;
6
6
  setIsOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
7
7
  expanded: boolean;
@@ -25,6 +25,7 @@ export interface AutocompleteProps extends CustomDataAttributes {
25
25
  onEnter?: (option: SelectOption) => void;
26
26
  onInputEnter?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
27
27
  disableSelectOnEnter?: boolean;
28
+ clearSearchOnSelect?: boolean;
28
29
  /** @deprecated Use `onChange` instead */
29
30
  onSearchTextChange?: (value: string) => void;
30
31
  /** @deprecated Use `onSelect` instead */
@@ -12,6 +12,7 @@ declare const MultiSelect: React.ForwardRefExoticComponent<{
12
12
  label?: React.ReactNode;
13
13
  onChange: (values: string[]) => void;
14
14
  onSearchChange?: (searchText: string) => void;
15
+ clearSearchOnSelect?: boolean;
15
16
  disabled?: boolean;
16
17
  enableSearch?: boolean;
17
18
  width?: string;
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -18,6 +18,7 @@ type MultiSelectProps = {
18
18
  label?: string | React.ReactNode;
19
19
  onChange: (values: string[]) => void;
20
20
  onSearchChange?: (searchText: string) => void;
21
+ clearSearchOnSelect?: boolean;
21
22
  disabled?: boolean;
22
23
  enableSearch?: boolean;
23
24
  width?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbwb-ds",
3
- "version": "3.36.2",
3
+ "version": "3.37.0",
4
4
  "type": "module",
5
5
  "description": "Sistema de design para padronização dos processos visuais do portal SUBWEB",
6
6
  "main": "dist/sbwb-ds.js",
@@ -95,6 +95,11 @@ const meta: Meta<typeof Autocomplete> = {
95
95
  description:
96
96
  'Prevents selecting the focused option when pressing Enter; pair with onInputEnter to handle filtering.',
97
97
  },
98
+ clearSearchOnSelect: {
99
+ control: 'boolean',
100
+ description:
101
+ 'Clears the search term after selecting an option. Set to false for withTag to keep filtering during multiple selections.',
102
+ },
98
103
  onSearchTextChange: {
99
104
  description: 'Function to be called when the search text is changed.',
100
105
  },
@@ -166,6 +166,13 @@ export default Example;`}
166
166
  autocomplete input. Options include "singleValue" for single selection
167
167
  and "withTag" for multi-selection with tags.
168
168
  </li>
169
+ <li>
170
+ <InlineCode>clearSearchOnSelect</InlineCode>: Clears the search term
171
+ after selecting an option. Defaults to <InlineCode>true</InlineCode>;
172
+ set it to <InlineCode>false</InlineCode> with{' '}
173
+ <InlineCode>variant="withTag"</InlineCode> to keep filtering while the
174
+ menu remains open.
175
+ </li>
169
176
  <li>
170
177
  <InlineCode>onTagClose</InlineCode>: A callback function that handles
171
178
  the removal of a tag in multi-select mode.
@@ -11,7 +11,11 @@ export const useAutocomplete = (
11
11
  options,
12
12
  size,
13
13
  variant,
14
- }: Pick<AutocompleteProps, 'value' | 'options' | 'size' | 'variant'>
14
+ clearSearchOnSelect,
15
+ }: Pick<
16
+ AutocompleteProps,
17
+ 'value' | 'options' | 'size' | 'variant' | 'clearSearchOnSelect'
18
+ >
15
19
  ) => {
16
20
  const [isOpen, setIsOpen] = useState(false);
17
21
  const [expanded, setExpanded] = useState(false);
@@ -57,7 +61,9 @@ export const useAutocomplete = (
57
61
  }, [options]);
58
62
 
59
63
  function autocompleteOption(option: SelectOption) {
60
- setInputValue('');
64
+ if (clearSearchOnSelect || variant === 'singleValue') {
65
+ setInputValue('');
66
+ }
61
67
  setExpanded(variant === 'singleValue' ? false : true);
62
68
  setIsOpen(variant === 'singleValue' ? false : true);
63
69
  inputContainerRef?.current && inputContainerRef.current.focus();
@@ -1,5 +1,6 @@
1
1
  import { render, screen, act, fireEvent } from '@testing-library/react';
2
2
  import '@testing-library/jest-dom';
3
+ import { useState } from 'react';
3
4
  import { SelectOption } from '../../../../models/select-option';
4
5
  import { Autocomplete } from '../../atoms/index';
5
6
 
@@ -28,7 +29,7 @@ describe('Autocomplete component', () => {
28
29
  />
29
30
  );
30
31
 
31
- const autocomplete = screen.getByTestId(/autocomplete/i);
32
+ const autocomplete = screen.getByTestId('autocomplete');
32
33
 
33
34
  act(() => {
34
35
  fireEvent.click(autocomplete);
@@ -52,7 +53,7 @@ describe('Autocomplete component', () => {
52
53
  size="Small"
53
54
  />
54
55
  );
55
- const autocomplete = screen.getByTestId(/autocomplete/i);
56
+ const autocomplete = screen.getByTestId('autocomplete');
56
57
 
57
58
  act(() => {
58
59
  fireEvent.click(autocomplete);
@@ -186,7 +187,7 @@ describe('Autocomplete component', () => {
186
187
  />
187
188
  );
188
189
 
189
- const autocomplete = screen.getByTestId(/autocomplete/i);
190
+ const autocomplete = screen.getByTestId('autocomplete');
190
191
 
191
192
  act(() => {
192
193
  fireEvent.click(autocomplete);
@@ -200,4 +201,89 @@ describe('Autocomplete component', () => {
200
201
 
201
202
  expect(option).not.toBeInTheDocument();
202
203
  });
204
+
205
+ it('should preserve the search term after selecting a withTag option when clearSearchOnSelect is false', () => {
206
+ const WithTagAutocomplete = () => {
207
+ const [value, setValue] = useState<SelectOption[]>([]);
208
+
209
+ return (
210
+ <Autocomplete
211
+ options={options}
212
+ value={value}
213
+ variant="withTag"
214
+ clearSearchOnSelect={false}
215
+ onSelect={(nextValue) =>
216
+ setValue((nextValue as SelectOption[]) ?? [])
217
+ }
218
+ />
219
+ );
220
+ };
221
+
222
+ render(<WithTagAutocomplete />);
223
+
224
+ const input = screen.getByTestId('autocomplete-input');
225
+ fireEvent.change(input, { target: { value: 'th' } });
226
+ fireEvent.click(screen.getByTestId('3'));
227
+
228
+ expect(input).toHaveValue('th');
229
+ expect(
230
+ screen.getByTestId('autocomplete-menu-multi-select')
231
+ ).toBeInTheDocument();
232
+ });
233
+
234
+ it('should clear the preserved search term after closing the withTag menu', () => {
235
+ const WithTagAutocomplete = () => {
236
+ const [value, setValue] = useState<SelectOption[]>([]);
237
+
238
+ return (
239
+ <Autocomplete
240
+ options={options}
241
+ value={value}
242
+ variant="withTag"
243
+ clearSearchOnSelect={false}
244
+ onSelect={(nextValue) =>
245
+ setValue((nextValue as SelectOption[]) ?? [])
246
+ }
247
+ />
248
+ );
249
+ };
250
+
251
+ render(<WithTagAutocomplete />);
252
+
253
+ const input = screen.getByTestId('autocomplete-input');
254
+ fireEvent.change(input, { target: { value: 'th' } });
255
+ fireEvent.click(screen.getByTestId('3'));
256
+ fireEvent.mouseDown(document.body);
257
+
258
+ expect(input).toHaveValue('');
259
+ });
260
+
261
+ it('should clear the preserved search term when pressing Escape', () => {
262
+ const WithTagAutocomplete = () => {
263
+ const [value, setValue] = useState<SelectOption[]>([]);
264
+
265
+ return (
266
+ <Autocomplete
267
+ options={options}
268
+ value={value}
269
+ variant="withTag"
270
+ clearSearchOnSelect={false}
271
+ onSelect={(nextValue) =>
272
+ setValue((nextValue as SelectOption[]) ?? [])
273
+ }
274
+ />
275
+ );
276
+ };
277
+
278
+ render(<WithTagAutocomplete />);
279
+
280
+ const input = screen.getByTestId('autocomplete-input');
281
+ fireEvent.change(input, { target: { value: 'th' } });
282
+ fireEvent.keyDown(input, { key: 'Escape' });
283
+
284
+ expect(input).toHaveValue('');
285
+ expect(
286
+ screen.queryByTestId('autocomplete-menu-multi-select')
287
+ ).not.toBeInTheDocument();
288
+ });
203
289
  });
@@ -2,7 +2,7 @@ import { Icons } from '../../../../main/helpers/functions/icons';
2
2
  import { SelectOption } from '../../../../models/select-option';
3
3
  import * as C from './styles';
4
4
  import { useAutocomplete } from './hooks/useAutocomplete';
5
- import { forwardRef, ReactNode, useEffect } from 'react';
5
+ import { forwardRef, ReactNode } from 'react';
6
6
  import MenuSelect from '../MenuSelect';
7
7
  import MenuMultiSelect from '../../atoms/MenuMultiSelect';
8
8
  import Tag from '../../atoms/Tag';
@@ -35,6 +35,7 @@ export interface AutocompleteProps extends CustomDataAttributes {
35
35
  onEnter?: (option: SelectOption) => void;
36
36
  onInputEnter?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
37
37
  disableSelectOnEnter?: boolean;
38
+ clearSearchOnSelect?: boolean;
38
39
 
39
40
  /** @deprecated Use `onChange` instead */
40
41
  onSearchTextChange?: (value: string) => void;
@@ -101,6 +102,7 @@ const Autocomplete: React.FC<AutocompleteProps> = forwardRef<
101
102
  onEnter,
102
103
  onInputEnter,
103
104
  disableSelectOnEnter = false,
105
+ clearSearchOnSelect = true,
104
106
  onSearchTextChange,
105
107
  onChange,
106
108
  errorMessage,
@@ -151,7 +153,13 @@ const Autocomplete: React.FC<AutocompleteProps> = forwardRef<
151
153
  isValue,
152
154
  handleRemoveOption,
153
155
  handleBackspace,
154
- } = useAutocomplete(handlerOption, { value, options, size, variant });
156
+ } = useAutocomplete(handlerOption, {
157
+ value,
158
+ options,
159
+ size,
160
+ variant,
161
+ clearSearchOnSelect,
162
+ });
155
163
 
156
164
  const hasError = !!errorMessage;
157
165
 
@@ -173,6 +181,12 @@ const Autocomplete: React.FC<AutocompleteProps> = forwardRef<
173
181
  } else if (!Array.isArray(value) && value) {
174
182
  handleBackspace();
175
183
  }
184
+ } else if (e.key === 'Escape') {
185
+ e.preventDefault();
186
+ setInputValue('');
187
+ setIsOpen(false);
188
+ setExpanded(false);
189
+ inputContainerRef.current?.blur();
176
190
  } else if (e.key === 'Enter') {
177
191
  e.preventDefault();
178
192
  if (onInputEnter) {
@@ -33,6 +33,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
33
33
  onChange,
34
34
  onSearchChange, // Nova prop para capturar mudanças no texto de busca
35
35
  searchValue, // Nova prop para controlar o valor de busca externamente
36
+ clearSearchOnSelect = true,
36
37
  disabled = false,
37
38
  enableSearch = true,
38
39
  width = '100%',
@@ -126,6 +127,11 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
126
127
  setIsFocused(false);
127
128
  }, []);
128
129
 
130
+ const clearSearch = useCallback(() => {
131
+ setInternalSearchText('');
132
+ onSearchChange?.('');
133
+ }, [onSearchChange]);
134
+
129
135
  const selectableOptions = useMemo(
130
136
  () => options.filter((opt) => !opt.disabled),
131
137
  [options]
@@ -150,14 +156,16 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
150
156
  : [...values, optionValue];
151
157
  handleChange(newValues);
152
158
  }
153
- setInternalSearchText('');
154
- onSearchChange?.('');
159
+ if (clearSearchOnSelect) {
160
+ clearSearch();
161
+ }
155
162
  handleInputFocus();
156
163
  },
157
164
  [
158
165
  values,
159
166
  handleChange,
160
- onSearchChange,
167
+ clearSearch,
168
+ clearSearchOnSelect,
161
169
  handleInputFocus,
162
170
  enableSelectAll,
163
171
  isAllSelected,
@@ -172,13 +180,14 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
172
180
  }
173
181
  if (e.key === 'Escape') {
174
182
  setIsOpen(false);
183
+ clearSearch();
175
184
  inputRef.current?.blur();
176
185
  }
177
186
  if (e.key === 'Enter') {
178
187
  e.preventDefault();
179
188
  }
180
189
  },
181
- [searchText, values, handleChange]
190
+ [searchText, values, handleChange, clearSearch]
182
191
  );
183
192
 
184
193
  const handleKeyUp = useCallback(
@@ -225,16 +234,14 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
225
234
  !selectContainerRef.current.contains(target)
226
235
  ) {
227
236
  setIsOpen(false);
228
- if (searchValue === undefined && !onSearchChange) {
229
- setInternalSearchText('');
230
- }
237
+ clearSearch();
231
238
  }
232
239
  };
233
240
  document.addEventListener('mousedown', handleClickOutside);
234
241
  return () => {
235
242
  document.removeEventListener('mousedown', handleClickOutside);
236
243
  };
237
- }, [isOpen, searchValue, onSearchChange]);
244
+ }, [isOpen, clearSearch]);
238
245
 
239
246
  useEffect(() => {
240
247
  if (!ref) return;
@@ -242,14 +249,17 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
242
249
  focus: () => inputRef.current?.focus(),
243
250
  blur: () => inputRef.current?.blur(),
244
251
  open: () => setIsOpen(true),
245
- close: () => setIsOpen(false),
252
+ close: () => {
253
+ clearSearch();
254
+ setIsOpen(false);
255
+ },
246
256
  };
247
257
  if (typeof ref === 'function') {
248
258
  ref(handle);
249
259
  } else {
250
260
  ref.current = handle;
251
261
  }
252
- }, [ref]);
262
+ }, [ref, clearSearch]);
253
263
 
254
264
  return (
255
265
  <C.SelectContainer
@@ -262,7 +272,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
262
272
  onBlur={() => {
263
273
  setIsFocused(false);
264
274
  if (!isOpen) {
265
- setInternalSearchText('');
275
+ clearSearch();
266
276
  }
267
277
  }}
268
278
  data-status={multiSelectStatus}
@@ -402,7 +412,12 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
402
412
  <C.WrapperIcon
403
413
  onClick={(e) => {
404
414
  e.stopPropagation();
405
- if (!disabled) setIsOpen((prev) => !prev);
415
+ if (!disabled) {
416
+ if (isOpen) {
417
+ clearSearch();
418
+ }
419
+ setIsOpen((prev) => !prev);
420
+ }
406
421
  }}
407
422
  >
408
423
  <Icon
@@ -40,6 +40,11 @@ const meta: Meta<typeof MultiSelect> = {
40
40
  control: false,
41
41
  description: 'Callback fired when the search input changes.',
42
42
  },
43
+ clearSearchOnSelect: {
44
+ control: 'boolean',
45
+ description:
46
+ 'Clears the search term after selecting an option. Set to false to keep filtering during multiple selections.',
47
+ },
43
48
  disabled: { description: 'Disables the multiSelect.' },
44
49
  enableSearch: {
45
50
  control: { type: 'boolean' },
@@ -136,6 +136,12 @@ export default Example;`}
136
136
  <InlineCode>onChange</InlineCode>: A callback function that is triggered
137
137
  when the selection changes.
138
138
  </li>
139
+ <li>
140
+ <InlineCode>clearSearchOnSelect</InlineCode>: Clears the search term
141
+ after selecting an option. Defaults to <InlineCode>true</InlineCode>;
142
+ set it to <InlineCode>false</InlineCode> to keep filtering while the
143
+ menu remains open.
144
+ </li>
139
145
  <li>
140
146
  <InlineCode>disabled</InlineCode>: Disables the MultiSelect input,
141
147
  preventing user interaction.
@@ -0,0 +1,63 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+ import { useState } from 'react';
4
+ import MultiSelect from './';
5
+ import { MultiSelectOption } from './types';
6
+
7
+ const options: MultiSelectOption[] = [
8
+ { label: 'North Port', value: 'north-port' },
9
+ { label: 'Portland', value: 'portland' },
10
+ { label: 'South', value: 'south' },
11
+ ];
12
+
13
+ describe('MultiSelect search preservation', () => {
14
+ it('should preserve the search term after selecting an option when clearSearchOnSelect is false', () => {
15
+ const MultiSelectHarness = () => {
16
+ const [values, setValues] = useState<string[]>([]);
17
+
18
+ return (
19
+ <MultiSelect
20
+ options={options}
21
+ values={values}
22
+ onChange={setValues}
23
+ clearSearchOnSelect={false}
24
+ />
25
+ );
26
+ };
27
+
28
+ render(<MultiSelectHarness />);
29
+
30
+ const input = screen.getByTestId('multi-select-input');
31
+ fireEvent.change(input, { target: { value: 'port' } });
32
+ fireEvent.click(screen.getByTestId('north-port'));
33
+
34
+ expect(input).toHaveValue('port');
35
+ expect(
36
+ screen.getByTestId('multi-select-menu-multi-select')
37
+ ).toBeInTheDocument();
38
+ });
39
+
40
+ it('should clear the preserved search term after closing the menu', () => {
41
+ const MultiSelectHarness = () => {
42
+ const [values, setValues] = useState<string[]>([]);
43
+
44
+ return (
45
+ <MultiSelect
46
+ options={options}
47
+ values={values}
48
+ onChange={setValues}
49
+ clearSearchOnSelect={false}
50
+ />
51
+ );
52
+ };
53
+
54
+ render(<MultiSelectHarness />);
55
+
56
+ const input = screen.getByTestId('multi-select-input');
57
+ fireEvent.change(input, { target: { value: 'port' } });
58
+ fireEvent.click(screen.getByTestId('north-port'));
59
+ fireEvent.mouseDown(document.body);
60
+
61
+ expect(input).toHaveValue('');
62
+ });
63
+ });
@@ -20,6 +20,7 @@ type MultiSelectProps = {
20
20
  label?: string | React.ReactNode;
21
21
  onChange: (values: string[]) => void;
22
22
  onSearchChange?: (searchText: string) => void;
23
+ clearSearchOnSelect?: boolean;
23
24
  disabled?: boolean;
24
25
  enableSearch?: boolean;
25
26
  width?: string;