From dbdc2323f4a4a715681a3698b6d101e09bbdaca0 Mon Sep 17 00:00:00 2001 From: lukicenturi Date: Thu, 23 May 2024 13:34:50 +0700 Subject: [PATCH 1/2] feat(AutoComplete): add some missing props --- example/src/views/AutoCompleteView.vue | 3 + example/src/views/ChipView.vue | 173 +++++++++++++++++- src/components/chips/Chip.vue | 32 ++-- .../auto-complete/RuiAutoComplete.spec.ts | 1 + .../forms/auto-complete/RuiAutoComplete.vue | 94 ++++++++-- src/components/forms/select/RuiMenuSelect.vue | 30 ++- src/composables/dropdown-menu.ts | 37 ++-- src/composables/popper.ts | 2 +- 8 files changed, 320 insertions(+), 52 deletions(-) diff --git a/example/src/views/AutoCompleteView.vue b/example/src/views/AutoCompleteView.vue index cfa639c..4d21835 100644 --- a/example/src/views/AutoCompleteView.vue +++ b/example/src/views/AutoCompleteView.vue @@ -181,12 +181,14 @@ const autoCompletePrimitive = ref[]>([ }, { value: [], + chips: true, variant: 'outlined', label: 'Multiple', options: primitiveOptions, }, { value: [], + chips: true, dense: true, variant: 'outlined', label: 'Multiple', @@ -194,6 +196,7 @@ const autoCompletePrimitive = ref[]>([ }, { value: [], + chips: true, dense: true, disabled: true, variant: 'outlined', diff --git a/example/src/views/ChipView.vue b/example/src/views/ChipView.vue index 245fe21..4a9e328 100644 --- a/example/src/views/ChipView.vue +++ b/example/src/views/ChipView.vue @@ -6,6 +6,12 @@ import { type ChipProps, RuiChip } from '@rotki/ui-library-compat'; const dismissed = ref>({}); const chips = ref([ + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: true, + }, { disabled: false, color: 'primary', @@ -42,6 +48,12 @@ const chips = ref([ variant: 'filled', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'filled', + closeable: true, + }, { disabled: true, color: 'primary', @@ -78,6 +90,12 @@ const chips = ref([ variant: 'filled', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: false, + }, { disabled: false, color: 'primary', @@ -114,6 +132,13 @@ const chips = ref([ variant: 'filled', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: false, + tile: true, + }, { disabled: false, color: 'primary', @@ -157,6 +182,14 @@ const chips = ref([ tile: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: false, + tile: true, + clickable: true, + }, { disabled: false, color: 'primary', @@ -205,6 +238,13 @@ const chips = ref([ tile: true, clickable: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + size: 'sm', + closeable: true, + }, { disabled: false, color: 'primary', @@ -247,6 +287,13 @@ const chips = ref([ size: 'sm', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'filled', + size: 'sm', + closeable: true, + }, { disabled: true, color: 'primary', @@ -289,6 +336,12 @@ const chips = ref([ size: 'sm', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: false, + }, { disabled: false, color: 'primary', @@ -325,6 +378,12 @@ const chips = ref([ variant: 'filled', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + closeable: true, + }, { disabled: false, color: 'primary', @@ -361,6 +420,12 @@ const chips = ref([ variant: 'outlined', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'outlined', + closeable: true, + }, { disabled: true, color: 'primary', @@ -397,6 +462,12 @@ const chips = ref([ variant: 'outlined', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + closeable: false, + }, { disabled: false, color: 'primary', @@ -433,6 +504,13 @@ const chips = ref([ variant: 'outlined', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + size: 'sm', + closeable: true, + }, { disabled: false, color: 'primary', @@ -475,6 +553,13 @@ const chips = ref([ size: 'sm', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'outlined', + size: 'sm', + closeable: true, + }, { disabled: true, color: 'primary', @@ -517,6 +602,12 @@ const chips = ref([ size: 'sm', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + closeable: false, + }, { disabled: false, color: 'primary', @@ -556,6 +647,12 @@ const chips = ref([ ]); const prependChips = ref([ + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: true, + }, { disabled: false, color: 'primary', @@ -592,6 +689,12 @@ const prependChips = ref([ variant: 'filled', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'filled', + closeable: true, + }, { disabled: true, color: 'primary', @@ -628,6 +731,12 @@ const prependChips = ref([ variant: 'filled', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + closeable: false, + }, { disabled: false, color: 'primary', @@ -664,6 +773,13 @@ const prependChips = ref([ variant: 'filled', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + size: 'sm', + closeable: true, + }, { disabled: false, color: 'primary', @@ -706,6 +822,13 @@ const prependChips = ref([ size: 'sm', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'filled', + size: 'sm', + closeable: true, + }, { disabled: true, color: 'primary', @@ -748,6 +871,13 @@ const prependChips = ref([ size: 'sm', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'filled', + size: 'sm', + closeable: false, + }, { disabled: false, color: 'primary', @@ -790,6 +920,12 @@ const prependChips = ref([ size: 'sm', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + closeable: true, + }, { disabled: false, color: 'primary', @@ -826,6 +962,12 @@ const prependChips = ref([ variant: 'outlined', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'outlined', + closeable: true, + }, { disabled: true, color: 'primary', @@ -862,6 +1004,12 @@ const prependChips = ref([ variant: 'outlined', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + closeable: false, + }, { disabled: false, color: 'primary', @@ -898,6 +1046,13 @@ const prependChips = ref([ variant: 'outlined', closeable: false, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + size: 'sm', + closeable: true, + }, { disabled: false, color: 'primary', @@ -940,6 +1095,13 @@ const prependChips = ref([ size: 'sm', closeable: true, }, + { + disabled: true, + color: 'grey', + variant: 'outlined', + size: 'sm', + closeable: true, + }, { disabled: true, color: 'primary', @@ -982,6 +1144,13 @@ const prependChips = ref([ size: 'sm', closeable: true, }, + { + disabled: false, + color: 'grey', + variant: 'outlined', + size: 'sm', + closeable: false, + }, { disabled: false, color: 'primary', @@ -1045,7 +1214,7 @@ function onRemove(identifier: number | string) { > Chips -
+
Chips with Prepend -
+
(() => { @apply rounded-sm; } + &:not(.readonly):not(.disabled) { + @apply hover:brightness-90 focus:brightness-75; + } + &.grey { @apply text-rui-text; &.filled { - @apply bg-black/[0.08]; - - &:not(.readonly):not(.disabled) { - @apply hover:bg-black/[0.12] focus:bg-black/[0.20]; - } + @apply bg-rui-grey-200; } &.outlined { @@ -152,17 +152,13 @@ const style = computed(() => { &.#{$color} { &.filled { @apply text-rui-dark-text bg-rui-#{$color}; - - &:not(.readonly):not(.disabled) { - @apply hover:bg-rui-#{$color}-darker; - } } &.outlined { @apply border text-rui-#{$color} border-rui-#{$color}/50 bg-transparent; &:not(.readonly):not(.disabled) { - @apply hover:bg-rui-#{$color}/[0.04]; + @apply hover:bg-rui-#{$color}/[0.04] focus:bg-rui-#{$color}/[0.12]; } } } @@ -227,11 +223,21 @@ const style = computed(() => { :global(.dark) { .chip { + &:not(.readonly):not(.disabled) { + @apply hover:brightness-110 focus:brightness-75; + } + &.grey { - @apply bg-white/[0.16]; + &.filled { + @apply bg-rui-grey-800; + } - &:not(.readonly):not(.disabled) { - @apply hover:bg-white/[0.20] focus:bg-white/[0.24]; + &.outlined { + @apply border-white/[0.26]; + + &:not(.readonly):not(.disabled) { + @apply hover:bg-white/[0.04] focus:bg-white/[0.12]; + } } } diff --git a/src/components/forms/auto-complete/RuiAutoComplete.spec.ts b/src/components/forms/auto-complete/RuiAutoComplete.spec.ts index f818871..c89ae59 100644 --- a/src/components/forms/auto-complete/RuiAutoComplete.spec.ts +++ b/src/components/forms/auto-complete/RuiAutoComplete.spec.ts @@ -130,6 +130,7 @@ describe('autocomplete', () => { const wrapper = createWrapper({ propsData: { autoSelectFirst: true, + chips: true, keyAttr: 'id', options, textAttr: 'label', diff --git a/src/components/forms/auto-complete/RuiAutoComplete.vue b/src/components/forms/auto-complete/RuiAutoComplete.vue index e45e4e3..4447d93 100644 --- a/src/components/forms/auto-complete/RuiAutoComplete.vue +++ b/src/components/forms/auto-complete/RuiAutoComplete.vue @@ -4,20 +4,22 @@ import RuiButton from '@/components/buttons/button/Button.vue'; import RuiIcon from '@/components/icons/Icon.vue'; import RuiChip from '@/components/chips/Chip.vue'; import RuiMenu, { type MenuProps } from '@/components/overlays/menu/Menu.vue'; +import RuiProgress from '@/components/progress/Progress.vue'; import type { Ref } from 'vue'; export type T = any; export type K = string; -export type ModelValue = MV | MV[] | null; +export type ModelValue = T | T[] | T[K] | null; export interface Props { options: T[]; keyAttr?: K; textAttr?: K; - value?: ModelValue; + value?: ModelValue; disabled?: boolean; + loading?: boolean; readOnly?: boolean; dense?: boolean; clearable?: boolean; @@ -35,21 +37,30 @@ export interface Props { successMessages?: string | string[]; hideDetails?: boolean; autoSelectFirst?: boolean; + chips?: boolean; searchInput?: string; noFilter?: boolean; + hideNoData?: boolean; + noDataText?: string; filter?: (item: T, queryText: string) => boolean; + hideSelected?: boolean; + placeholder?: string; + returnObject?: boolean; } defineOptions({ name: 'RuiAutoComplete', + inheritAttrs: false, }); const props = withDefaults(defineProps>(), { disabled: false, + loading: false, readOnly: false, dense: false, clearable: false, hideDetails: false, + chips: false, label: 'Select', prependWidth: 0, appendWidth: 0, @@ -63,10 +74,14 @@ const props = withDefaults(defineProps>(), { autoSelectFirst: false, searchInput: '', noFilter: false, + hideNoData: false, + noDataText: 'No data available', + hideSelected: false, + returnObject: false, }); const emit = defineEmits<{ - (e: 'input', value: ModelValue): void; + (e: 'input', value: ModelValue): void; (e: 'update:search-input', search: string): void; }>(); @@ -115,7 +130,7 @@ const filteredOptions = computed(() => { return optionsVal.filter(item => usedFilter(item, search)); }); -function input(value: ModelValue) { +function input(value: ModelValue) { emit('input', value); } @@ -125,14 +140,16 @@ const value = computed<(T extends string ? T : Record)[]>({ const keyAttr = props.keyAttr; const valueToArray = value ? (Array.isArray(value) ? value : [value]) : []; - if (keyAttr) - return get(options).filter(item => valueToArray.includes(item[keyAttr])); + if (keyAttr && !props.returnObject) { + const filtered = get(options).filter(item => valueToArray.includes(item[keyAttr])); + return get(multiple) || filtered.length <= 1 ? filtered : [filtered[0]]; + } return valueToArray; }, set: (selected: T[]) => { const keyAttr = props.keyAttr; - const selection = keyAttr ? selected.map(item => item[keyAttr]) : selected; + const selection = keyAttr && !props.returnObject ? selected.map(item => item[keyAttr]) : selected; if (get(multiple)) return input(selection); @@ -176,6 +193,7 @@ const { menuRef, setValue, autoSelectFirst: props.autoSelectFirst, + hideSelected: props.hideSelected, }); const outlined = computed(() => get(variant) === 'outlined'); @@ -316,6 +334,24 @@ function onInputDeletePressed() { clear(); } } + +function chipAttrs(item: (T extends string ? T : Record)) { + return { + 'data-value': getIdentifier(item), + }; +} + +function chipListener(item: (T extends string ? T : Record), index: number) { + return { + 'keydown': (event: KeyboardEvent) => { + const { key } = event; + if (['Backspace', 'Delete'].includes(key)) + setValue(item); + }, + 'click.stop': () => setValueFocus(index), + 'click:close': () => setValue(item), + }; +} @@ -316,7 +344,7 @@ function setValue(val: T, index?: number) { } &.dense { - @apply py-1 min-h-10; + @apply py-1.5 min-h-10; ~ .fieldset { @apply px-2; diff --git a/src/composables/dropdown-menu.ts b/src/composables/dropdown-menu.ts index ce5b5c9..0c788aa 100644 --- a/src/composables/dropdown-menu.ts +++ b/src/composables/dropdown-menu.ts @@ -6,7 +6,7 @@ export interface DropdownOptions { value: Ref; menuRef: Ref; keyAttr?: K; - textAttr?: K; + textAttr?: K | ((item: T) => string); appendWidth?: number; prependWidth?: number; itemHeight?: number; @@ -14,6 +14,7 @@ export interface DropdownOptions { autoSelectFirst?: boolean; autoFocus?: boolean; setValue?: (val: T) => void; + hideSelected?: boolean; } export function useDropdownMenu({ @@ -21,16 +22,25 @@ export function useDropdownMenu({ autoFocus, autoSelectFirst, dense, + hideSelected, itemHeight = 48, keyAttr, menuRef, - options, + options: allOptions, overscan = 5, prependWidth, setValue, textAttr, value, }: DropdownOptions) { + const options = computed(() => { + const options = get(allOptions); + if (!hideSelected) + return options; + + return options.filter(item => !isActiveItem(item)); + }); + const { containerProps, list, wrapperProps } = useVirtualList( options, { @@ -89,9 +99,14 @@ export function useDropdownMenu({ set(isOpen, state); } - function getText(item: T): T[K] | T { - if (textAttr) - return item[textAttr]; + function getText(item: T): T[K] | T | string { + if (textAttr) { + if (typeof textAttr === 'function') + return textAttr(item); + + else + return item[textAttr]; + } return item; } @@ -137,7 +152,7 @@ export function useDropdownMenu({ container.scrollTop = index * itemHeight; if (get(autoFocus)) { const elem = get(menuRef).getElementsByClassName('highlighted')[0]; - if ('focus' in elem && typeof elem.focus === 'function') + if (elem && 'focus' in elem && typeof elem.focus === 'function') elem.focus(); } } @@ -175,16 +190,8 @@ export function useDropdownMenu({ }); watch(options, () => { - if (get(highlightedIndex) !== -1) { - if (get(value)) { - const index = get(options).findIndex(isActiveItem); - if (index > -1) { - set(highlightedIndex, index); - return; - } - } + if (get(highlightedIndex) !== -1) set(highlightedIndex, 0); - } }); const moveHighlight = (up: boolean) => { diff --git a/src/composables/popper.ts b/src/composables/popper.ts index 5e1e925..5327573 100644 --- a/src/composables/popper.ts +++ b/src/composables/popper.ts @@ -191,7 +191,7 @@ export function usePopper(options: Ref, disabled: Ref = }); }); - useResizeObserver(reference, async () => { + useResizeObserver([reference, popper], async () => { const instanceVal = get(instance); if (get(open) && instanceVal) await instanceVal.update(); From 842b7a73e74bdc24163f0d0d6b9a5b57a91b5c82 Mon Sep 17 00:00:00 2001 From: lukicenturi Date: Fri, 24 May 2024 18:03:43 +0700 Subject: [PATCH 2/2] test: fix test --- example/cypress/e2e/chip.cy.ts | 4 ++-- example/cypress/e2e/forms/autocomplete.cy.ts | 4 ++-- src/composables/dropdown-menu.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/example/cypress/e2e/chip.cy.ts b/example/cypress/e2e/chip.cy.ts index 87db456..dd1aebc 100644 --- a/example/cypress/e2e/chip.cy.ts +++ b/example/cypress/e2e/chip.cy.ts @@ -9,8 +9,8 @@ describe('chip', () => { cy.contains('h2[data-cy=chips]', 'Chips'); cy.get('[data-cy*=chip-0').first().as('dismissibleChip'); - cy.get('[data-cy*=chip-6').first().as('disabledChip'); - cy.get('[data-cy*=chip-12').first().as('inDismissibleChip'); + cy.get('[data-cy*=chip-7').first().as('disabledChip'); + cy.get('[data-cy*=chip-14').first().as('inDismissibleChip'); cy.get('@dismissibleChip').find('button').should('not.be.disabled'); cy.get('@dismissibleChip').find('button').trigger('click'); diff --git a/example/cypress/e2e/forms/autocomplete.cy.ts b/example/cypress/e2e/forms/autocomplete.cy.ts index 7bc4250..a8bf4d2 100644 --- a/example/cypress/e2e/forms/autocomplete.cy.ts +++ b/example/cypress/e2e/forms/autocomplete.cy.ts @@ -7,12 +7,12 @@ describe('forms/Auto Completes', () => { cy.contains('h2[data-cy=auto-completes]', 'Auto Completes'); cy.get('div[data-cy=auto-complete-0]').as('firstAutoComplete'); - cy.get('@firstAutoComplete').find('[data-id="activator"]').should('be.exist'); + cy.get('@firstAutoComplete').should('be.exist'); cy.get('@firstAutoComplete').find('span[class*=_label_]').should('contain.text', 'Select'); cy.get('div[role=menu]').should('not.exist'); - cy.get('@firstAutoComplete').find('[data-id="activator"]').click(); + cy.get('@firstAutoComplete').click(); cy.get('div[role=menu]').should('be.visible'); cy.get('div[role=menu] button:first-child').click(); diff --git a/src/composables/dropdown-menu.ts b/src/composables/dropdown-menu.ts index 0c788aa..0291e58 100644 --- a/src/composables/dropdown-menu.ts +++ b/src/composables/dropdown-menu.ts @@ -136,7 +136,7 @@ export function useDropdownMenu({ return itemIndexInValue(item) !== -1; } - function adjustScrollByHighlightedIndex() { + function adjustScrollByHighlightedIndex(smooth: boolean = false) { const index = get(highlightedIndex); nextTick(() => { const container = get(menuRef)?.parentElement; @@ -144,7 +144,7 @@ export function useDropdownMenu({ const highlightedElem = get(menuRef).getElementsByClassName('highlighted')[0]; if (highlightedElem) { - highlightedElem.scrollIntoView?.({ block: 'nearest' }); + highlightedElem.scrollIntoView?.({ behavior: smooth ? 'smooth' : 'auto', block: 'nearest' }); if (get(autoFocus) && 'focus' in highlightedElem && typeof highlightedElem.focus === 'function') highlightedElem?.focus(); } @@ -184,7 +184,7 @@ export function useDropdownMenu({ watch(highlightedIndex, (curr, prev) => { if (curr !== prev) { nextTick(() => { - adjustScrollByHighlightedIndex(); + adjustScrollByHighlightedIndex(true); }); } });