Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Commit

Permalink
refactor(AutoComplete): sync drop down menu with vue3 library
Browse files Browse the repository at this point in the history
  • Loading branch information
kelsos committed May 22, 2024
1 parent 0da45c4 commit 2b91381
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 98 deletions.
2 changes: 0 additions & 2 deletions src/components/forms/auto-complete/RuiAutoComplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ describe('autocomplete', () => {
it('works with primitive options', () => {
const wrapper = createWrapper({
propsData: {
keyAttr: 'id',
options: options.map(item => item.label),
textAttr: 'label',
value: options[4].label,
},
});
Expand Down
75 changes: 34 additions & 41 deletions src/components/forms/auto-complete/RuiAutoComplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Ref } from 'vue';
export type T = any;
export type K = Extract<keyof T, string>;
export type K = string;
export type ModelValue<MV> = MV | MV[] | null;
Expand Down Expand Up @@ -73,17 +73,15 @@ const emit = defineEmits<{
const css = useCssModule();
const attrs = useAttrs();
const { dense, variant, disabled } = toRefs(props);
const multiple = computed(() => Array.isArray(props.value));
const { dense, variant, disabled, options } = toRefs(props);
const textInput = ref();
const { focused: searchInputFocused } = useFocus(textInput);
const activator = ref();
const menuRef = ref();
const isPrimitiveOptions = computed(() => !(props.options[0] instanceof Object));
const multiple = computed(() => Array.isArray(props.value));
const keyProp = computed<K>(() => props.keyAttr ?? 'key' as K);
const textProp = computed<K>(() => props.textAttr ?? 'label' as K);
const { focused: searchInputFocused } = useFocus(textInput);
const internalSearch: Ref<string> = ref('');
const debouncedInternalSearch = refDebounced(internalSearch, 200);
Expand All @@ -96,28 +94,20 @@ watchImmediate(searchInputModel, (search) => {
set(internalSearch, search);
});
const mappedOptions = computed<(T extends string ? T : Record<K, T>)[]>(() => {
const filtered = props.options;
if (!get(isPrimitiveOptions))
return filtered;
return filtered.map(item => ({
[get(keyProp)]: item,
[get(textProp)]: item,
}));
});
const filteredOptions = computed(() => {
const search = get(debouncedInternalSearch);
const optionsVal = get(mappedOptions);
const optionsVal = get(options);
if (props.noFilter || !search)
return optionsVal;
const keyAttr = props.keyAttr;
const textAttr = props.textAttr;
const usedFilter = props.filter || ((item, search) => {
const keywords = [item[get(keyProp)]];
const keywords = [keyAttr ? item[keyAttr] : item.toString()];
if (!get(isPrimitiveOptions))
keywords.push(item[get(textProp)]);
if (textAttr && typeof item === 'object')
keywords.push(item[textAttr]);
return keywords.some(keyword => getTextToken(keyword).includes(getTextToken(search)));
});
Expand All @@ -132,15 +122,17 @@ function input(value: ModelValue<T>) {
const value = computed<(T extends string ? T : Record<K, T>)[]>({
get: () => {
const value = props.value;
const keyAttr = props.keyAttr;
const valueToArray = value ? (Array.isArray(value) ? value : [value]) : [];
if (props.keyAttr || get(isPrimitiveOptions))
return get(mappedOptions).filter(item => valueToArray.includes(item[get(keyProp)]));
if (keyAttr)
return get(options).filter(item => valueToArray.includes(item[keyAttr]));
return valueToArray;
},
set: (selected: T[]) => {
const selection = props.keyAttr || get(isPrimitiveOptions) ? selected.map(item => item[get(keyProp)]) : selected;
const keyAttr = props.keyAttr;
const selection = keyAttr ? selected.map(item => item[keyAttr]) : selected;
if (get(multiple))
return input(selection);
Expand Down Expand Up @@ -171,17 +163,17 @@ const {
getIdentifier,
isActiveItem,
itemIndexInValue,
menuRef,
highlightedIndex,
moveHighlight,
applyHighlighted,
} = useDropdownMenu<T, K>({
itemHeight: props.itemHeight ?? (props.dense ? 30 : 48),
keyAttr: get(keyProp),
textAttr: get(textProp),
keyAttr: props.keyAttr,
textAttr: props.textAttr,
options: filteredOptions,
dense,
value,
menuRef,
setValue,
autoSelectFirst: props.autoSelectFirst,
});
Expand Down Expand Up @@ -260,7 +252,9 @@ watch(focusedValueIndex, (index) => {
return;
nextTick(() => {
const data = get(value)[index][get(keyProp)];
const keyAttr = props.keyAttr;
const entry = get(value)[index];
const data = keyAttr ? entry[keyAttr] : entry;
const activeChip = get(activator).querySelector(`[data-value="${data}"]`);
activeChip?.focus();
});
Expand Down Expand Up @@ -291,7 +285,6 @@ function moveSelectedValueHighlight(next: boolean) {
}
}
const activator = ref();
const { focused: activatorFocusedWithin } = useFocusWithin(activator);
const { focused: menuFocusedWithin } = useFocusWithin(containerProps.ref);
const anyFocused = logicOr(activatorFocusedWithin, menuFocusedWithin);
Expand All @@ -310,7 +303,7 @@ function onInputFocused() {
}
function clear() {
emit('input', null);
emit('input', Array.isArray(props.value) ? [] : null);
}
function onInputDeletePressed() {
Expand Down Expand Up @@ -399,10 +392,10 @@ function onInputDeletePressed() {
<template v-for="(item, i) in value">
<RuiChip
v-if="multiple"
:key="item[keyProp]"
:key="getIdentifier(item)"
tabindex="-1"
:size="dense ? 'sm' : 'md'"
:data-value="item[keyProp]"
:data-value="getIdentifier(item)"
closeable
clickable
@keydown.delete="setValue(item)"
Expand All @@ -429,7 +422,7 @@ function onInputDeletePressed() {
</RuiChip>
<div
v-else
:key="item[keyProp]"
:key="getIdentifier(item)"
class="flex"
>
<slot
Expand Down Expand Up @@ -502,19 +495,19 @@ function onInputDeletePressed() {
ref="menuRef"
>
<RuiButton
v-for="(item) in renderedData"
:key="item.index"
v-for="({ item, index }) in renderedData"
:key="index"
:active="isActiveItem(item)"
:size="dense ? 'sm' : undefined"
:value="getIdentifier(item)"
variant="list"
:class="{
highlighted: highlightedIndex === item.index,
[css.highlighted]: highlightedIndex === item.index,
highlighted: highlightedIndex === index,
[css.highlighted]: highlightedIndex === index,
[css.active]: isActiveItem(item),
}"
@input="setValue(item, item.index)"
@mousedown="highlightedIndex = item.index"
@input="setValue(item, index)"
@mousedown="highlightedIndex = index"
>
<template #prepend>
<slot
Expand Down
2 changes: 0 additions & 2 deletions src/components/forms/select/RuiMenuSelect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ describe('menu select', () => {
it('works with primitive options', () => {
const wrapper = createWrapper({
propsData: {
keyAttr: 'id',
options: options.map(item => item.label),
textAttr: 'label',
value: options[4].label,
},
});
Expand Down
55 changes: 22 additions & 33 deletions src/components/forms/select/RuiMenuSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RuiMenu, { type MenuProps } from '@/components/overlays/menu/Menu.vue';
export type T = any;
export type K = Extract<keyof T, string>;
export type K = string;
export interface Props<T> {
options: T[];
Expand Down Expand Up @@ -63,34 +63,22 @@ const emit = defineEmits<{
const css = useCssModule();
const attrs = useAttrs();
const { dense, variant } = toRefs(props);
const { dense, variant, options } = toRefs(props);
const menuRef = ref();
const activator = ref();
const { focused } = useFocus(activator);
const isPrimitiveOptions = computed(() => !(props.options[0] instanceof Object));
const keyProp = computed<K>(() => props.keyAttr ?? 'key' as K);
const textProp = computed<K>(() => props.textAttr ?? 'label' as K);
const mappedOptions = computed<(T extends string ? T : Record<K, T>)[]>(() => {
if (!get(isPrimitiveOptions))
return props.options;
return props.options.map(option => ({
[get(keyProp)]: option,
[get(textProp)]: option,
}));
});
const value = computed<(T extends string ? T : Record<K, T>) | undefined>({
const value = computed<T | undefined>({
get: () => {
if (props.keyAttr || get(isPrimitiveOptions))
return get(mappedOptions).find(option => option[get(keyProp)] === props.value);
return props.value;
const keyAttr = props.keyAttr;
if (keyAttr)
return props.options.find(option => option[keyAttr] === props.value);
return props.value as T;
},
set: (selected?: T) => {
const selection = (props.keyAttr || get(isPrimitiveOptions)) && selected ? selected[get(keyProp)] : selected;
const keyAttr = props.keyAttr;
const selection = keyAttr && selected ? selected[keyAttr] : selected;
return emit('input', selection);
},
});
Expand All @@ -111,17 +99,18 @@ const {
getText,
getIdentifier,
isActiveItem,
menuRef,
highlightedIndex,
moveHighlight,
valueKey,
} = useDropdownMenu<T, K>({
itemHeight: props.itemHeight ?? (props.dense ? 30 : 48),
keyAttr: get(keyProp),
textAttr: get(textProp),
options: mappedOptions,
keyAttr: props.keyAttr,
textAttr: props.textAttr,
options,
autoFocus: true,
dense,
value,
menuRef,
autoSelectFirst: props.autoSelectFirst,
});
Expand Down Expand Up @@ -256,7 +245,7 @@ function setValue(val: T, index?: number) {
</fieldset>
</slot>
<input
:value="value ? value[keyProp] : ''"
:value="valueKey"
class="hidden"
type="hidden"
/>
Expand All @@ -275,18 +264,18 @@ function setValue(val: T, index?: number) {
ref="menuRef"
>
<RuiButton
v-for="(item) in renderedData"
:key="item.index"
v-for="({ item, index }) in renderedData"
:key="index"
:active="isActiveItem(item)"
:size="dense ? 'sm' : undefined"
:value="getIdentifier(item)"
variant="list"
:class="{
highlighted: highlightedIndex === item.index,
[css.highlighted]: !isActiveItem(item) && highlightedIndex === item.index,
highlighted: highlightedIndex === index,
[css.highlighted]: !isActiveItem(item) && highlightedIndex === index,
}"
@input="setValue(item, item.index)"
@mousedown="highlightedIndex = item.index"
@input="setValue(item, index)"
@mousedown="highlightedIndex = index"
>
<template #prepend>
<slot
Expand Down
Loading

0 comments on commit 2b91381

Please sign in to comment.