diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae7682df70..da451d654b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -814,7 +814,6 @@ packages: '@evilmartians/lefthook@1.7.15': resolution: {integrity: sha512-YgKTw9noH/SG0aOaPgrZNRgY7iJ0FuIVfvdB/aGsNulYLqiFnJyID2sfCaQ7PsGaFng2wdFBhmK/wbnb8+qPcA==} - cpu: [x64, arm64, ia32] os: [darwin, linux, win32] hasBin: true diff --git a/sandbox/pages/SandboxMultiselect.vue b/sandbox/pages/SandboxMultiselect.vue index 6352236fef..13a8470dd8 100644 --- a/sandbox/pages/SandboxMultiselect.vue +++ b/sandbox/pages/SandboxMultiselect.vue @@ -281,36 +281,44 @@ import SandboxSectionComponent from '../components/SandboxSectionComponent.vue' import type { MultiselectItem } from '@/types' import { KongIcon } from '@kong/icons' -const multiselectItems: MultiselectItem[] = [{ - label: 'Service A (long truncated with ellipsis item)', - value: 'a', - selected: true, -}, { - label: 'Service B', - value: 'b', -}, { - label: 'Service F', - value: 'f', - disabled: true, - selected: true, -}, { - label: 'Service A1', - value: 'a1', - group: 'Series 1', -}, { - label: 'Service B1', - value: 'b1', - group: 'Series 1', - selected: true, -}, { - label: 'Service A2', - value: 'a2', - group: 'Series 2', -}, { - label: 'Service B2', - value: 'b2', - group: 'Series 2', -}] +const multiselectItems: MultiselectItem[] = [ + { + label: 'Service B2', + value: 'b2', + group: 'Series 2', + }, + { + label: 'Service A (long truncated with ellipsis item)', + value: 'a', + selected: true, + }, + { + label: 'Service B', + value: 'b', + }, + { + label: 'Service F', + value: 'f', + disabled: true, + selected: true, + }, + { + label: 'Service A1', + value: 'a1', + group: 'Series 1', + }, + { + label: 'Service B1', + value: 'b1', + group: 'Series 1', + selected: true, + }, + { + label: 'Service A2', + value: 'a2', + group: 'Series 2', + }, +] const multiselectItemsSelected = JSON.parse(JSON.stringify(multiselectItems)).map((item: MultiselectItem) => { item.selected = true diff --git a/sandbox/pages/SandboxSelect.vue b/sandbox/pages/SandboxSelect.vue index c63f1af79a..f9659abee0 100644 --- a/sandbox/pages/SandboxSelect.vue +++ b/sandbox/pages/SandboxSelect.vue @@ -297,34 +297,42 @@ import SandboxSectionComponent from '../components/SandboxSectionComponent.vue' import { KongIcon } from '@kong/icons' import type { SelectItem } from '@/types' -const selectItems: SelectItem[] = [{ - label: 'Cats', - value: 'cats', - selected: true, -}, { - label: 'Dogs', - value: 'dogs', -}, { - label: 'Bunnies', - value: 'bunnies', - disabled: true, -}, { - label: 'Duck', - value: 'duck', - group: 'Birds', -}, { - label: 'Oriole', - value: 'oriole', - group: 'Birds', -}, { - label: 'Trout', - value: 'trout', - group: 'Fish', -}, { - label: 'Salmon', - value: 'salmon', - group: 'Fish', -}] +const selectItems: SelectItem[] = [ + { + label: 'Salmon', + value: 'salmon', + group: 'Fish', + }, + { + label: 'Cats', + value: 'cats', + selected: true, + }, + { + label: 'Dogs', + value: 'dogs', + }, + { + label: 'Bunnies', + value: 'bunnies', + disabled: true, + }, + { + label: 'Duck', + value: 'duck', + group: 'Birds', + }, + { + label: 'Trout', + value: 'trout', + group: 'Fish', + }, + { + label: 'Oriole', + value: 'oriole', + group: 'Birds', + }, +] const vModel = ref('cats') diff --git a/src/components/KMultiselect/KMultiselect.vue b/src/components/KMultiselect/KMultiselect.vue index 63ff1e8c34..c9690151fb 100644 --- a/src/components/KMultiselect/KMultiselect.vue +++ b/src/components/KMultiselect/KMultiselect.vue @@ -39,6 +39,8 @@ :tabindex="isDisabled || isReadonly || collapsedContext ? -1 : 0" v-bind="modifiedAttrs" @click="handleFilterClick" + @keydown.enter="onTriggerKeypress" + @keydown.space="onTriggerKeypress" >
@@ -453,6 +457,8 @@ const emit = defineEmits<{ (e: 'item-removed', value: MultiselectItem): void }>() +const kMultiselectItems = ref | null>(null) + const isRequired = computed((): boolean => attrs.required !== undefined && String(attrs.required) !== 'false') const strippedLabel = computed((): string => stripRequiredLabel(props.label, isRequired.value)) const hasLabelTooltip = computed((): boolean => !!(props.labelAttributes?.help || props.labelAttributes?.info || slots['label-tooltip'])) @@ -506,7 +512,7 @@ const uniqueFilterStr = computed((): boolean => { return true }) -const popper = ref(null) +const popper = ref | null>(null) // A clone of `props.items`, normalized. May contain additional custom items that have been created. const unfilteredItems: Ref = ref([]) @@ -695,7 +701,6 @@ const handleMultipleItemsSelect = (items: MultiselectItem[]) => { const selectedItem = unfilteredItems.value.filter(anItem => anItem.value === itemToSelect.value)?.[0] || null selectedItem.selected = true - selectedItem.key = selectedItem?.key?.includes('-selected') ? selectedItem.key : `${selectedItem.key}-selected` // if it isn't already in selectedItems, add it if (!selectedItems.value.filter(anItem => anItem.value === selectedItem.value).length) { selectedItems.value.push(selectedItem) @@ -718,7 +723,6 @@ const handleMultipleItemsDeselect = (items: MultiselectItem[], restage = false) // deselect item itemToDeselect.selected = false - itemToDeselect.key = itemToDeselect.key?.replace(/-selected/gi, '') // if some items are hidden grab the first hidden one and add it into the visible array if (invisibleSelectedItemsStaging.value.length) { @@ -771,7 +775,6 @@ const handleItemSelect = (item: MultiselectItem, isNew?: boolean) => { } // deselect item selectedItem.selected = false - selectedItem.key = selectedItem.key?.replace(/-selected/gi, '') // if some items are hidden grab the first hidden one and add it into the visible array if (invisibleSelectedItemsStaging.value.length) { @@ -789,7 +792,6 @@ const handleItemSelect = (item: MultiselectItem, isNew?: boolean) => { } } else { // newly selected item selectedItem.selected = true - selectedItem.key = selectedItem.key?.includes('-selected') ? selectedItem.key : `${selectedItem.key}-selected` selectedItems.value.push(selectedItem) visibleSelectedItemsStaging.value.push(selectedItem) // track it if it's a newly added item @@ -826,12 +828,16 @@ const handleAddItem = (): void => { filterString.value = '' } -// sort dropdown items. Selected items displayed before unselected items +// Sort items. Non-grouped items are displayed first, then grouped items. +// Within non-grouped and grouped items, selected items are displayed first. const sortItems = () => { - const selItems = filteredItems.value.filter((item: MultiselectItem) => item.selected) - const unselItems = filteredItems.value.filter((item: MultiselectItem) => !item.selected) + const selectedItems = filteredItems.value.filter((item: MultiselectItem) => item.selected) + const unselectedItems = filteredItems.value.filter((item: MultiselectItem) => !item.selected) + const allItems = [...selectedItems, ...unselectedItems] + const ungroupedItems = allItems.filter(item => !item.group) + const groupedItems = allItems.filter(item => item.group).sort((a, b) => a.group.toLowerCase().localeCompare(b.group.toLowerCase())) - sortedItems.value = selItems.concat(unselItems) + sortedItems.value = [...ungroupedItems, ...groupedItems] } const clearSelection = (): void => { @@ -841,7 +847,6 @@ const clearSelection = (): void => { } anItem.selected = false - anItem.key = anItem?.key?.replace(/-selected/gi, '') if (anItem.custom) { // we must emit that we are removing each item before we actually clear them since this is our only reference @@ -885,6 +890,20 @@ const triggerFocus = (evt: any, isToggled: Ref):void => { if (evt.keyCode === 27) { isToggled.value = false } + + if ((evt.code === 'ArrowDown' || evt.code === 'ArrowUp')) { + kMultiselectItems.value?.setFocus() + } +} + +const onTriggerKeypress = () => { + popper.value?.showPopover() +} + +const onDropdownInputKeyup = (event: any) => { + if ((event.code === 'ArrowDown' || event.code === 'ArrowUp')) { + kMultiselectItems.value?.setFocus() + } } const onInputFocus = async (): Promise => { @@ -996,7 +1015,6 @@ watch(() => props.items, (newValue, oldValue) => { if (props.modelValue.includes(unfilteredItems.value[i].value) || unfilteredItems.value[i].selected) { const selectedItem = unfilteredItems.value[i] selectedItem.selected = true - selectedItem.key = selectedItem.key?.includes('-selected') ? selectedItem.key : `${selectedItem.key}-selected` // if it isn't already in the selectedItems array, add it if (!selectedItems.value.filter(anItem => anItem.value === selectedItem.value).length) { selectedItems.value.push(selectedItem) @@ -1155,7 +1173,7 @@ $kMultiselectInputHelpTextHeight: var(--kui-line-height-20, $kui-line-height-20) width: 100%; } - &.hovered { + &.hovered, &:hover { @include inputHover; } diff --git a/src/components/KMultiselect/KMultiselectItem.vue b/src/components/KMultiselect/KMultiselectItem.vue index 3564abf27a..6d6243768d 100644 --- a/src/components/KMultiselect/KMultiselectItem.vue +++ b/src/components/KMultiselect/KMultiselectItem.vue @@ -13,6 +13,8 @@ type="button" :value="item.value" @click="handleClick" + @keydown.down.prevent="$emit('arrow-down')" + @keydown.up.prevent="$emit('arrow-up')" > {{ item.label }} @@ -37,6 +39,8 @@ const props = defineProps({ const emit = defineEmits<{ (e: 'selected', value: MultiselectItem): void; + (e: 'arrow-down'): void; + (e: 'arrow-up'): void; }>() const handleClick = (): void => { diff --git a/src/components/KMultiselect/KMultiselectItems.vue b/src/components/KMultiselect/KMultiselectItems.vue index e84ff34b1f..6ca60e2cab 100644 --- a/src/components/KMultiselect/KMultiselectItems.vue +++ b/src/components/KMultiselect/KMultiselectItems.vue @@ -2,7 +2,10 @@ +