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

🎸 feat: a11y に対応した combobox の作成 #362

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
130 changes: 62 additions & 68 deletions src/components/shared/InputSelectMultiple.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts" setup>
import { ChevronDownIcon } from '@heroicons/vue/24/solid';
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';

import { ChevronDownIcon } from '@heroicons/vue/24/solid'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ValueValue = Record<string, any> | string | null
Expand Down Expand Up @@ -163,71 +162,69 @@ const pushTag = () => {
])
searchQuery.value = ''
}
const handleClick = () => {
const openDropdown = () => {
isDropdownOpen.value = true
if (inputRef.value === null) return
inputRef.value.focus()
}

/* キーボードによる操作の処理 */
const handleKeydown = (e: KeyboardEvent, option: Value) => {
if (e.key === 'Tab') {
isDropdownOpen.value = false
emit('close')
}
if (e.key === 'ArrowDown') {
e.preventDefault()
if (searchQuery.value !== '') return
if (listItemRefs.value === null) return
const length =
searchQuery.value !== ''
? searchedOptions.value.length
: (props.options?.length ?? 0)
focusingListItemIndex.value = (focusingListItemIndex.value + 1) % length
const buttonEl = listItemRefs.value[focusingListItemIndex.value]
.firstChild as HTMLButtonElement
buttonEl.focus({ preventScroll: false })
if (listRef.value === null) return
if (focusingListItemIndex.value > 3) {
listRef.value.scrollBy({
top: 12,
left: 0,
behavior: 'smooth'
})
}
switch (e.key) {
case 'Tab':
isDropdownOpen.value = false
emit('close')
break
case 'Enter':
e.preventDefault()
if (focusingListItemIndex.value === -1) {
pushTag()
} else {
selectValue(option)
}
break
case 'ArrowDown':
e.preventDefault()
if (searchQuery.value !== '') return
navigateList('down')
if (listRef.value === null) return
if (focusingListItemIndex.value > 3) {
listRef.value.scrollBy({
top: 12,
left: 0,
behavior: 'smooth'
})
}
break
case 'ArrowUp':
e.preventDefault()
if (searchQuery.value !== '') return
navigateList('up')
if (listRef.value === null) return
if (focusingListItemIndex.value < length - 3) {
listRef.value.scrollBy({
top: -12,
left: 0,
behavior: 'smooth'
})
}
break
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (searchQuery.value !== '') return
if (listItemRefs.value === null) return
const length =
searchQuery.value !== ''
? searchedOptions.value.length
: (props.options?.length ?? 0)
focusingListItemIndex.value =
(focusingListItemIndex.value - 1 + length) % length
const buttonEl = listItemRefs.value[focusingListItemIndex.value]
.firstChild as HTMLButtonElement
buttonEl.focus({ preventScroll: false })
if (listRef.value === null) return
}

if (focusingListItemIndex.value < length - 3) {
listRef.value.scrollBy({
top: -12,
left: 0,
behavior: 'smooth'
})
}
}
if (e.key === 'Enter') {
e.preventDefault()
if (focusingListItemIndex.value === -1) {
pushTag()
} else {
selectValue(option)
}
}
const navigateList = (direction: 'up' | 'down') => {
if (listItemRefs.value === null) return

const length = searchedOptions.value.length
const increment = direction === 'down' ? 1 : -1
focusingListItemIndex.value =
(focusingListItemIndex.value + increment + length) % length

const buttonEl = listItemRefs.value[focusingListItemIndex.value]
.firstChild as HTMLButtonElement
buttonEl?.focus({ preventScroll: false })
}

const handleInputKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
Expand All @@ -238,13 +235,6 @@ const handleInputKeydown = (e: KeyboardEvent) => {
}
}

const calcWidth = computed(() => {
if (/w-/.test(props.class)) {
return props.class
}
return `${props.class} w-72`
})

/* ドロップダウンの位置を計算する処理 */
const updateHeight = () => {
if (listRef.value === null) return
Expand All @@ -270,11 +260,15 @@ onUnmounted(() => {
<template>
<div
ref="inputSelectRef"
:class="`relative ${disabled && 'cursor-not-allowed'} ${calcWidth}`">
:class="`relative ${disabled && 'cursor-not-allowed'}`">
<div
class="flex cursor-text items-center rounded border border-surface-secondary py-1 pl-1"
role="button"
tabindex="0"
class="flex cursor-text items-center w-full py-1 pl-1 rounded border border-surface-secondary"
:class="`${disabled && 'pointer-events-none'}`"
@click.prevent="handleClick">
@click.prevent="openDropdown"
@keydown.enter.prevent="openDropdown"
@keydown.space.prevent="openDropdown">
<div class="flex items-center overflow-x-auto">
<div
v-for="selectedValue in selectedValues"
Expand Down
18 changes: 8 additions & 10 deletions src/components/shared/InputSelectSingle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const selectValue = (selectedOption: Value) => {
return
}
}
//add
// add
emit('update:modelValue', selectedOption.value)
if (inputRef.value === null) return
isDropdownOpen.value = false
Expand Down Expand Up @@ -96,7 +96,7 @@ const searchedOptions = computed(() => {
return regexp.test(option.key)
})
})
const handleClick = () => {
const openDropdown = () => {
isDropdownOpen.value = true
if (inputRef.value === null) return
inputRef.value.focus()
Expand Down Expand Up @@ -145,12 +145,6 @@ const handleKeydown = (e: KeyboardEvent) => {
}
}
}
const calcWidth = computed(() => {
if (/w-/.test(props.class)) {
return props.class
}
return `${props.class} w-72`
})

const updateHeight = () => {
if (listRef.value === null) return
Expand All @@ -176,11 +170,15 @@ onUnmounted(() => {
<template>
<div
ref="inputSelectRef"
:class="`relative ${disabled && 'cursor-not-allowed'} ${calcWidth}`">
:class="`relative ${disabled && 'cursor-not-allowed'}`">
<div
role="button"
tabindex="0"
class="flex w-full cursor-text items-center gap-1 rounded border border-surface-secondary py-1 pr-1"
:class="`${disabled && 'pointer-events-none'}`"
@click="handleClick">
@click="openDropdown"
@keydown.enter="openDropdown"
@keydown.space="openDropdown">
<div class="relative left-2 flex w-full">
<span
v-if="selectedValue"
Expand Down
Loading