Skip to content

Commit

Permalink
fix(AutoComplete): prevent focus reset to body after selecting a value
Browse files Browse the repository at this point in the history
  • Loading branch information
lukicenturi committed Jan 23, 2025
1 parent 8806c00 commit dafafaa
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 24 deletions.
28 changes: 16 additions & 12 deletions src/components/forms/auto-complete/RuiAutoComplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ describe('autocomplete', () => {
await vi.delay();
await nextTick();
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([selectedIndex.toString()]);
expect(document.activeElement?.classList.contains('group')).toBe(true);

await wrapper.setProps({
modelValue: selectedIndex.toString(),
Expand Down Expand Up @@ -154,6 +155,15 @@ describe('autocomplete', () => {
expect(wrapper.emitted('update:modelValue')?.[1]).toEqual([newSelectedIndexToString]);
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('Greece');

await vi.delay();
expect(document.body.querySelector('div[role=menu]')).toBeFalsy();

expect((wrapper.find('input').element as HTMLInputElement).value).toBe('Greece');

// Open menu again
await wrapper.find('[data-id=activator]').trigger('click');
await vi.delay();

// Delete option should also remove selected value with that option
const newOptions = options.filter(item => item.id !== newSelectedIndexToString);

Expand All @@ -163,21 +173,15 @@ describe('autocomplete', () => {
});
await nextTick();

// Even if the options changed, the search value should not be touch as long as the focus still there, so the UX is not breaking
// Even if the options changed, the search value should not be touch as long as the menu is still opened, so the UX is not breaking
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('Greece');

// Still not supposed to change the search value
const menu = document.body.querySelector('div[role=menu]') as HTMLDivElement;
menu.focus();
await nextTick();
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('Greece');
// Only after the menu is closed, the search value can be reset
await wrapper.find('[data-id=activator]').trigger('keydown.esc');
await vi.delay();

expect(document.body.querySelector('div[role=menu]')).toBeFalsy();

// Only after nothing is focused anymore, the search value can be reset
menu.blur();
(wrapper.find('input').element as HTMLInputElement).blur();
await nextTick();
await vi.delay(100);
expect(document.activeElement).toBe(document.body);
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('');

// doesn't break when use chips
Expand Down
45 changes: 33 additions & 12 deletions src/components/forms/auto-complete/RuiAutoComplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,9 @@ const menuWrapperRef = ref();
const { focused: activatorFocusedWithin } = useFocusWithin(activator);
const { focused: menuWrapperFocusedWithin } = useFocusWithin(menuWrapperRef);
const { focused: searchInputFocused } = useFocus(textInput);
const { focused: activatorFocused } = useFocus(activator);
const anyFocused = logicOr(activatorFocusedWithin, menuWrapperFocusedWithin);
const debouncedAnyFocused = refDebounced(anyFocused, 100);
const recentlyFocused = logicOr(debouncedAnyFocused, anyFocused);
const renderedOptions = ref<ComponentPublicInstance[]>([]);
Expand Down Expand Up @@ -174,6 +173,8 @@ const value = computed<TItem[]>({
const keyAttr = props.keyAttr;
const multiple = Array.isArray(value);
const shouldUpdateInternalSearch = get(shouldApplyValueAsSearch) && !get(isOpen);

Check warning on line 176 in src/components/forms/auto-complete/RuiAutoComplete.vue

View workflow job for this annotation

GitHub Actions / ci

'isOpen' was used before it was defined
if (keyAttr && props.returnObject) {
const valueToArray = value ? (multiple ? value : [value]) : [];
const filtered: TItem[] = [];
Expand All @@ -187,13 +188,13 @@ const value = computed<TItem[]>({
});
if (multiple || filtered.length === 0) {
if (get(shouldApplyValueAsSearch) && !get(recentlyFocused))
if (shouldUpdateInternalSearch)
updateInternalSearch();
return filtered;
}
else {
const val = filtered[0];
if (get(shouldApplyValueAsSearch) && !get(recentlyFocused))
if (shouldUpdateInternalSearch)
updateInternalSearch(getText(val));
return [val];
Expand All @@ -211,7 +212,7 @@ const value = computed<TItem[]>({
return filtered.push(textValueToProperValue(val, props.returnObject));
});
if (get(shouldApplyValueAsSearch) && !get(recentlyFocused)) {
if (shouldUpdateInternalSearch) {
if (filtered.length > 0)
updateInternalSearch(getText(filtered[0]));
else
Expand Down Expand Up @@ -292,7 +293,9 @@ async function setValue(val: TItem, index?: number, skipRefocused = false): Prom
if (isDefined(index))
set(highlightedIndex, index);
if (get(multiple)) {
const isMultiple = get(multiple);
if (isMultiple) {
const newValue = [...get(value)];
const indexInValue = itemIndexInValue(val);
if (indexInValue === -1) {
Expand All @@ -304,9 +307,6 @@ async function setValue(val: TItem, index?: number, skipRefocused = false): Prom
set(value, newValue);
}
else {
await nextTick(() => {
set(isOpen, false);
});
if (get(shouldApplyValueAsSearch))
updateInternalSearch(getText(val));
else
Expand All @@ -315,8 +315,21 @@ async function setValue(val: TItem, index?: number, skipRefocused = false): Prom
set(value, [val]);
}
if (!skipRefocused && get(multiple))
if (!isMultiple) {
if (!skipRefocused) {
set(activatorFocused, true);
get(activator)?.focus();
await nextTick(() => {
set(isOpen, false);
});
}
else {
set(isOpen, false);
}
}
else if (!skipRefocused) {
set(searchInputFocused, true);
}
}
async function setInputFocus(): Promise<void> {
Expand All @@ -325,14 +338,22 @@ async function setInputFocus(): Promise<void> {
});
}
async function onActivatorFocused() {
await nextTick(() => {
if (!get(activatorFocused)) {
set(searchInputFocused, true);
}
});
}
const focusedValueIndex = ref<number>(-1);
function setValueFocus(index: number): void {
set(focusedValueIndex, index);
}
watch(value, () => {
set(focusedValueIndex, -1);
setValueFocus(-1);
});
watch(focusedValueIndex, async (index) => {
Expand Down Expand Up @@ -564,7 +585,7 @@ defineExpose({
data-id="activator"
:tabindex="disabled || readOnly ? -1 : 0"
@click="setInputFocus()"
@focus="setInputFocus()"
@focus="onActivatorFocused()"
@keydown.enter="onEnter($event)"
@keydown.tab="onTab($event)"
@keydown.left="moveSelectedValueHighlight($event, false)"
Expand Down

0 comments on commit dafafaa

Please sign in to comment.