@purpurds/autocomplete 5.29.0 → 5.30.1

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.
@@ -9,8 +9,8 @@ export type Option = {
9
9
  };
10
10
  export type InputProps = Omit<ComponentPropsWithRef<"input">, "onFocus" | "onMouseDown"> & {
11
11
  "data-testid"?: string;
12
- onFocus: () => void;
13
- onMouseDown: () => void;
12
+ onFocus: (event: React.FocusEvent<HTMLInputElement> | undefined) => void;
13
+ onMouseDown: (event: React.MouseEvent<HTMLInputElement> | undefined) => void;
14
14
  };
15
15
  export type UseAutocompleteOptions<T extends Option> = {
16
16
  combobox?: boolean;
@@ -22,14 +22,18 @@ export type UseAutocompleteOptions<T extends Option> = {
22
22
  listboxLabel: string;
23
23
  listboxMaxHeight?: string | number;
24
24
  noOptionsText?: ReactNode;
25
+ onInputBlur?: React.FocusEventHandler<HTMLInputElement>;
25
26
  onInputChange?: (value: string) => void;
27
+ onInputFocus?: (event: React.FocusEvent<HTMLInputElement> | undefined) => undefined;
28
+ onInputKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
29
+ onInputMouseDown?: (event: React.MouseEvent<HTMLInputElement> | undefined) => undefined;
26
30
  openOnFocus?: boolean;
27
31
  onSelect?: (option: T | undefined) => void;
28
32
  options: T[];
29
33
  selectedOption?: T;
30
34
  ["data-testid"]?: string;
31
35
  };
32
- export declare const useAutocomplete: <T extends Option>({ combobox, highlightFirstOption, defaultInputValue, inputValue, filterOption, id, listboxLabel, listboxMaxHeight, onInputChange, openOnFocus, noOptionsText, onSelect, options, selectedOption, ["data-testid"]: dataTestid, }: UseAutocompleteOptions<T>) => {
36
+ export declare const useAutocomplete: <T extends Option>({ combobox, highlightFirstOption, defaultInputValue, inputValue, filterOption, id, listboxLabel, listboxMaxHeight, onInputBlur, onInputChange, onInputFocus, onInputKeyDown, onInputMouseDown, openOnFocus, noOptionsText, onSelect, options, selectedOption, ["data-testid"]: dataTestid, }: UseAutocompleteOptions<T>) => {
33
37
  id: string;
34
38
  inputProps: InputProps;
35
39
  internalRef: import('react').MutableRefObject<HTMLDivElement | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"useAutocomplete.d.ts","sourceRoot":"","sources":["../src/useAutocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIxE,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC,GAAG;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,MAAM,IAAI;IAIrD,QAAQ,CAAC,EAAE,OAAO,CAAC;IAInB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAI/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAI3B,UAAU,CAAC,EAAE,MAAM,CAAC;IAIpB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;IAItE,EAAE,EAAE,MAAM,CAAC;IAIX,YAAY,EAAE,MAAM,CAAC;IAIrB,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAInC,aAAa,CAAC,EAAE,SAAS,CAAC;IAI1B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAIxC,WAAW,CAAC,EAAE,OAAO,CAAC;IAItB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IAI3C,OAAO,EAAE,CAAC,EAAE,CAAC;IAIb,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,mOAgB7C,sBAAsB,CAAC,CAAC,CAAC;;;;;;;;kCAqLW,CAAC,SAAS,MAAM,KAAG,gBAAgB;;CAqDzE,CAAC"}
1
+ {"version":3,"file":"useAutocomplete.d.ts","sourceRoot":"","sources":["../src/useAutocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAIxE,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC,GAAG;IACzF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IACzE,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,MAAM,IAAI;IAIrD,QAAQ,CAAC,EAAE,OAAO,CAAC;IAInB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAI/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAI3B,UAAU,CAAC,EAAE,MAAM,CAAC;IAIpB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;IAItE,EAAE,EAAE,MAAM,CAAC;IAIX,YAAY,EAAE,MAAM,CAAC;IAIrB,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAInC,aAAa,CAAC,EAAE,SAAS,CAAC;IAI1B,WAAW,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;IAIxD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAIxC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,SAAS,KAAK,SAAS,CAAC;IAIpF,cAAc,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAI9D,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,SAAS,KAAK,SAAS,CAAC;IAIxF,WAAW,CAAC,EAAE,OAAO,CAAC;IAItB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IAI3C,OAAO,EAAE,CAAC,EAAE,CAAC;IAIb,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC,aAAa,CAAC,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,gSAoB7C,sBAAsB,CAAC,CAAC,CAAC;;;;;;;;kCAyLW,CAAC,SAAS,MAAM,KAAG,gBAAgB;;CAqDzE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purpurds/autocomplete",
3
- "version": "5.29.0",
3
+ "version": "5.30.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "main": "./dist/autocomplete.cjs.js",
6
6
  "types": "./dist/autocomplete.d.ts",
@@ -15,11 +15,13 @@
15
15
  "source": "src/autocomplete.tsx",
16
16
  "dependencies": {
17
17
  "classnames": "~2.5.0",
18
- "@purpurds/icon": "5.29.0",
19
- "@purpurds/listbox": "5.29.0",
20
- "@purpurds/text-field": "5.29.0",
21
- "@purpurds/tokens": "5.29.0",
22
- "@purpurds/paragraph": "5.29.0"
18
+ "@purpurds/heading": "5.30.1",
19
+ "@purpurds/icon": "5.30.1",
20
+ "@purpurds/listbox": "5.30.1",
21
+ "@purpurds/text-field": "5.30.1",
22
+ "@purpurds/notification": "5.30.1",
23
+ "@purpurds/paragraph": "5.30.1",
24
+ "@purpurds/tokens": "5.30.1"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@rushstack/eslint-patch": "~1.10.0",
@@ -45,10 +47,10 @@
45
47
  "typescript": "^5.6.3",
46
48
  "vite": "5.4.8",
47
49
  "vitest": "^2.1.2",
48
- "@purpurds/button": "5.29.0",
50
+ "@purpurds/icon": "5.30.1",
51
+ "@purpurds/button": "5.30.1",
49
52
  "@purpurds/component-rig": "1.0.0",
50
- "@purpurds/icon": "5.29.0",
51
- "@purpurds/search-field": "5.29.0"
53
+ "@purpurds/search-field": "5.30.1"
52
54
  },
53
55
  "scripts": {
54
56
  "build:dev": "vite",
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { Notification } from "@purpurds/notification";
2
3
  import { Paragraph } from "@purpurds/paragraph";
3
4
  import { SearchField } from "@purpurds/search-field";
4
5
  import { TextField } from "@purpurds/text-field";
@@ -10,6 +11,9 @@ import "@purpurds/button/styles";
10
11
  import "@purpurds/listbox/styles";
11
12
  import "@purpurds/search-field/styles";
12
13
  import "@purpurds/text-field/styles";
14
+ import "@purpurds/notification/styles";
15
+ import "@purpurds/paragraph/styles";
16
+ import "@purpurds/heading/styles";
13
17
  import { Autocomplete } from "./autocomplete";
14
18
 
15
19
  const options = [
@@ -35,6 +39,22 @@ const options = [
35
39
  { id: "19", label: "Strawberry" },
36
40
  ];
37
41
 
42
+ const OnInputEventPitfallNotification = () => (
43
+ <div style={{ marginTop: "var(--purpur-spacing-150)", marginRight: "-200px" }}>
44
+ <Notification status="warning" heading="Pitfall alert">
45
+ <Paragraph>
46
+ To handle input events, <b>always pass handlers to Autocomplete if defined</b> instead of
47
+ directly to the input. Autocomplete internally uses input events, and setting them yourself
48
+ will override the internal functionality.
49
+ </Paragraph>
50
+ <Paragraph style={{ marginTop: "var(--purpur-spacing-150)" }} variant="paragraph-100-bold">
51
+ This goes for <i>onInputChange</i>, <i>onInputKeyDown</i>, <i>onInputFocus</i>,{" "}
52
+ <i>onInputMouseDown</i> and <i>onInputBlur</i>.
53
+ </Paragraph>
54
+ </Notification>
55
+ </div>
56
+ );
57
+
38
58
  const meta = {
39
59
  title: "Inputs/Autocomplete",
40
60
  component: Autocomplete,
@@ -108,20 +128,23 @@ export const WithTextField: Story = {
108
128
  const [{ selectedOption }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
109
129
 
110
130
  return (
111
- <Autocomplete
112
- {...args}
113
- selectedOption={selectedOption}
114
- onSelect={(selectedOption) => updateArgs({ selectedOption })}
115
- renderInput={(inputProps) => (
116
- <TextField
117
- {...inputProps}
118
- label="With Text Field"
119
- id="autocomplete-input"
120
- type="text"
121
- placeholder="Enter a fruit"
122
- />
123
- )}
124
- />
131
+ <>
132
+ <Autocomplete
133
+ {...args}
134
+ selectedOption={selectedOption}
135
+ onSelect={(selectedOption) => updateArgs({ selectedOption })}
136
+ renderInput={(inputProps) => (
137
+ <TextField
138
+ {...inputProps}
139
+ label="With Text Field"
140
+ id="autocomplete-input"
141
+ type="text"
142
+ placeholder="Enter a fruit"
143
+ />
144
+ )}
145
+ />
146
+ <OnInputEventPitfallNotification />
147
+ </>
125
148
  );
126
149
  },
127
150
  tags: ["visual:check"],
@@ -142,7 +165,14 @@ export const WithSearchField: Story = {
142
165
  code: `
143
166
  <Autocomplete
144
167
  selectedOption={selectedOption}
145
- onSelect={(selectedOption) => updateArgs({ selectedOption })}
168
+ /*
169
+ * NOTE! To handle input change events, always use onInputChange on Autocomplete instead of
170
+ * onChange on the input (SearchField). Autocomplete internally uses onChange on the input,
171
+ * and setting it yourself will override this.
172
+ */
173
+ onInputChange={(value) => {
174
+ setInputValue(value);
175
+ }}
146
176
  renderInput={(inputProps) => (
147
177
  <SearchField
148
178
  {...inputProps}
@@ -157,33 +187,36 @@ export const WithSearchField: Story = {
157
187
  const [{ selectedOption }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
158
188
 
159
189
  return (
160
- <Autocomplete
161
- {...args}
162
- selectedOption={selectedOption}
163
- onSelect={(selectedOption) => updateArgs({ selectedOption })}
164
- onInputChange={(value) => {
165
- setInputValue(value);
166
- updateArgs({ selectedOption: undefined });
167
- }}
168
- inputValue={inputValue}
169
- renderInput={(inputProps) => (
170
- <SearchField
171
- {...inputProps}
172
- label="With Search Field"
173
- id="autocomplete-input"
174
- type="text"
175
- onClear={() => {
176
- setInputValue("");
177
- updateArgs({ selectedOption: undefined });
178
- }}
179
- placeholder="Find your fruit"
180
- clearButtonAllyLabel="Clear search field"
181
- variant="button-attached"
182
- iconOnlySearchButton
183
- searchButtonLabel="Search"
184
- />
185
- )}
186
- />
190
+ <>
191
+ <Autocomplete
192
+ {...args}
193
+ selectedOption={selectedOption}
194
+ onSelect={(selectedOption) => updateArgs({ selectedOption })}
195
+ onInputChange={(value) => {
196
+ setInputValue(value);
197
+ updateArgs({ selectedOption: undefined });
198
+ }}
199
+ inputValue={inputValue}
200
+ renderInput={(inputProps) => (
201
+ <SearchField
202
+ {...inputProps}
203
+ label="With Search Field"
204
+ id="autocomplete-input"
205
+ type="text"
206
+ onClear={() => {
207
+ setInputValue("");
208
+ updateArgs({ selectedOption: undefined });
209
+ }}
210
+ placeholder="Find your fruit"
211
+ clearButtonAllyLabel="Clear search field"
212
+ variant="button-attached"
213
+ iconOnlySearchButton
214
+ searchButtonLabel="Search"
215
+ />
216
+ )}
217
+ />
218
+ <OnInputEventPitfallNotification />
219
+ </>
187
220
  );
188
221
  },
189
222
  tags: ["visual:check"],
@@ -643,4 +643,27 @@ describe("B2xAutocomplete", () => {
643
643
  expect(input.value).toEqual(options[0].label);
644
644
  });
645
645
  });
646
+
647
+ it.each([["focus" as const], ["blur" as const], ["keyDown" as const], ["mouseDown" as const]])(
648
+ "should invoke onInput correctly: %s",
649
+ async (eventName) => {
650
+ const onInputEventMock = vi.fn();
651
+ const onInputEventPropName = `onInput${eventName[0].toUpperCase()}${eventName.slice(1)}`;
652
+ render(
653
+ <Autocomplete
654
+ options={options}
655
+ id="test-id"
656
+ data-testid="test-id"
657
+ listboxLabel="Options"
658
+ renderInput={(props) => <input {...props} data-testid="autocomplete-input" />}
659
+ {...{ [onInputEventPropName]: onInputEventMock }}
660
+ />
661
+ );
662
+
663
+ const input: HTMLInputElement = screen.getByTestId("autocomplete-input");
664
+ fireEvent[eventName](input);
665
+
666
+ expect(onInputEventMock).toBeCalledTimes(1);
667
+ }
668
+ );
646
669
  });
@@ -76,8 +76,8 @@ const AutocompleteComponent = <T extends Option>(
76
76
  type="button"
77
77
  className={cx(`${rootClassName}__chevron-button`)}
78
78
  onClick={() => {
79
- inputProps.onFocus();
80
- inputProps.onMouseDown();
79
+ inputProps.onFocus(undefined);
80
+ inputProps.onMouseDown(undefined);
81
81
  }}
82
82
  aria-hidden="true"
83
83
  >
@@ -13,8 +13,8 @@ export type Option = {
13
13
 
14
14
  export type InputProps = Omit<ComponentPropsWithRef<"input">, "onFocus" | "onMouseDown"> & {
15
15
  "data-testid"?: string;
16
- onFocus: () => void;
17
- onMouseDown: () => void;
16
+ onFocus: (event: React.FocusEvent<HTMLInputElement> | undefined) => void;
17
+ onMouseDown: (event: React.MouseEvent<HTMLInputElement> | undefined) => void;
18
18
  };
19
19
 
20
20
  export type UseAutocompleteOptions<T extends Option> = {
@@ -55,9 +55,25 @@ export type UseAutocompleteOptions<T extends Option> = {
55
55
  */
56
56
  noOptionsText?: ReactNode;
57
57
  /*
58
- * Event handler invoked when the input value changes.
58
+ * NOTE! Always use this instead of onBlur directly on the input element. Event handler invoked on input blur.
59
+ */
60
+ onInputBlur?: React.FocusEventHandler<HTMLInputElement>;
61
+ /*
62
+ * NOTE! Always use this instead of onChange directly on the input element. Event handler invoked when the input value changes.
59
63
  */
60
64
  onInputChange?: (value: string) => void;
65
+ /*
66
+ * NOTE! Always use this instead of onFocus directly on the input element. Event handler invoked on input focus.
67
+ */
68
+ onInputFocus?: (event: React.FocusEvent<HTMLInputElement> | undefined) => undefined;
69
+ /*
70
+ * NOTE! Always use this instead of onKeyDown directly on the input element. Event handler invoked on input key down.
71
+ */
72
+ onInputKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
73
+ /*
74
+ * NOTE! Always use this instead of onMouseDown directly on the input element. Event handler invoked on input mouse down.
75
+ */
76
+ onInputMouseDown?: (event: React.MouseEvent<HTMLInputElement> | undefined) => undefined;
61
77
  /*
62
78
  * Set to open the listbox when input gets focus.
63
79
  */
@@ -86,7 +102,11 @@ export const useAutocomplete = <T extends Option>({
86
102
  id,
87
103
  listboxLabel,
88
104
  listboxMaxHeight,
105
+ onInputBlur,
89
106
  onInputChange,
107
+ onInputFocus,
108
+ onInputKeyDown,
109
+ onInputMouseDown,
90
110
  openOnFocus,
91
111
  noOptionsText,
92
112
  onSelect,
@@ -204,12 +224,13 @@ export const useAutocomplete = <T extends Option>({
204
224
  nextOption && scrollOptionIntoView(optionRefs.current[nextOption.id]);
205
225
  };
206
226
 
207
- const handleOnKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
208
- switch (e.key) {
227
+ const handleOnKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
228
+ onInputKeyDown?.(event);
229
+ switch (event.key) {
209
230
  case "ArrowUp":
210
231
  case "ArrowDown":
211
- e.preventDefault(); // Preventing default to not move cursor in input
212
- highlightNextOption(e.key);
232
+ event.preventDefault(); // Preventing default to not move cursor in input
233
+ highlightNextOption(event.key);
213
234
  break;
214
235
  case "Enter": {
215
236
  const optionToSelect = highlightedOption || (highlightFirstOption ? options[0] : undefined);
@@ -234,16 +255,19 @@ export const useAutocomplete = <T extends Option>({
234
255
  scrollOptionIntoView(optionRefs.current[nextOptionsToShow[0].id]);
235
256
  };
236
257
 
237
- const handleOnMouseDown = () => {
258
+ const handleOnMouseDown = (event: React.MouseEvent<HTMLInputElement> | undefined) => {
259
+ onInputMouseDown?.(event);
238
260
  showListbox ? closeListbox() : openListbox({ eventType: "CLICK" });
239
261
  };
240
262
 
241
- const handleOnFocus = () => {
263
+ const handleOnFocus = (event: React.FocusEvent<HTMLInputElement> | undefined) => {
264
+ onInputFocus?.(event);
242
265
  !listboxIsOpen && openOnFocus && openListbox({ eventType: "KEYBOARD" });
243
266
  inputRef.current?.select();
244
267
  };
245
268
 
246
- const handleOnBlur = () => {
269
+ const handleOnBlur: React.FocusEventHandler<HTMLInputElement> = (event) => {
270
+ onInputBlur?.(event);
247
271
  if (combobox) {
248
272
  // Use a timeout to allow click events on the listbox to fire before this check
249
273
  setTimeout(() => {