Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(AutoComplete): prevent focus reset to body after selecting a value #327

Merged
merged 1 commit into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,15 +102,14 @@
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[]>([]);

const menuMinHeight = computed<number>(() => {
const renderedOptionsData = get(renderedOptions).slice(0, Math.min(5, get(renderedData).length));

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

View workflow job for this annotation

GitHub Actions / ci

'renderedData' was used before it was defined
return renderedOptionsData.reduce((currentValue, item) => currentValue + item.$el.offsetHeight, 0);
});

Expand Down Expand Up @@ -174,6 +173,8 @@
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 @@
});

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 @@
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 @@
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 @@
set(value, newValue);
}
else {
await nextTick(() => {
set(isOpen, false);
});
if (get(shouldApplyValueAsSearch))
updateInternalSearch(getText(val));
else
Expand All @@ -315,8 +315,21 @@
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 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 @@
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 Expand Up @@ -777,7 +798,7 @@
.activator {
@apply relative inline-flex items-center w-full;
@apply outline-none focus-within:outline-none cursor-pointer min-h-14 pl-3 py-2 pr-8 rounded;
@apply m-0 bg-white transition-all text-body-1 text-left hover:border-black;

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

View workflow job for this annotation

GitHub Actions / ci

File has too many lines (1009). Maximum allowed is 800

&:not(.outlined) {
@apply hover:bg-gray-100 focus-within:bg-gray-100;
Expand Down
Loading