mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
feat(design-system): improve combobox input behavior
This commit is contained in:
parent
cd74deaf69
commit
a9d28626f5
|
|
@ -141,13 +141,14 @@ export const Items = {
|
|||
},
|
||||
template: `
|
||||
<div style="${storyContainerStyle}">
|
||||
<Combobox :items="args.items" v-model="value" placeholder="Search status..." />
|
||||
<Combobox v-bind="args" v-model="value" placeholder="Search status..." />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
items: statusItems,
|
||||
modelValue: undefined,
|
||||
openOnFocus: true,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
|
|
|
|||
|
|
@ -353,6 +353,19 @@ describe('v2/components/Combobox', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should clear the selection when the input value is deleted', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
items: ['Option 1', 'Option 2'],
|
||||
modelValue: 'Option 1',
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.clear(getComboboxInput(wrapper));
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')).toContainEqual([undefined]);
|
||||
});
|
||||
|
||||
it('should display selected value for string items', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
|
|
@ -449,6 +462,49 @@ describe('v2/components/Combobox', () => {
|
|||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('should open when the input receives focus', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
},
|
||||
});
|
||||
|
||||
getComboboxInput(wrapper).focus();
|
||||
|
||||
await getPopoverContainer();
|
||||
});
|
||||
|
||||
it('should stay closed on focus when openOnFocus is false', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
openOnFocus: false,
|
||||
},
|
||||
});
|
||||
|
||||
getComboboxInput(wrapper).focus();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="listbox"][data-state="open"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should close after selecting an item when opened on focus', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
items: ['Option 1', 'Option 2', 'Option 3'],
|
||||
},
|
||||
});
|
||||
|
||||
getComboboxInput(wrapper).focus();
|
||||
const { popover } = await getPopoverContainer();
|
||||
await userEvent.click(within(popover).getByRole('option', { name: 'Option 1' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="listbox"][data-state="open"]')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit update:open when dropdown opens', async () => {
|
||||
const wrapper = render(Combobox, {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type AcceptableValue = string | Record<string, unknown>;
|
|||
|
||||
export type ComboboxProps = Omit<ComboboxRootProps<AcceptableValue>, 'dir' | 'openOnFocus'> &
|
||||
Pick<ComboboxContentProps, 'side' | 'sideOffset' | 'align'> & {
|
||||
openOnFocus?: boolean;
|
||||
size?: ComboboxSizes;
|
||||
icon?: IconName;
|
||||
placeholder?: string;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@ import {
|
|||
ComboboxViewport,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui';
|
||||
import { computed, nextTick, useCssModule, useTemplateRef } from 'vue';
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useTemplateRef,
|
||||
} from 'vue';
|
||||
|
||||
import Icon from '@n8n/design-system/components/N8nIcon/Icon.vue';
|
||||
import { useI18n } from '@n8n/design-system/composables/useI18n';
|
||||
|
|
@ -49,6 +56,18 @@ const props = withDefaults(defineProps<ComboboxProps>(), {
|
|||
clearable: false,
|
||||
teleported: true,
|
||||
});
|
||||
const attrs = useAttrs();
|
||||
const instance = getCurrentInstance();
|
||||
const openOnFocus = computed(() => {
|
||||
const assignedProps = instance?.vnode.props;
|
||||
const isAssigned =
|
||||
Object.hasOwn(assignedProps ?? {}, 'openOnFocus') ||
|
||||
Object.hasOwn(assignedProps ?? {}, 'open-on-focus');
|
||||
|
||||
if (!isAssigned) return true;
|
||||
|
||||
return (attrs.openOnFocus ?? props.openOnFocus) !== false;
|
||||
});
|
||||
const emit = defineEmits<ComboboxEmits>();
|
||||
const slots = defineSlots<ComboboxSlots>();
|
||||
const { t } = useI18n();
|
||||
|
|
@ -200,6 +219,15 @@ function onClear() {
|
|||
function onTagsUpdate(value: TagsInputValue[]) {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
function onInput(event: Event) {
|
||||
if (props.multiple || !hasValue.value) return;
|
||||
if (!(event.target instanceof HTMLInputElement)) return;
|
||||
|
||||
if (event.target.value === '') {
|
||||
emit('update:modelValue', undefined);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -209,6 +237,7 @@ function onTagsUpdate(value: TagsInputValue[]) {
|
|||
:disabled="props.disabled"
|
||||
:default-value="props.defaultValue"
|
||||
:model-value="props.modelValue"
|
||||
:open-on-focus="openOnFocus"
|
||||
>
|
||||
<ComboboxArrowKeyLoop />
|
||||
<ComboboxAnchor
|
||||
|
|
@ -272,6 +301,7 @@ function onTagsUpdate(value: TagsInputValue[]) {
|
|||
:auto-focus="props.autoFocus"
|
||||
:display-value="getDisplayValue"
|
||||
:aria-label="$attrs['aria-label'] ?? props.placeholder"
|
||||
@input="onInput"
|
||||
/>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ When `multiple` is true, selected values render via embedded `N8nTagsInput2` (sh
|
|||
- `resetSearchTermOnBlur?: boolean` — Reset search text on blur | reka default: `true`
|
||||
- `resetSearchTermOnSelect?: boolean` — Reset search text on select | reka default: `true`
|
||||
- `openOnClick?: boolean` — Open dropdown when input is clicked | reka default: `false`
|
||||
- `openOnFocus?: boolean` — Open dropdown when input receives focus | default: `true`
|
||||
- `highlightOnHover?: boolean` — Highlight items on hover (reka-ui root)
|
||||
- `clearable?: boolean` — When `true`, shows a clear button (×) when a value is selected. Hidden when disabled or empty. Default: `false`
|
||||
- `teleported?: boolean` — Whether to teleport the dropdown to body. Default: `true`
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user