@bbl-digital/snorre 3.0.2 → 3.0.6

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.
@@ -0,0 +1,236 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import { matchSorter } from 'match-sorter';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { useDebounce } from '../../utils/debounce';
5
+
6
+ const useAutocomplete = props => {
7
+ const [showValues, setShowValues] = useState(!!props.isOpen);
8
+ const [highlightedIndex, setHighlightedIndex] = useState(null);
9
+ const [value, setValueChanged] = useState(props.value ?? '');
10
+ const [inputDirty, setInputDirty] = useState(false);
11
+ const debouncedValue = useDebounce(value, props.debounceDelay ?? 0);
12
+ const [inputValues, setInputValues] = useState(props.values);
13
+
14
+ const handleValueClick = value => {
15
+ props.onSelectItem?.(value);
16
+ setShowValues(false);
17
+ };
18
+
19
+ const clearSelectedItem = () => handleValueClick({
20
+ [props.labelFromValues ?? 'label']: '',
21
+ [props.keyFromValues ?? 'key']: ''
22
+ });
23
+
24
+ const onInputChange = e => {
25
+ setInputDirty(true);
26
+ if (inputValues) setHighlightedIndex(0);
27
+ setValueChanged(e.target.value); // Should not use onChange whenever we have fuzzy search on
28
+
29
+ if (props.fuzzy) return;
30
+ if (props.onChange) props.onChange(e);
31
+ };
32
+
33
+ const handleOnInputClick = () => {
34
+ if (inputValues?.length) setHighlightedIndex(0);
35
+ setShowValues(true);
36
+ };
37
+
38
+ const onFuzzyBlur = e => {
39
+ // If the value of the input is changed, and does not match the value of the values array,
40
+ // we should revert back the input value to the original value
41
+ //What
42
+ // if (!e.target.value.length) clearSelectedItem()
43
+ if (e.target.value === props.value) return; // If target value is the same as a value from the values array, we should set the value for the user
44
+
45
+ if (inputValues?.length) {
46
+ const valueInInputValues = inputValues.find(item => item[props.labelFromValues ?? 'label'].length && item[props.labelFromValues ?? 'label'].toLowerCase() === e.target.value.toLowerCase());
47
+
48
+ if (valueInInputValues) {
49
+ const val = valueInInputValues[props.labelFromValues ?? 'label'];
50
+ setValueChanged(val);
51
+ handleValueClick(valueInInputValues);
52
+ return;
53
+ } // Otherwise we should return to the original input value
54
+
55
+
56
+ setValueChanged(props.value ?? '');
57
+ }
58
+ };
59
+
60
+ const renderedValues = useMemo(() => {
61
+ if (!props.values?.length) return [];
62
+
63
+ if (props.fuzzy) {
64
+ const fuzzyOptions = {
65
+ keys: [props.labelFromValues ?? 'label']
66
+ };
67
+ setShowValues(Boolean(value));
68
+ return matchSorter(props.values, value ?? '', fuzzyOptions);
69
+ }
70
+
71
+ return props.values;
72
+ }, [value]);
73
+
74
+ const handleEscape = () => {
75
+ setShowValues(false);
76
+ setHighlightedIndex(null);
77
+ };
78
+
79
+ const handleClear = () => {
80
+ clearSelectedItem();
81
+ handleEscape();
82
+ };
83
+
84
+ const handleEnter = e => {
85
+ e.preventDefault();
86
+ e.stopPropagation();
87
+ if (!showValues) return;
88
+
89
+ if (!inputValues?.length) {
90
+ // If there is no autocomplete value in the list, send object with just { value }
91
+ handleValueClick({
92
+ value
93
+ });
94
+ }
95
+
96
+ if (highlightedIndex === null || !inputValues?.length) {
97
+ return;
98
+ }
99
+
100
+ const item = inputValues[highlightedIndex];
101
+ handleValueClick(item);
102
+ setShowValues(false);
103
+ };
104
+
105
+ const handleUp = e => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ if (!showValues) return;
109
+
110
+ if (highlightedIndex === null || !inputValues?.length) {
111
+ return;
112
+ }
113
+
114
+ const newHighlightIndex = highlightedIndex - 1;
115
+
116
+ if (newHighlightIndex < 0) {
117
+ setHighlightedIndex(null);
118
+ setShowValues(false);
119
+ return;
120
+ }
121
+
122
+ setHighlightedIndex(newHighlightIndex);
123
+ };
124
+
125
+ const handleDown = e => {
126
+ if (!showValues) return;
127
+ e.preventDefault();
128
+ e.stopPropagation();
129
+
130
+ if (highlightedIndex === null || !inputValues?.length) {
131
+ return;
132
+ }
133
+
134
+ const newHighlightIndex = highlightedIndex + 1;
135
+
136
+ if (newHighlightIndex === inputValues.length) {
137
+ setHighlightedIndex(null);
138
+ setShowValues(false);
139
+ return;
140
+ }
141
+
142
+ setHighlightedIndex(newHighlightIndex);
143
+ };
144
+
145
+ const handleCustomOnKeyDown = e => {
146
+ const {
147
+ key
148
+ } = e;
149
+
150
+ if (key === 'Escape') {
151
+ handleEscape();
152
+ return;
153
+ }
154
+
155
+ props.onKeyDown?.(e);
156
+ }; // Makes it possible to navigate using keyboard shortcuts
157
+
158
+
159
+ const handleOnKeyDown = e => {
160
+ const {
161
+ key
162
+ } = e;
163
+
164
+ if (showValues && inputValues?.length) {
165
+ setHighlightedIndex(0);
166
+ } else {
167
+ setHighlightedIndex(null);
168
+ }
169
+
170
+ switch (key) {
171
+ case 'Enter':
172
+ handleEnter(e);
173
+ break;
174
+
175
+ case 'ArrowUp':
176
+ handleUp(e);
177
+ break;
178
+
179
+ case 'ArrowDown':
180
+ case 'Tab':
181
+ handleDown(e);
182
+ break;
183
+
184
+ case 'Escape':
185
+ handleEscape();
186
+ break;
187
+
188
+ default:
189
+ break;
190
+ }
191
+ }; // Handle debounce
192
+
193
+
194
+ useEffect(() => {
195
+ const handleDebounceChange = value => {
196
+ if (props.onDebounceChange && inputDirty) {
197
+ props.onDebounceChange(value);
198
+ }
199
+ };
200
+
201
+ handleDebounceChange(debouncedValue);
202
+ }, [debouncedValue]); // Change local open state if props.isOpen changes
203
+
204
+ useEffect(() => {
205
+ setShowValues(!!props.isOpen);
206
+ }, [props.isOpen]); // Update local values state if props.values changes
207
+
208
+ useEffect(() => {
209
+ setInputValues(props.values);
210
+ }, [props.values]); // Update local value if props.value changes
211
+
212
+ useEffect(() => {
213
+ const value = props.value ?? '';
214
+ setValueChanged(value);
215
+ }, [props.value]);
216
+ useEffect(() => {
217
+ if (!props.openCustomValueInputOnKeyPress) return;
218
+ if (value.length) setShowValues(true);
219
+ }, [value]);
220
+ return {
221
+ setShowValues,
222
+ handleClear,
223
+ handleValueClick,
224
+ onInputChange,
225
+ handleOnKeyDown,
226
+ handleCustomOnKeyDown,
227
+ handleOnInputClick,
228
+ onFuzzyBlur,
229
+ value,
230
+ highlightedIndex,
231
+ showValues,
232
+ renderedValues
233
+ };
234
+ };
235
+
236
+ export default useAutocomplete;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,192 +1,44 @@
1
1
  /** @jsxImportSource @emotion/react */
2
- import React, { createRef, useState } from 'react';
3
- import { styles, ErrorMessage } from './styles';
2
+ import React, { createRef } from 'react';
3
+ import { styles, ErrorMessage, Clear } from './styles';
4
4
  import { useEffect } from 'react';
5
- import { useDebounce } from '../utils/debounce';
6
5
  import IconErrorOutline from '../../icons/General/IconErrorOutline';
7
6
  import Spinner from '../Spinner';
8
7
  import useHandleOptionsHeight from './utils/useHandleOptionsHeight';
9
8
  import Link from '../Link';
9
+ import IconClose from '../../icons/General/IconClose';
10
+ import useAutocomplete from './hooks/useAutocomplete';
10
11
  import { jsx as _jsx } from "@emotion/react/jsx-runtime";
11
12
  import { jsxs as _jsxs } from "@emotion/react/jsx-runtime";
12
13
  import { Fragment as _Fragment } from "@emotion/react/jsx-runtime";
13
14
  const Autocomplete = /*#__PURE__*/React.forwardRef(({
14
- onDebounceChange,
15
- debounceDelay,
16
15
  height,
17
16
  css,
18
17
  ...props
19
18
  }, ref) => {
19
+ const valuesRef = /*#__PURE__*/createRef();
20
20
  const {
21
21
  optionsHeight,
22
22
  optionsRef
23
23
  } = useHandleOptionsHeight();
24
- const valuesRef = /*#__PURE__*/createRef();
25
- const [showValues, setShowValues] = useState(!!props.isOpen);
26
- const [highlightedIndex, setHighlightedIndex] = useState(null);
27
- const [value, setValueChanged] = useState(props.value ? props.value : '');
28
- const [inputDirty, setInputDirty] = useState(false);
29
- const debouncedValue = useDebounce(value, debounceDelay ? debounceDelay : 0);
30
-
31
- const onInputChange = e => {
32
- setInputDirty(true);
33
- setValueChanged(e.target.value);
34
- if (props.values) setHighlightedIndex(0);
35
- if (props.onChange) props.onChange(e);
36
- };
37
-
38
- const handleOnInputClick = () => {
39
- if (props.values?.length) setHighlightedIndex(0);
40
- setShowValues(true);
41
- };
42
-
43
- const handleValueClick = value => {
44
- setShowValues(false);
45
- props.onSelectItem?.(value);
46
- };
47
-
48
- const handleEnter = e => {
49
- e.preventDefault();
50
- e.stopPropagation();
51
- if (!showValues) return;
52
-
53
- if (!props.values?.length) {
54
- // If there is no autocomplete value in the list, send object with just { value }
55
- props.onSelectItem?.({
56
- value
57
- });
58
- }
59
-
60
- if (highlightedIndex === null || !props.values?.length) {
61
- return;
62
- }
63
-
64
- const item = props.values[highlightedIndex];
65
- props.onSelectItem?.(item);
66
- setShowValues(false);
67
- };
68
-
69
- const handleUp = e => {
70
- e.preventDefault();
71
- e.stopPropagation();
72
- if (!showValues) return;
73
-
74
- if (highlightedIndex === null || !props.values?.length) {
75
- return;
76
- }
77
-
78
- const newHighlightIndex = highlightedIndex - 1;
79
-
80
- if (newHighlightIndex < 0) {
81
- setHighlightedIndex(null);
82
- setShowValues(false);
83
- return;
84
- }
85
-
86
- setHighlightedIndex(newHighlightIndex);
87
- };
88
-
89
- const handleDown = e => {
90
- if (!showValues) return;
91
- e.preventDefault();
92
- e.stopPropagation();
93
-
94
- if (highlightedIndex === null || !props.values?.length) {
95
- return;
96
- }
97
-
98
- const newHighlightIndex = highlightedIndex + 1;
99
-
100
- if (newHighlightIndex === props.values.length) {
101
- setHighlightedIndex(null);
102
- setShowValues(false);
103
- return;
104
- }
105
-
106
- setHighlightedIndex(newHighlightIndex);
107
- };
108
-
109
- const handleEscape = () => {
110
- setShowValues(false);
111
- setHighlightedIndex(null);
112
- };
113
-
114
- const handleCustomOnKeyDown = e => {
115
- const {
116
- key
117
- } = e;
118
-
119
- if (key === 'Escape') {
120
- handleEscape();
121
- return;
122
- }
123
-
124
- props.onKeyDown?.(e);
125
- };
126
-
127
- const handleOnKeyDown = e => {
128
- const {
129
- key
130
- } = e;
131
-
132
- if (showValues && props.values?.length) {
133
- setHighlightedIndex(0);
134
- } else {
135
- setHighlightedIndex(null);
136
- }
137
-
138
- switch (key) {
139
- case 'Enter':
140
- handleEnter(e);
141
- break;
142
-
143
- case 'ArrowUp':
144
- handleUp(e);
145
- break;
146
-
147
- case 'ArrowDown':
148
- case 'Tab':
149
- handleDown(e);
150
- break;
151
-
152
- case 'Escape':
153
- handleEscape();
154
- break;
155
-
156
- default:
157
- break;
158
- }
159
- }; // Handle debounce
160
-
161
-
162
- useEffect(() => {
163
- const handleDebounceChange = value => {
164
- if (onDebounceChange && inputDirty) {
165
- onDebounceChange(value);
166
- }
167
- };
168
-
169
- handleDebounceChange(debouncedValue); // eslint-disable-next-line react-hooks/exhaustive-deps
170
- }, [debouncedValue]);
171
- useEffect(() => {
172
- setShowValues(!!props.isOpen);
173
- }, [props.isOpen]);
174
- useEffect(() => {
175
- const value = props.value ? props.value : '';
176
- setValueChanged(value);
177
- }, [props.value]);
178
- useEffect(() => {
179
- if (!props.openCustomValueInputOnKeyPress) return;
180
- if (value.length) setShowValues(true); // eslint-disable-next-line react-hooks/exhaustive-deps
181
- }, [value]);
24
+ const {
25
+ value,
26
+ highlightedIndex,
27
+ showValues,
28
+ renderedValues,
29
+ setShowValues,
30
+ handleClear,
31
+ handleOnInputClick,
32
+ handleCustomOnKeyDown,
33
+ handleOnKeyDown,
34
+ handleValueClick,
35
+ onInputChange,
36
+ onFuzzyBlur
37
+ } = useAutocomplete(props);
182
38
  useEffect(() => {
183
39
  const handleClickOutside = e => {
184
40
  const node = valuesRef.current;
185
-
186
- if (node && node.contains(e.target)) {
187
- return;
188
- }
189
-
41
+ if (node && node.contains(e.target)) return;
190
42
  setShowValues(false);
191
43
  };
192
44
 
@@ -198,7 +50,7 @@ const Autocomplete = /*#__PURE__*/React.forwardRef(({
198
50
 
199
51
  return () => {
200
52
  document.removeEventListener('mousedown', handleClickOutside);
201
- };
53
+ }; // eslint-disable-next-line react-hooks/exhaustive-deps
202
54
  }, [showValues, valuesRef]);
203
55
  return _jsxs(_Fragment, {
204
56
  children: [_jsxs("label", {
@@ -224,7 +76,7 @@ const Autocomplete = /*#__PURE__*/React.forwardRef(({
224
76
  value: value,
225
77
  disabled: props.disabled,
226
78
  autoFocus: props.focus,
227
- onBlur: props.onBlur,
79
+ onBlur: props.fuzzy ? onFuzzyBlur : props.onBlur,
228
80
  onFocus: props.onFocus,
229
81
  onChange: onInputChange,
230
82
  onKeyDown: props.onKeyDown ? handleCustomOnKeyDown : handleOnKeyDown,
@@ -234,6 +86,12 @@ const Autocomplete = /*#__PURE__*/React.forwardRef(({
234
86
  autoComplete: "off",
235
87
  css: theme => [props.disabled && styles.disabled(theme)],
236
88
  children: React.Children.map(props.children, child => child || null)
89
+ }), props.clear && _jsx(Clear, {
90
+ nostyle: true,
91
+ onClick: handleClear,
92
+ children: _jsx(IconClose, {
93
+ size: 14
94
+ })
237
95
  }), props.loading && _jsx(Spinner, {
238
96
  size: 14
239
97
  }), props.actions && _jsx("div", {
@@ -253,17 +111,19 @@ const Autocomplete = /*#__PURE__*/React.forwardRef(({
253
111
  children: props.invalidMessage
254
112
  })]
255
113
  })]
256
- }), (props.values?.length || props.renderCustomValueInput) && !props.loading && showValues && _jsxs("div", {
114
+ }), (Boolean(props.values?.length) || props.renderCustomValueInput) && !props.loading && showValues && _jsxs("div", {
257
115
  ref: valuesRef,
258
116
  css: () => [!props.renderCustomValueInput && styles.listWrapper, props.dynamicallyPlaceInput && Boolean(optionsHeight) && styles.listWrapperTopPosition(optionsHeight), props.dynamicallyPlaceInput && Boolean(optionsHeight) && props.invalidMessage && styles.listWrapperTopPosition(optionsHeight + 30), props.inputValuesMaxWidth && styles.listWrapperMaxWidth(props.inputValuesMaxWidth)],
259
- children: [props.values?.length && _jsx("ul", {
117
+ children: [Boolean(renderedValues?.length) && _jsx("ul", {
260
118
  css: theme => [styles.list(theme)],
261
- children: props.values.map((item, index) => _jsx("li", {
262
- tabIndex: 0,
263
- css: index === highlightedIndex ? styles.highlightedItem : null,
264
- onClick: () => handleValueClick(item),
265
- children: props.labelFromValues ? item[props.labelFromValues] : item.label
266
- }, props.keyFromValues ? item[props.keyFromValues] : item.key))
119
+ children: renderedValues.map((item, index) => {
120
+ return _jsx("li", {
121
+ tabIndex: 0,
122
+ css: index === highlightedIndex ? styles.highlightedItem : null,
123
+ onClick: () => handleValueClick(item),
124
+ children: props.labelFromValues ? item[props.labelFromValues] : item.label
125
+ }, props.keyFromValues ? item[props.keyFromValues] : item.key);
126
+ })
267
127
  }), props.renderCustomValueInput && _jsx("div", {
268
128
  children: props.renderCustomValueInput
269
129
  })]