Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Tab order #1394

Merged
merged 12 commits into from
May 11, 2022
32 changes: 18 additions & 14 deletions src/components/VAllResultsGrid/VAllResultsGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
class="results-grid grid grid-cols-2 lg:grid-cols-5 2xl:grid-cols-6 gap-4 mb-4"
>
<VContentLink
v-for="[mediaType, count] in resultCounts"
v-for="([mediaType, count], i) in resultCounts"
:key="mediaType"
:media-type="mediaType"
:results-count="count"
:to="localePath({ path: `/search/${mediaType}`, query: $route.query })"
class="lg:col-span-2"
@shift-tab="handleShiftTab($event, i)"
/>
</div>
<VGridSkeleton
Expand Down Expand Up @@ -39,10 +40,12 @@
</div>
</template>

<script>
<script lang="ts">
import { computed, defineComponent, useContext } from '@nuxtjs/composition-api'

import { useMediaStore } from '~/stores/media'
import { useFocusFilters } from '~/composables/use-focus-filters'
import { Focus } from '~/utils/focus-management'

import VImageCellSquare from '~/components/VAllResultsGrid/VImageCellSquare.vue'
import VAudioCell from '~/components/VAllResultsGrid/VAudioCell.vue'
Expand All @@ -59,31 +62,21 @@ export default defineComponent({
VGridSkeleton,
VContentLink,
},
props: ['canLoadMore'],
setup(_, { emit }) {
setup() {
const { i18n } = useContext()
const mediaStore = useMediaStore()

const onLoadMore = () => {
emit('load-more')
}

/** @type {import('@nuxtjs/composition-api').ComputedRef<boolean>} */
const resultsLoading = computed(() => {
return (
Boolean(mediaStore.fetchState.fetchingError) ||
mediaStore.fetchState.isFetching
)
})

/**
* @type { ComputedRef<import('~/models/media').Media[]> }
*/
const allMedia = computed(() => mediaStore.allMedia)

const isError = computed(() => !!mediaStore.fetchState.fetchingError)

/** @type {import('@nuxtjs/composition-api').ComputedRef<import('~/composables/use-fetch-state').FetchState>} */
const fetchState = computed(() => mediaStore.fetchState)

const errorHeader = computed(() => {
Expand All @@ -96,16 +89,27 @@ export default defineComponent({
const noResults = computed(
() => fetchState.value.isFinished && allMedia.value.length === 0
)
const focusFilters = useFocusFilters()
/**
* Move focus to the filters sidebar if shift-tab is pressed on the first content link.
* @param i - the index of the content link.
* @param event - keydown event
*/
const handleShiftTab = (event: KeyboardEvent, i: number) => {
if (i === 0) {
focusFilters.focusFilterSidebar(event, Focus.Last)
}
}

return {
isError,
errorHeader,
allMedia,
onLoadMore,
fetchState,
resultsLoading,
resultCounts,
noResults,
handleShiftTab,
}
},
})
Expand Down
10 changes: 5 additions & 5 deletions src/components/VAudioTrack/VAudioTrack.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
:aria-label="ariaLabel"
role="region"
v-bind="layoutBasedProps"
@keydown.native="handleKeydown"
@keydown.native.shift.tab.exact="$emit('shift-tab', $event)"
@keydown.native.space="handleSpace"
>
<Component
:is="layoutComponent"
Expand Down Expand Up @@ -57,7 +58,6 @@ import { useActiveMediaStore } from '~/stores/active-media'
import { useMediaStore } from '~/stores/media'

import { AUDIO } from '~/constants/media'
import { keycodes } from '~/constants/key-codes'

import type { AudioDetail } from '~/models/media'
import {
Expand Down Expand Up @@ -385,8 +385,8 @@ export default defineComponent({
: i18n.t('audio-track.aria-label', { title: props.audio.title })
)

const handleKeydown = (event: KeyboardEvent) => {
if (!isBoxed.value || event.key !== keycodes.Spacebar) return
const handleSpace = (event: KeyboardEvent) => {
if (!isBoxed.value) return
event.preventDefault()
status.value = status.value === 'playing' ? 'paused' : 'playing'
handleToggle(status.value)
Expand All @@ -398,7 +398,7 @@ export default defineComponent({
ariaLabel,
handleToggle,
handleSeeked,
handleKeydown,
handleSpace,

currentTime,
duration,
Expand Down
15 changes: 10 additions & 5 deletions src/components/VContentLink/VContentLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<VLink
:href="to"
class="text-dark-charcoal bg-white border border-dark-charcoal/20 rounded-sm flex flex-col md:flex-row md:justify-between items-start md:items-center hover:bg-dark-charcoal hover:text-white hover:no-underline focus:border-tx overflow-hidden py-4 ps-4 pe-12 w-full md:p-6 focus:outline-none focus-visible:ring focus-visible:ring-pink"
@keydown.native.shift.tab.exact="$emit('shift-tab', $event)"
>
<div class="flex flex-col items-start md:flex-row md:items-center">
<VIcon :icon-path="iconPath" />
Expand All @@ -16,11 +17,13 @@
</VLink>
</template>

<script>
import { computed, defineComponent } from '@nuxtjs/composition-api'
<script lang="ts">
import { computed, defineComponent, PropType } from '@nuxtjs/composition-api'

import { useI18nResultsCount } from '~/composables/use-i18n-utilities'
import { AUDIO, IMAGE, supportedMediaTypes } from '~/constants/media'
import { AUDIO, IMAGE, SupportedMediaType } from '~/constants/media'

import { defineEvent } from '~/types/emits'

import VIcon from '~/components/VIcon/VIcon.vue'
import VLink from '~/components/VLink.vue'
Expand All @@ -41,9 +44,8 @@ export default defineComponent({
* One of the media types supported.
*/
mediaType: {
type: String,
type: String as PropType<SupportedMediaType>,
required: true,
validator: (val) => supportedMediaTypes.includes(val),
},
/**
* The number of results that the search returned.
Expand All @@ -59,6 +61,9 @@ export default defineComponent({
type: String,
},
},
emits: {
'shift-tab': defineEvent<[KeyboardEvent]>(),
},
setup(props) {
const iconPath = computed(() => iconMapping[props.mediaType])
const { getI18nCount } = useI18nResultsCount()
Expand Down
94 changes: 60 additions & 34 deletions src/components/VFilters/VFilterChecklist.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<!-- License explanation -->
<VPopover
v-if="filterType === 'licenses'"
:label="$t('browse-page.aria.license-explanation')"
:label="$t('browse-page.aria.license-explanation').toString()"
>
<template #trigger="{ a11yProps }">
<VButton
Expand Down Expand Up @@ -55,8 +55,13 @@
</fieldset>
</template>

<script>
<script lang="ts">
import { computed, defineComponent, PropType } from '@nuxtjs/composition-api'

import { useSearchStore } from '~/stores/search'
import { useI18n } from '~/composables/use-i18n'
import type { NonMatureFilterCategory, FilterItem } from '~/constants/filters'
import { defineEvent } from '~/types/emits'

import VLicenseExplanation from '~/components/VFilters/VLicenseExplanation.vue'
import VCheckbox from '~/components/VCheckbox/VCheckbox.vue'
Expand All @@ -69,7 +74,12 @@ import VPopover from '~/components/VPopover/VPopover.vue'
import helpIcon from '~/assets/icons/help.svg'
import closeSmallIcon from '~/assets/icons/close-small.svg'

export default {
type toggleFilterPayload = {
filterType: NonMatureFilterCategory
code: string
}

export default defineComponent({
name: 'FilterCheckList',
components: {
VCheckbox,
Expand All @@ -81,41 +91,57 @@ export default {
VPopover,
},
props: {
options: { type: Array, required: false },
title: { type: String },
filterType: { type: String, required: true },
disabled: { type: Boolean, default: false },
},
data() {
return {
icons: { help: helpIcon, closeSmall: closeSmallIcon },
}
},
computed: {
itemName() {
return this.filterType === 'searchBy'
? this.$t('filters.search-by.title')
: this.title
options: {
type: Array as PropType<FilterItem[]>,
required: false,
},
title: {
type: String,
},
filterType: {
type: String as PropType<NonMatureFilterCategory>,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: {
'toggle-filter': defineEvent<[toggleFilterPayload]>(),
},
methods: {
itemLabel(item) {
return ['audioProviders', 'imageProviders'].includes(this.filterType)
setup(props, { emit }) {
const i18n = useI18n()
const itemName = computed(() => {
return props.filterType === 'searchBy'
? i18n.t('filters.search-by.title').toString()
: props.title
})

const itemLabel = (item: FilterItem) =>
['audioProviders', 'imageProviders'].includes(props.filterType)
? item.name
: this.$t(item.name)
},
onValueChange({ value }) {
this.$emit('toggle-filter', {
: i18n.t(item.name).toString()

const onValueChange = ({ value }: { value: string }) => {
emit('toggle-filter', {
code: value,
filterType: this.filterType,
filterType: props.filterType,
})
},
isDisabled(item) {
return (
useSearchStore().isFilterDisabled(item, this.filterType) ??
this.disabled
)
},
}

const isDisabled = (item: FilterItem) =>
useSearchStore().isFilterDisabled(item, props.filterType) ??
props.disabled
const icons = { help: helpIcon, closeSmall: closeSmallIcon }

return {
icons,
itemName,
isDisabled,
itemLabel,
onValueChange,
}
},
}
})
</script>
Loading