Skip to content

Commit

Permalink
Merge pull request #4025 from traPtitech/feat/navigation_bar_a11y
Browse files Browse the repository at this point in the history
ナビゲーションバーをキーボード操作可能に
  • Loading branch information
mehm8128 committed Apr 26, 2024
2 parents 7cf0f91 + bf23f13 commit 4b11357
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 124 deletions.
2 changes: 1 addition & 1 deletion src/components/Main/MainView/MessageInput/MessageInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import useTextStampPickerInvoker from '../composables/useTextStampPickerInvoker'
import useAttachments from './composables/useAttachments'
import useModifierKey from './composables/useModifierKey'
import usePostMessage from './composables/usePostMessage'
import useFocus from './composables/useFocus'
import useFocus from '/@/composables/dom/useFocus'
import { useToastStore } from '/@/store/ui/toast'
import useMessageInputState from '/@/composables/messageInputState/useMessageInputState'
import useMessageInputStateAttachment from '/@/composables/messageInputState/useMessageInputStateAttachment'
Expand Down
96 changes: 71 additions & 25 deletions src/components/Main/NavigationBar/ChannelList/ChannelElement.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
<template>
<div
:class="$style.container"
:aria-selected="isSelected"
:data-is-selected="$boolAttr(isSelected)"
:data-is-inactive="$boolAttr(!channel.active)"
>
<!-- チャンネル表示本体 -->
<div
:class="$style.channel"
@mousedown="openChannel"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<div :class="$style.channelContainer">
<channel-element-hash
:class="$style.channelHash"
:has-child="hasChildren"
Expand All @@ -20,18 +15,32 @@
:has-notification-on-child="notificationState.hasNotificationOnChild"
:is-inactive="!channel.active"
@mousedown.stop="onChannelHashClick"
@mouseenter="onHashMouseEnter"
@mouseleave="onHashMouseLeave"
/>
<channel-element-name
:channel="channel"
:show-shortened-path="showShortenedPath"
:is-selected="isSelected"
/>
<channel-element-unread-badge
:is-noticeable="notificationState.isNoticeable"
:unread-count="notificationState.unreadCount"
@keydown.enter="onChannelHashKeydownEnter"
@mouseenter="onHashHovered"
@mouseleave="onHashHoveredLeave"
/>
<router-link
:to="channelIdToLink(props.channel.id)"
:class="$style.channel"
:aria-current="isSelected && 'page'"
:aria-expanded="hasChildren && isOpened ? true : undefined"
:data-is-inactive="$boolAttr(!channel.active)"
:aria-label="showShortenedPath ? pathTooltip : pathToShow"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@focus="onFocus"
@blur="onBlur"
>
<channel-element-name
:channel="channel"
:show-shortened-path="showShortenedPath"
:is-selected="isSelected"
/>
<channel-element-unread-badge
:is-noticeable="notificationState.isNoticeable"
:unread-count="notificationState.unreadCount"
/>
</router-link>
</div>

<div :class="$style.slot">
Expand All @@ -40,9 +49,10 @@

<!-- チャンネルの背景 -->
<div
v-if="isSelected || isChannelBgHovered"
v-if="isSelected || isChannelBgHovered || isFocused"
:class="$style.selectedBg"
:data-is-hovered="$boolAttr(isChannelBgHovered)"
:data-is-focused="$boolAttr(isFocused)"
/>
</div>
</template>
Expand All @@ -60,6 +70,11 @@ import ChannelElementName from './ChannelElementName.vue'
import useNotificationState from '../composables/useNotificationState'
import { useOpenLink } from '/@/composables/useOpenLink'
import useChannelPath from '/@/composables/useChannelPath'
import useFocus from '/@/composables/dom/useFocus'
import {
usePath,
type TypedProps
} from '/@/components/Main/NavigationBar/ChannelList/composables/usePath'
const props = withDefaults(
defineProps<{
Expand All @@ -86,6 +101,11 @@ const isSelected = computed(
props.channel.id === primaryView.value.channelId
)
const onChannelHashKeydownEnter = () => {
if (hasChildren.value) {
emit('clickHash', props.channel.id)
}
}
const onChannelHashClick = (e: MouseEvent) => {
if (hasChildren.value && e.button === LEFT_CLICK_BUTTON) {
emit('clickHash', props.channel.id)
Expand All @@ -100,14 +120,25 @@ const openChannel = (event: MouseEvent) => {
openLink(event, channelIdToLink(props.channel.id))
}
const { pathToShow, pathTooltip } = usePath(props as TypedProps)
const notificationState = useNotificationState(toRef(props, 'channel'))
const { isHovered, onMouseEnter, onMouseLeave } = useHover()
const { isFocused, onFocus, onBlur } = useFocus()
const {
isHovered: isHashHovered,
onMouseEnter: onHashMouseEnter,
onMouseLeave: onHashMouseLeave
} = useHover()
const onHashHovered = () => {
onHashMouseEnter()
onMouseEnter()
}
const onHashHoveredLeave = () => {
onHashMouseLeave()
onMouseLeave()
}
const isChannelBgHovered = computed(
() => isHovered.value && !(hasChildren.value && isHashHovered.value)
)
Expand All @@ -127,22 +158,36 @@ $bgLeftShift: 8px;
&[data-is-inactive] {
@include color-ui-secondary;
}
&[aria-selected='true'] {
&[data-is-selected] {
@include color-accent-primary;
}
}
.channel {
display: flex;
align-items: center;
.channelContainer {
position: relative;
display: flex;
height: $elementHeight;
padding-left: 24px;
padding-right: 4px;
margin-left: $bgLeftShift;
z-index: 0;
&[data-is-inactive] {
@include color-ui-secondary;
}
&[aria-current='page'] {
@include color-accent-primary;
}
}
.channel {
display: flex;
align-items: center;
margin-left: $bgLeftShift;
width: calc(100% - $bgLeftShift);
}
.channelHash {
flex-shrink: 0;
cursor: pointer;
position: absolute;
left: 0;
}
.selectedBg {
position: absolute;
Expand All @@ -157,11 +202,12 @@ $bgLeftShift: 8px;
pointer-events: none;
display: none;
.container[aria-selected='true'] > & {
.container[data-is-selected] > & {
@include background-accent-primary;
display: block;
}
&[data-is-hovered] {
&[data-is-hovered],
&[data-is-focused] {
display: block;
background: $theme-ui-primary-background;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
<template>
<div :class="$style.container">
<component
:is="hasChild ? 'button' : 'div'"
:class="$style.container"
:aria-label="
hasChild && !isOpened
? 'チャンネルツリーを展開'
: hasChild && isOpened
? 'チャンネルツリーを閉じる'
: undefined
"
>
<div
:class="$style.hash"
:data-container-type="hasChild ? 'parent' : 'leaf'"
:data-is-opened="$boolAttr(hasChild && isOpened)"
:aria-selected="isSelected"
:data-is-selected="$boolAttr(isSelected)"
:data-has-notification-on-child="$boolAttr(hasNotificationOnChild)"
:data-is-inactive="$boolAttr(isInactive)"
>
Expand All @@ -13,7 +23,7 @@
<div v-if="hasNotification" :class="$style.indicator">
<notification-indicator :border-width="2" />
</div>
</div>
</component>
</template>

<script lang="ts" setup>
Expand Down Expand Up @@ -70,12 +80,13 @@ withDefaults(
@include color-ui-secondary;
border-color: $theme-ui-secondary-default;
}
&[aria-selected='true'] {
&[data-is-selected] {
@include color-accent-primary;
}
}
&[data-container-type='parent'] {
&:hover::before {
&:hover::before,
.container:focus &::before {
content: '';
border-radius: 4px;
display: block;
Expand All @@ -88,47 +99,54 @@ withDefaults(
&[data-is-opened] {
color: var(--specific-channel-hash-opened);
background: $theme-ui-primary-background;
&:hover::before {
&:hover::before,
.container:focus &::before {
background: $theme-ui-primary-background;
opacity: 0.5;
}
&[data-is-inactive] {
background: $theme-ui-secondary-background;
&:hover::before {
&:hover::before,
.container:focus &::before {
background: $theme-ui-secondary-background;
}
}
&[aria-selected='true'] {
&[data-is-selected] {
@include background-accent-primary;
&:hover::before {
&:hover::before,
.container:focus &::before {
@include background-accent-primary;
}
}
}
&:not([data-is-opened]) {
@include color-ui-primary;
border-color: $theme-ui-primary-default;
&:hover::before {
&:hover::before,
.container:focus &::before {
background: $theme-ui-primary-background;
opacity: 0.2;
}
&[data-is-inactive] {
@include color-ui-secondary;
border-color: $theme-ui-secondary-default;
&:hover::before {
&:hover::before,
.container:focus &::before {
background: $theme-ui-secondary-background;
}
}
&[data-has-notification-on-child] {
border-color: $theme-accent-notification-default;
&:hover::before {
&:hover::before,
.container:focus &::before {
background: $theme-accent-notification-background;
}
}
&[aria-selected='true'] {
&[data-is-selected] {
@include color-accent-primary;
border-color: $theme-accent-primary-default;
&:hover::before {
&:hover::before,
.container:focus &::before {
@include background-accent-primary;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,60 +19,13 @@
</div>
</template>

<script lang="ts">
import { computed, reactive } from 'vue'
import useChannelPath from '/@/composables/useChannelPath'
import { useQallSession } from '../../MainView/ChannelView/ChannelSidebar/composables/useChannelRTCSession'
import type { ChannelTreeNode } from '/@/lib/channelTree'
import type { Channel } from '@traptitech/traq'
interface TreeProps {
channel: ChannelTreeNode
showShortenedPath: false
}
interface ListProps {
channel: Channel
showShortenedPath: true
}
type TypedProps = TreeProps | ListProps
const usePath = (typedProps: TypedProps) => {
const { channelIdToShortPathString, channelIdToPathString } = useChannelPath()
const getPathWithAncestor = (skippedAncestorNames?: string[]) =>
skippedAncestorNames
? [...skippedAncestorNames].reverse().join('/').concat('/')
: ''
const pathToShow = computed(() =>
typedProps.showShortenedPath
? channelIdToShortPathString(typedProps.channel.id)
: getPathWithAncestor(typedProps.channel.skippedAncestorNames) +
typedProps.channel.name
)
const pathTooltip = computed(() =>
typedProps.showShortenedPath
? `#${channelIdToPathString(typedProps.channel.id)}`
: undefined
)
return { pathToShow, pathTooltip }
}
const useRTCState = (typedProps: TypedProps) => {
const { sessionUserIds } = useQallSession(
reactive({ channelId: computed(() => typedProps.channel.id) })
)
return { qallUserIds: sessionUserIds }
}
</script>

<script lang="ts" setup>
import AIcon from '/@/components/UI/AIcon.vue'
import UserIconEllipsisList from '/@/components/UI/UserIconEllipsisList.vue'
import type { ChannelTreeNode } from '/@/lib/channelTree'
import type { Channel } from '@traptitech/traq'
import type { TypedProps } from './composables/usePath'
import { usePath, useRTCState } from './composables/usePath'
const props = withDefaults(
defineProps<{
Expand Down
Loading

0 comments on commit 4b11357

Please sign in to comment.