@skyscanner/backpack-web 40.2.1 → 40.3.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.
|
@@ -79,8 +79,23 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
79
79
|
const originalInputOnPreviewRef = useRef(null);
|
|
80
80
|
const hasInteractedRef = useRef(false);
|
|
81
81
|
const hasLoadedInitiallyRef = useRef(false);
|
|
82
|
+
const committedSelectionRef = useRef(false);
|
|
83
|
+
const savedHighlightedIndexRef = useRef(null);
|
|
82
84
|
const suggestionsCount = suggestions.length;
|
|
83
85
|
const hasSuggestions = suggestionsCount > 0;
|
|
86
|
+
const flattenedSuggestions = multiSection ? suggestions.flatMap(section => getSectionSuggestions?.(section) ?? []) : suggestions;
|
|
87
|
+
const getTargetHighlightedIndex = (currentHighlightedIndex, isMenuOpening, isArrowKeyNavigation = false) => {
|
|
88
|
+
if (isMenuOpening && !isArrowKeyNavigation && highlightFirstSuggestion && flattenedSuggestions.length > 0) {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
if (currentHighlightedIndex !== null && currentHighlightedIndex !== undefined && currentHighlightedIndex >= 0) {
|
|
92
|
+
return currentHighlightedIndex;
|
|
93
|
+
}
|
|
94
|
+
if (savedHighlightedIndexRef.current !== null && savedHighlightedIndexRef.current >= 0) {
|
|
95
|
+
return savedHighlightedIndexRef.current;
|
|
96
|
+
}
|
|
97
|
+
return currentHighlightedIndex;
|
|
98
|
+
};
|
|
84
99
|
function stateReducer(state, actionAndChanges) {
|
|
85
100
|
const {
|
|
86
101
|
changes,
|
|
@@ -93,23 +108,30 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
93
108
|
isOpen: true
|
|
94
109
|
};
|
|
95
110
|
}
|
|
111
|
+
const isMenuOpening = changes.isOpen === true && state.isOpen === false;
|
|
96
112
|
switch (type) {
|
|
97
113
|
case useCombobox.stateChangeTypes.InputClick:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
{
|
|
115
|
+
const targetHighlightedIndex = getTargetHighlightedIndex(state.highlightedIndex, isMenuOpening);
|
|
116
|
+
return {
|
|
117
|
+
...changes,
|
|
118
|
+
isOpen: state.isOpen,
|
|
119
|
+
highlightedIndex: targetHighlightedIndex ?? -1
|
|
120
|
+
};
|
|
121
|
+
}
|
|
102
122
|
default:
|
|
103
123
|
{
|
|
104
124
|
const forceOpen = !isDesktop && !!changes.inputValue;
|
|
125
|
+
const isArrowKeyNavigation = type === useCombobox.stateChangeTypes.InputKeyDownArrowDown || type === useCombobox.stateChangeTypes.InputKeyDownArrowUp;
|
|
126
|
+
const targetHighlightedIndex = getTargetHighlightedIndex(changes.highlightedIndex, isMenuOpening, isArrowKeyNavigation);
|
|
105
127
|
return {
|
|
106
128
|
...changes,
|
|
107
|
-
isOpen: forceOpen ? true : changes.isOpen
|
|
129
|
+
isOpen: forceOpen ? true : changes.isOpen,
|
|
130
|
+
highlightedIndex: targetHighlightedIndex ?? changes.highlightedIndex
|
|
108
131
|
};
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
}
|
|
112
|
-
const flattenedSuggestions = multiSection ? suggestions.flatMap(section => getSectionSuggestions?.(section) ?? []) : suggestions;
|
|
113
135
|
const {
|
|
114
136
|
getInputProps,
|
|
115
137
|
getItemProps,
|
|
@@ -119,6 +141,7 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
119
141
|
inputValue,
|
|
120
142
|
isOpen,
|
|
121
143
|
openMenu,
|
|
144
|
+
selectItem,
|
|
122
145
|
setInputValue
|
|
123
146
|
} = useCombobox({
|
|
124
147
|
stateReducer,
|
|
@@ -137,11 +160,13 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
137
160
|
newValue: newInputValue ?? ''
|
|
138
161
|
});
|
|
139
162
|
if (type === useCombobox.stateChangeTypes.InputChange) {
|
|
140
|
-
if (newInputValue
|
|
163
|
+
if (newInputValue && newInputValue.length > 0) {
|
|
141
164
|
if (newIsOpen) {
|
|
142
165
|
onSuggestionsFetchRequested(newInputValue);
|
|
143
166
|
}
|
|
144
167
|
} else {
|
|
168
|
+
// Clear old suggestions before requesting defaults
|
|
169
|
+
onSuggestionsClearRequested?.();
|
|
145
170
|
onSuggestionsFetchRequested('');
|
|
146
171
|
}
|
|
147
172
|
}
|
|
@@ -151,11 +176,14 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
151
176
|
selectedItem
|
|
152
177
|
} = changes;
|
|
153
178
|
if (selectedItem) {
|
|
154
|
-
|
|
179
|
+
const newValue = getSuggestionValue(selectedItem);
|
|
180
|
+
setInputValue(newValue);
|
|
155
181
|
originalInputOnPreviewRef.current = null;
|
|
182
|
+
committedSelectionRef.current = true;
|
|
183
|
+
hasInteractedRef.current = true;
|
|
156
184
|
onSuggestionSelected?.({
|
|
157
185
|
suggestion: selectedItem,
|
|
158
|
-
inputValue
|
|
186
|
+
inputValue: newValue
|
|
159
187
|
});
|
|
160
188
|
if (alwaysRenderSuggestions) {
|
|
161
189
|
// Manually clear suggestions or hide menu
|
|
@@ -222,11 +250,11 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
222
250
|
whileElementsMounted: isDesktop ? autoUpdate : undefined
|
|
223
251
|
});
|
|
224
252
|
useEffect(() => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
setInputValue('');
|
|
253
|
+
// Prevent defaultValue from overwriting input after interaction or selection
|
|
254
|
+
if (hasInteractedRef.current || committedSelectionRef.current) {
|
|
255
|
+
return;
|
|
229
256
|
}
|
|
257
|
+
setInputValue(defaultValue ?? '');
|
|
230
258
|
}, [defaultValue, setInputValue]);
|
|
231
259
|
useEffect(() => {
|
|
232
260
|
if (!isDesktop) {
|
|
@@ -244,6 +272,8 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
244
272
|
useEffect(() => {
|
|
245
273
|
if (highlightedIndex === previousHighlightedIndexRef.current) return;
|
|
246
274
|
previousHighlightedIndexRef.current = highlightedIndex;
|
|
275
|
+
// Save highlighted index to allow auto-selection on blur
|
|
276
|
+
savedHighlightedIndexRef.current = highlightedIndex;
|
|
247
277
|
const currentSuggestion = highlightedIndex != null && highlightedIndex >= 0 ? flattenedSuggestions?.[highlightedIndex] ?? null : null;
|
|
248
278
|
if (!currentSuggestion && originalInputOnPreviewRef.current !== null) {
|
|
249
279
|
if ((inputValue ?? '') !== originalInputOnPreviewRef.current) {
|
|
@@ -256,22 +286,40 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
256
286
|
});
|
|
257
287
|
}, [highlightedIndex, flattenedSuggestions, onSuggestionHighlighted, inputValue, setInputValue]);
|
|
258
288
|
const handleInputInteraction = () => {
|
|
289
|
+
if (isOpen) {
|
|
290
|
+
if (originalInputOnPreviewRef.current !== null) {
|
|
291
|
+
originalInputOnPreviewRef.current = null;
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
259
295
|
hasInteractedRef.current = true;
|
|
296
|
+
// Reset committed selection to allow auto-select on blur
|
|
297
|
+
committedSelectionRef.current = false;
|
|
298
|
+
|
|
299
|
+
// Keep the preview value (from arrow keys) when clicking the input
|
|
300
|
+
if (originalInputOnPreviewRef.current !== null) {
|
|
301
|
+
originalInputOnPreviewRef.current = null;
|
|
302
|
+
}
|
|
303
|
+
onSuggestionsClearRequested?.();
|
|
260
304
|
if (shouldRenderSuggestions) {
|
|
261
305
|
shouldRenderSuggestions(inputValue);
|
|
262
|
-
openMenu();
|
|
263
306
|
}
|
|
264
307
|
if (!isOpen && inputValue.length) {
|
|
265
308
|
onSuggestionsFetchRequested(inputValue);
|
|
266
309
|
openMenu();
|
|
267
310
|
} else if (alwaysRenderSuggestions && !inputValue) {
|
|
268
311
|
onSuggestionsFetchRequested('');
|
|
312
|
+
if (!isOpen) {
|
|
313
|
+
openMenu();
|
|
314
|
+
}
|
|
315
|
+
} else if (!isOpen) {
|
|
316
|
+
openMenu();
|
|
317
|
+
onClick?.();
|
|
269
318
|
} else {
|
|
270
319
|
onClick?.();
|
|
271
320
|
}
|
|
272
321
|
|
|
273
|
-
//
|
|
274
|
-
// Every other use case is within a new screen or modal so is interacted with via the user navigating into the modal/new screen
|
|
322
|
+
// Track desktop homepage interaction (mobile interaction is tracked via modal entry)
|
|
275
323
|
if (isDesktop) {
|
|
276
324
|
onLoad?.(inputValue);
|
|
277
325
|
}
|
|
@@ -284,9 +332,13 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
284
332
|
const clearSuggestions = e => {
|
|
285
333
|
e?.stopPropagation();
|
|
286
334
|
setInputValue('');
|
|
335
|
+
onSuggestionsClearRequested?.();
|
|
336
|
+
onSuggestionsFetchRequested('');
|
|
337
|
+
if (!isOpen) {
|
|
338
|
+
openMenu();
|
|
339
|
+
}
|
|
287
340
|
if (alwaysRenderSuggestions) {
|
|
288
341
|
hasLoadedInitiallyRef.current = true;
|
|
289
|
-
onSuggestionsFetchRequested('');
|
|
290
342
|
}
|
|
291
343
|
};
|
|
292
344
|
|
|
@@ -302,6 +354,11 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
302
354
|
const isFirst = globalIndex === 0;
|
|
303
355
|
const itemId = sectionId ? `item-${sectionIndex}-${localIndex}` : undefined;
|
|
304
356
|
const isHighlighted = highlightedIndex === globalIndex || highlightFirstSuggestion && isFirst && highlightedIndex === -1;
|
|
357
|
+
|
|
358
|
+
// Build stable unique key (prefer entityId/id, fallback to index)
|
|
359
|
+
const suggestionItem = suggestion;
|
|
360
|
+
const suggestionId = suggestionItem.entityId ?? suggestionItem.id ?? globalIndex;
|
|
361
|
+
const suggestionKey = sectionId ? `${sectionId}-${suggestionId}` : `item-${suggestionId}`;
|
|
305
362
|
return /*#__PURE__*/_jsx("li", {
|
|
306
363
|
"aria-labelledby": sectionId && itemId ? `${sectionId} ${itemId}` : undefined,
|
|
307
364
|
...getItemProps({
|
|
@@ -315,7 +372,7 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
315
372
|
id: itemId,
|
|
316
373
|
children: renderSuggestion(suggestion)
|
|
317
374
|
}) : renderSuggestion(suggestion)
|
|
318
|
-
},
|
|
375
|
+
}, suggestionKey);
|
|
319
376
|
});
|
|
320
377
|
|
|
321
378
|
// renderSections function to render multi-section suggestions
|
|
@@ -366,6 +423,7 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
366
423
|
...restInputProps
|
|
367
424
|
} = inputProps;
|
|
368
425
|
const {
|
|
426
|
+
onBlur: downshiftOnBlur,
|
|
369
427
|
ref: downshiftInputRef,
|
|
370
428
|
value,
|
|
371
429
|
...finalInputProps
|
|
@@ -377,7 +435,40 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
377
435
|
'aria-label': inputAriaLabel,
|
|
378
436
|
className: inputClassName || theme.input,
|
|
379
437
|
...restInputProps
|
|
438
|
+
}, {
|
|
439
|
+
// Suppress the warning because we manually handle the ref
|
|
440
|
+
suppressRefError: true
|
|
380
441
|
});
|
|
442
|
+
const handleBlur = e => {
|
|
443
|
+
// Call Downshift's onBlur first
|
|
444
|
+
downshiftOnBlur?.(e);
|
|
445
|
+
// Call original onBlur if provided
|
|
446
|
+
restInputProps.onBlur?.(e);
|
|
447
|
+
if (!isDesktop) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (committedSelectionRef.current) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Auto-select the highlighted suggestion on blur using the saved index
|
|
455
|
+
const savedIndex = savedHighlightedIndexRef.current;
|
|
456
|
+
let highlightedSuggestion = null;
|
|
457
|
+
if (savedIndex != null && savedIndex >= 0 && flattenedSuggestions[savedIndex]) {
|
|
458
|
+
// User highlighted a specific suggestion with arrow keys
|
|
459
|
+
highlightedSuggestion = flattenedSuggestions[savedIndex];
|
|
460
|
+
} else if (highlightFirstSuggestion && flattenedSuggestions.length > 0 && (savedIndex === -1 || savedIndex === null)) {
|
|
461
|
+
// First suggestion is highlighted by default (highlightFirstSuggestion is true)
|
|
462
|
+
const [firstSuggestion] = flattenedSuggestions;
|
|
463
|
+
highlightedSuggestion = firstSuggestion;
|
|
464
|
+
}
|
|
465
|
+
if (highlightedSuggestion) {
|
|
466
|
+
// Use setTimeout to ensure selectItem runs after the blur event completes
|
|
467
|
+
setTimeout(() => {
|
|
468
|
+
selectItem(highlightedSuggestion);
|
|
469
|
+
}, 0);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
381
472
|
const setInputRef = node => {
|
|
382
473
|
if (refs.reference?.current === node) return;
|
|
383
474
|
|
|
@@ -390,10 +481,12 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
390
481
|
};
|
|
391
482
|
if (renderInputComponent) {
|
|
392
483
|
return renderInputComponent({
|
|
484
|
+
...finalInputProps,
|
|
393
485
|
ref: setInputRef,
|
|
394
486
|
enterKeyHint,
|
|
395
|
-
|
|
396
|
-
|
|
487
|
+
onBlur: handleBlur,
|
|
488
|
+
onClear: clearSuggestions,
|
|
489
|
+
value: inputValue ?? ''
|
|
397
490
|
});
|
|
398
491
|
}
|
|
399
492
|
return /*#__PURE__*/_jsx("div", {
|
|
@@ -418,6 +511,7 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
|
|
|
418
511
|
name: inputName || id,
|
|
419
512
|
id: id,
|
|
420
513
|
...finalInputProps,
|
|
514
|
+
onBlur: handleBlur,
|
|
421
515
|
enterKeyHint: enterKeyHint,
|
|
422
516
|
onClear: clearSuggestions
|
|
423
517
|
})
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
|
-
.bpk-table{width:100%;margin-bottom:.5rem;border-collapse:collapse;table-layout:fixed;box-shadow:0 0 0 1px #c1c7cf}.bpk-table__cell{padding:1rem}.bpk-table__cell--head{background-color:#eff3f8;text-align:left;font-size:1rem;line-height:1.5rem;font-weight:700}html[dir=rtl] .bpk-table__cell--head{text-align:right}.bpk-table__cell--wordBreak{white-space:normal;
|
|
18
|
+
.bpk-table{width:100%;margin-bottom:.5rem;border-collapse:collapse;table-layout:fixed;box-shadow:0 0 0 1px #c1c7cf}.bpk-table__cell{padding:1rem}.bpk-table__cell--head{background-color:#eff3f8;text-align:left;font-size:1rem;line-height:1.5rem;font-weight:700}html[dir=rtl] .bpk-table__cell--head{text-align:right}.bpk-table__cell--wordBreak{white-space:normal;overflow-wrap:break-word}
|