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(kmultiselect): arrow keys navigation [KHCP-13121] #2433

Merged
merged 6 commits into from
Oct 4, 2024
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
1 change: 0 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 38 additions & 30 deletions sandbox/pages/SandboxMultiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 36 additions & 28 deletions sandbox/pages/SandboxSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('cats')

Expand Down
42 changes: 30 additions & 12 deletions src/components/KMultiselect/KMultiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
:tabindex="isDisabled || isReadonly || collapsedContext ? -1 : 0"
v-bind="modifiedAttrs"
@click="handleFilterClick"
@keydown.enter="onTriggerKeypress"
@keydown.space="onTriggerKeypress"
>
<div v-if="collapsedContext">
<KInput
Expand Down Expand Up @@ -168,12 +170,14 @@
type="text"
@click.stop
@focus="triggerInitialFocus"
@keyup="onDropdownInputKeyup"
@keyup.enter.stop
@update:model-value="onQueryChange"
/>
</div>
<div aria-live="polite">
<KMultiselectItems
ref="kMultiselectItems"
:items="sortedItems"
@selected="handleItemSelect"
>
Expand Down Expand Up @@ -453,6 +457,8 @@ const emit = defineEmits<{
(e: 'item-removed', value: MultiselectItem): void
}>()

const kMultiselectItems = ref<InstanceType<typeof KMultiselectItems> | 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']))
Expand Down Expand Up @@ -506,7 +512,7 @@ const uniqueFilterStr = computed((): boolean => {

return true
})
const popper = ref(null)
const popper = ref<InstanceType<typeof KPop> | null>(null)

// A clone of `props.items`, normalized. May contain additional custom items that have been created.
const unfilteredItems: Ref<MultiselectItem[]> = ref([])
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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 => {
Expand All @@ -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
Expand Down Expand Up @@ -885,6 +890,20 @@ const triggerFocus = (evt: any, isToggled: Ref<boolean>):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<void> => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1155,7 +1173,7 @@ $kMultiselectInputHelpTextHeight: var(--kui-line-height-20, $kui-line-height-20)
width: 100%;
}

&.hovered {
&.hovered, &:hover {
@include inputHover;
}

Expand Down
4 changes: 4 additions & 0 deletions src/components/KMultiselect/KMultiselectItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
type="button"
:value="item.value"
@click="handleClick"
@keydown.down.prevent="$emit('arrow-down')"
@keydown.up.prevent="$emit('arrow-up')"
>
<span class="multiselect-item-label">
<slot name="content">{{ item.label }}</slot>
Expand All @@ -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 => {
Expand Down
Loading