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

Commit

Permalink
Add a tabbed ContentSettings mobile modal (#1846)
Browse files Browse the repository at this point in the history
* Create a tabbed mobile modal
Use ::after for tab underline and add medium size VTab
Move components to VHeaderMobile directory
Fix tablist styles
Add text button snapshots
Make the modal footer buttons available in the page Tabbing order
* Move the close button closer to the end of the modal
* Close modal on click outside
  • Loading branch information
obulat authored Oct 6, 2022
1 parent 83dbe5d commit 96ed423
Show file tree
Hide file tree
Showing 25 changed files with 421 additions and 63 deletions.
9 changes: 9 additions & 0 deletions src/components/VButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,15 @@ a.button {
@apply border-tx bg-dark-charcoal-10 text-dark-charcoal-40;
}
.text {
@apply border-tx bg-tx px-0 text-sm font-semibold text-pink hover:underline focus-visible:ring focus-visible:ring-pink;
}
.text[disabled='disabled'],
.text[aria-disabled='true'] {
@apply border-tx bg-tx text-dark-charcoal-40;
}
.menu {
@apply border-tx bg-white text-dark-charcoal ring-offset-0;
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/VContentSwitcher/VSearchTypes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
:size="size"
:bordered="bordered"
type="radiogroup"
class="z-10 max-w-full md:w-[260px]"
class="z-10 max-w-full"
>
<div
v-for="(category, index) in contentTypeGroups"
Expand All @@ -16,6 +16,7 @@
}"
>
<h4
v-if="index !== 0"
:class="bordered ? 'ps-0' : 'ps-6'"
class="pt-6 pb-4 text-sr font-semibold uppercase pe-6"
>
Expand Down
35 changes: 31 additions & 4 deletions src/components/VFilters/VSearchGridFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
aria-labelledby="filters-heading"
class="filters py-8 px-10"
>
<div class="mt-2 mb-6 flex items-center justify-between">
<header
v-if="showFilterHeader"
class="mt-2 mb-6 flex items-center justify-between"
>
<h4
id="filters-heading"
class="py-2 text-sr font-semibold uppercase leading-8"
Expand All @@ -21,7 +24,7 @@
>
{{ $t('filter-list.clear') }}
</VButton>
</div>
</header>
<form
ref="filtersFormRef"
class="filters-form"
Expand All @@ -37,7 +40,10 @@
@toggle-filter="toggleFilter"
/>
</form>
<footer v-if="isAnyFilterApplied" class="flex justify-between md:hidden">
<footer
v-if="showFilterHeader && isAnyFilterApplied"
class="flex justify-between md:hidden"
>
<VButton variant="primary" @click="$emit('close')">
{{ $t('filter-list.show') }}
</VButton>
Expand Down Expand Up @@ -73,10 +79,29 @@ export default defineComponent({
VButton,
VFilterChecklist,
},
props: {
/**
* Whether to show the header with the title and the clear button.
*/
showFilterHeader: {
type: Boolean,
default: true,
},
/**
* When the filters are in the sidebar, we change the keyboard tabbing order:
* the focus moves from the Filters button to the filter,
* and from the last tabbable element to the main content on Tab,
* and from the filters to the filters button on Shift Tab.
*/
changeTabOrder: {
type: Boolean,
default: true,
},
},
emits: {
close: defineEvent(),
},
setup() {
setup(props) {
const searchStore = useSearchStore()
const { app, i18n } = useContext()
Expand Down Expand Up @@ -142,6 +167,7 @@ export default defineComponent({
* @param event
*/
const handleTabKey = (event: KeyboardEvent) => {
if (!props.changeTabOrder) return
if (lastFocusableElement.value === event.target) {
event.preventDefault()
focusIn(document.querySelector('main'), Focus.First)
Expand All @@ -153,6 +179,7 @@ export default defineComponent({
* @param event - The keydown event
*/
const handleShiftTabKey = (event: KeyboardEvent) => {
if (!props.changeTabOrder) return
if (
firstFocusableElement.value === event.target &&
!isAnyFilterApplied.value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
<VIconButton
size="new-small"
:icon-props="{ iconPath: sourceIcon }"
:button-props="{ variant: 'menu', pressed: isPressed }"
:has-dot="areFiltersSelected"
:button-props="{ variant: 'menu' }"
:aria-label="
areFiltersSelected
? $t('header.aria.menu-notification')
: $t('header.aria.menu')
"
:aria-haspopup="true"
:aria-pressed="isPressed"
aria-haspopup="dialog"
:aria-expanded="isPressed"
aria-controls="content-settings-modal"
@click="$emit('click')"
/>
Expand Down
151 changes: 151 additions & 0 deletions src/components/VHeader/VHeaderMobile/VContentSettingsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<template>
<VModal
ref="contentSettingsModalRef"
:label="$t('header.aria.menu').toString()"
:hide-on-click-outside="true"
variant="two-thirds"
class="flex items-center"
>
<template #trigger="{ visible, a11Props }">
<VContentSettingsButton
:is-pressed="visible"
v-bind="a11Props"
class="me-2"
/>
</template>
<VTabs
:selected-id="selectedTab"
tablist-style="ps-6 pe-2"
variant="plain"
label="content-settings"
class="flex min-h-0 flex-col"
@change="changeSelectedTab"
>
<template #tabs>
<VTab id="content-settings" size="medium" class="category me-4">{{
$t('search-type.heading')
}}</VTab>
<VTab id="filters" size="medium" class="category">{{
$t('filters.title')
}}</VTab>
<VIconButton
class="self-center ms-auto"
size="search-medium"
:icon-props="{ iconPath: closeIcon }"
:aria-label="$t('browse-page.aria.close')"
@click="closeModal"
/>
</template>
<VTabPanel id="content-settings">
<VSearchTypes size="small" :use-links="true" />
</VTabPanel>
<VTabPanel id="filters">
<VSearchGridFilter
class="!p-0"
:show-filter-header="false"
:change-tab-order="false"
/>
</VTabPanel>
</VTabs>
<footer
class="mt-auto flex h-20 flex-shrink-0 items-center justify-between border-t border-t-dark-charcoal-20 px-6 py-4"
>
<VButton
v-show="showClearFiltersButton"
variant="text"
:disabled="isClearButtonDisabled"
@click="clearFilters"
>{{ clearFiltersLabel }}
</VButton>
<VShowResultsButton :is-fetching="isFetching" @click="closeModal" />
</footer>
</VModal>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from '@nuxtjs/composition-api'
import { useSearchStore } from '~/stores/search'
import { useI18n } from '~/composables/use-i18n'
import VButton from '~/components/VButton.vue'
import VContentSettingsButton from '~/components/VHeader/VHeaderMobile/VContentSettingsButton.vue'
import VIconButton from '~/components/VIconButton/VIconButton.vue'
import VModal from '~/components/VModal/VModal.vue'
import VSearchGridFilter from '~/components/VFilters/VSearchGridFilter.vue'
import VSearchTypes from '~/components/VContentSwitcher/VSearchTypes.vue'
import VShowResultsButton from '~/components/VHeader/VHeaderMobile/VShowResultsButton.vue'
import VTab from '~/components/VTabs/VTab.vue'
import VTabPanel from '~/components/VTabs/VTabPanel.vue'
import VTabs from '~/components/VTabs/VTabs.vue'
import closeIcon from '~/assets/icons/close-small.svg'
export default defineComponent({
name: 'VContentSettingsModal',
components: {
VButton,
VContentSettingsButton,
VIconButton,
VModal,
VSearchGridFilter,
VSearchTypes,
VShowResultsButton,
VTab,
VTabPanel,
VTabs,
},
props: {
isFetching: {
type: Boolean,
default: false,
},
},
setup() {
const contentSettingsModalRef = ref<InstanceType<typeof VModal> | null>(
null
)
const i18n = useI18n()
const searchStore = useSearchStore()
const selectedTab = ref<'content-settings' | 'filters'>('content-settings')
const changeSelectedTab = (tab: 'content-settings' | 'filters') => {
selectedTab.value = tab
}
const showClearFiltersButton = computed(
() => selectedTab.value === 'filters'
)
const isClearButtonDisabled = computed(
() => !searchStore.isAnyFilterApplied
)
const clearFiltersLabel = computed(() =>
searchStore.isAnyFilterApplied
? i18n.t('filter-list.clear-numbered', {
number: searchStore.appliedFilterCount,
})
: i18n.t('filter-list.clear')
)
const clearFilters = () => {
searchStore.clearFilters()
}
const closeModal = () => {
contentSettingsModalRef.value?.close()
}
return {
contentSettingsModalRef,
closeIcon,
closeModal,
selectedTab,
changeSelectedTab,
showClearFiltersButton,
isClearButtonDisabled,
clearFiltersLabel,
clearFilters,
}
},
})
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,7 @@
{{ searchStatus }}
</span>
</VSearchBar>
<VModal
:label="$t('header.aria.menu').toString()"
class="flex items-stretch"
>
<template #trigger="{ visible, a11Props }">
<VContentSettingsButton :is-pressed="visible" v-bind="a11Props" />
</template>
<template #default>
<nav
id="content-switcher-modal"
class="p-6"
aria-labelledby="content-switcher-heading"
>
<VSearchTypes ref="searchTypesRef" size="small" :use-links="true" />
</nav>
</template>
</VModal>
<VContentSettingsModal :is-fetching="isFetching" />
</header>
</template>

Expand All @@ -52,18 +36,19 @@ import {
} from '@nuxtjs/composition-api'
import { ALL_MEDIA, searchPath } from '~/constants/media'
import { isMinScreen } from '~/composables/use-media-query'
import { useMatchSearchRoutes } from '~/composables/use-match-routes'
import { useI18n } from '~/composables/use-i18n'
import { useI18nResultsCount } from '~/composables/use-i18n-utilities'
import { useMediaStore } from '~/stores/media'
import { isSearchTypeSupported, useSearchStore } from '~/stores/search'
import VContentSettingsButton from '~/components/VHeader/VContentSettingsButton.vue'
import { IsHeaderScrolledKey, IsMinScreenMdKey } from '~/types/provides'
import VLogoButton from '~/components/VHeader/VLogoButton.vue'
import VModal from '~/components/VModal/VModal.vue'
import VSearchBar from '~/components/VHeader/VSearchBar/VSearchBar.vue'
import VSearchTypes from '~/components/VContentSwitcher/VSearchTypes.vue'
import VContentSettingsModal from '~/components/VHeader/VHeaderMobile/VContentSettingsModal.vue'
import closeIcon from '~/assets/icons/close.svg'
Expand All @@ -79,13 +64,14 @@ type HeaderMenu = 'filters' | 'content-switcher'
export default defineComponent({
name: 'VHeaderMobile',
components: {
VContentSettingsButton,
VContentSettingsModal,
VLogoButton,
VModal,
VSearchBar,
VSearchTypes,
},
setup() {
const contentSettingsModalRef = ref<InstanceType<typeof VModal> | null>(
null
)
const mediaStore = useMediaStore()
const searchStore = useSearchStore()
const { app } = useContext()
Expand All @@ -94,10 +80,8 @@ export default defineComponent({
const { matches: isSearchRoute } = useMatchSearchRoutes()
const isHeaderScrolled = inject('isHeaderScrolled', false)
const isMinScreenMd = isMinScreen('md', { shouldPassInSSR: true })
const menuModalRef = ref(null)
const isHeaderScrolled = inject(IsHeaderScrolledKey)
const isMinScreenMd = inject(IsMinScreenMdKey)
const openMenu = ref<null | HeaderMenu>(null)
const isMenuOpen = computed(() => openMenu.value !== null)
Expand All @@ -111,6 +95,12 @@ export default defineComponent({
const close = () => {
openMenu.value = null
}
const closeModal = () => {
if (contentSettingsModalRef.value) {
contentSettingsModalRef.value.close()
}
close()
}
const isFetching = computed(() => {
return mediaStore.fetchState.isFetching
Expand Down Expand Up @@ -199,8 +189,9 @@ export default defineComponent({
isSearchRoute,
areFiltersDisabled,
menuModalRef,
contentSettingsModalRef,
closeModal,
openMenu,
openMenuModal,
isMenuOpen,
Expand Down
Loading

0 comments on commit 96ed423

Please sign in to comment.