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

Commit

Permalink
Use a modal for external sources on mobile (#2078)
Browse files Browse the repository at this point in the history
* Use a modal for external sources on mobile

* Add aria-controls

* Fix the handling of `aria-controls`

Bind `attrs` to the actual popover/modal element instead of the parent that's not-visible

* Fix `full` modal width

* Update tests and snapshots

* Update snapshot

* Lint

* Update snapshots

* Fix types

* Fix scroll lock; button width & position change
  • Loading branch information
obulat authored Feb 1, 2023
1 parent 13ea928 commit 4fbfe92
Show file tree
Hide file tree
Showing 27 changed files with 219 additions and 115 deletions.
7 changes: 7 additions & 0 deletions src/components/VButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,13 @@ https://www.figma.com/file/GIIQ4sDbaToCfFQyKMvzr8/Openverse-Design-Library?node-
@apply w-full bg-dark-charcoal-06 font-semibold text-dark-charcoal;
}
.dropdown-label {
@apply border border-dark-charcoal-20 text-dark-charcoal focus-slim-tx hover:border-tx hover:bg-dark-charcoal hover:text-white;
}
.dropdown-label-pressed {
@apply border-tx bg-dark-charcoal text-white focus-bold-filled active:hover:border-white;
}
.connection-start {
@apply rounded-s-none;
}
Expand Down
108 changes: 101 additions & 7 deletions src/components/VExternalSearch/VExternalSearchForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<section
:key="type"
ref="sectionRef"
class="external-sources flex flex-row place-items-center justify-center p-4"
class="external-sources flex flex-row place-items-center justify-center py-4"
data-testid="external-sources-form"
@keydown.tab.exact="handleTab"
>
Expand Down Expand Up @@ -33,10 +33,56 @@
<template #query>{{ searchTerm }}</template>
</i18n>

<VExternalSourceList
class="inline-flex ms-2 md:justify-center"
:external-sources="externalSources"
/>
<VButton
id="external-sources-button"
ref="triggerRef"
:pressed="triggerA11yProps['aria-expanded']"
aria-haspopup="dialog"
:aria-controls="
isMd ? 'external-sources-popover' : 'external-sources-modal'
"
variant="dropdown-label"
size="disabled"
class="caption-regular min-w-max gap-1 py-1 px-3 text-dark-charcoal pe-1 ms-2 focus-visible:border-tx"
@click="onTriggerClick"
>{{ $t("external-sources.button").toString()
}}<VIcon
class="text-dark-charcoal-40"
:class="{ 'text-white': triggerA11yProps['aria-expanded'] }"
:icon-path="caretDownIcon"
/>
</VButton>
<template v-if="triggerElement">
<VPopoverContent
v-if="isMd"
id="external-sources-popover"
aria-labelledby="external-sources-button"
:hide="closeDialog"
:trigger-element="triggerElement"
:visible="isVisible"
z-index="popover"
>
<VExternalSourceList
class="flex flex-col"
:external-sources="externalSources"
@close="closeDialog"
/></VPopoverContent>
<VModalContent
v-else
id="external-sources-modal"
aria-labelledby="external-sources-button"
:trigger-element="triggerElement"
:hide="closeDialog"
:visible="isVisible"
variant="centered"
>
<VExternalSourceList
class="flex-col justify-center"
:external-sources="externalSources"
@close="closeDialog"
/>
</VModalContent>
</template>
</section>
</template>

Expand All @@ -46,20 +92,34 @@ import {
defineComponent,
PropType,
ref,
SetupContext,
} from "@nuxtjs/composition-api"
import { getFocusableElements } from "~/utils/focus-management"
import { defineEvent } from "~/types/emits"
import type { MediaType } from "~/constants/media"
import { useUiStore } from "~/stores/ui"
import { useDialogControl } from "~/composables/use-dialog-control"
import type { MediaType } from "~/constants/media"
import type { ExternalSource } from "~/types/external-source"
import VExternalSourceList from "~/components/VExternalSearch/VExternalSourceList.vue"
import VButton from "~/components/VButton.vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import VPopoverContent from "~/components/VPopover/VPopoverContent.vue"
import VModalContent from "~/components/VModal/VModalContent.vue"
import caretDownIcon from "~/assets/icons/caret-down.svg"
export default defineComponent({
name: "VExternalSearchForm",
components: {
VModalContent,
VPopoverContent,
VIcon,
VButton,
VExternalSourceList,
},
props: {
Expand Down Expand Up @@ -88,7 +148,29 @@ export default defineComponent({
tab: defineEvent<[KeyboardEvent]>(),
},
setup(_, { emit }) {
const sectionRef = ref<HTMLElement>()
const sectionRef = ref<HTMLElement | null>(null)
const triggerRef = ref<InstanceType<typeof VButton> | null>(null)
const uiStore = useUiStore()
const isMd = computed(() => uiStore.isBreakpoint("md"))
const triggerElement = computed(() => triggerRef.value?.$el as HTMLElement)
const lockBodyScroll = computed(() => !isMd.value)
const isVisible = ref(false)
const {
close: closeDialog,
open: openDialog,
onTriggerClick,
triggerA11yProps,
} = useDialogControl({
visibleRef: isVisible,
nodeRef: sectionRef,
lockBodyScroll,
emit: emit as SetupContext["emit"],
})
/**
* Find the last focusable element in VSearchGridFilter to add a 'Tab' keydown event
Expand All @@ -107,7 +189,19 @@ export default defineComponent({
}
return {
sectionRef,
triggerRef,
triggerElement,
handleTab,
isMd,
closeDialog,
openDialog,
onTriggerClick,
triggerA11yProps,
isVisible,
caretDownIcon,
}
},
})
Expand Down
92 changes: 32 additions & 60 deletions src/components/VExternalSearch/VExternalSourceList.vue
Original file line number Diff line number Diff line change
@@ -1,62 +1,35 @@
<template>
<VPopover
ref="sourceListPopover"
class="flex items-stretch"
:label="$t('external-sources.button').toString()"
placement="top-start"
>
<template #trigger="{ a11yProps }">
<VButton
:pressed="a11yProps['aria-expanded']"
:aria-haspopup="a11yProps['aria-haspopup']"
aria-controls="source-list-popover"
variant="action-menu-bordered"
size="disabled"
class="caption-regular justify-between py-1 px-3 text-dark-charcoal pe-2"
>{{ $t("external-sources.button").toString() }}
<VIcon
class="text-dark-charcoal-40 ms-1"
:class="a11yProps['aria-expanded'] ? 'rotate-180' : 'rotate-0'"
:icon-path="icons.caretDown"
/></VButton>
</template>
<template #default="{ close }">
<div
class="relative max-w-[280px] p-2 pt-0"
data-testid="source-list-popover"
>
<VIconButton
class="absolute top-0 border-none text-dark-charcoal-70 end-0"
:icon-props="{ iconPath: icons.closeSmall }"
:aria-label="$t('modal.close').toString()"
@click="close"
/>
<h2 class="description-bold mb-2 px-4 pt-5 text-start">
{{ $t("external-sources.title") }}
</h2>
<p class="caption-regular mb-4 px-4 text-start">
{{ $t("external-sources.caption", { openverse: "Openverse" }) }}
</p>
<VButton
v-for="source in externalSources"
:key="source.name"
as="VLink"
variant="plain"
size="disabled"
class="caption-bold w-full justify-between px-4 py-3 text-dark-charcoal hover:bg-dark-charcoal-10"
:href="source.url"
>
{{ source.name }}
<VIcon
:icon-path="icons.externalLink"
:size="4"
:rtl-flip="true"
class="ms-2"
/>
</VButton>
</div>
</template>
</VPopover>
<div class="relative max-w-[280px]" data-testid="source-list-popover">
<VIconButton
class="absolute top-0 border-none text-dark-charcoal-70 end-0"
:icon-props="{ iconPath: icons.closeSmall }"
:aria-label="$t('modal.close').toString()"
@click="$emit('close')"
/>
<h2 class="description-bold mb-2 px-4 pt-5 text-start">
{{ $t("external-sources.title") }}
</h2>
<p class="caption-regular mb-4 px-4 text-start">
{{ $t("external-sources.caption", { openverse: "Openverse" }) }}
</p>
<VButton
v-for="source in externalSources"
:key="source.name"
as="VLink"
variant="plain"
size="disabled"
class="caption-bold w-full justify-between px-4 py-3 text-dark-charcoal hover:bg-dark-charcoal-10"
:href="source.url"
>
{{ source.name }}
<VIcon
:icon-path="icons.externalLink"
:size="4"
:rtl-flip="true"
class="ms-2"
/>
</VButton>
</div>
</template>

<script lang="ts">
Expand All @@ -67,7 +40,6 @@ import type { ExternalSource } from "~/types/external-source"
import VButton from "~/components/VButton.vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import VIconButton from "~/components/VIconButton/VIconButton.vue"
import VPopover from "~/components/VPopover/VPopover.vue"
import externalLinkIcon from "~/assets/icons/external-link.svg"
import caretDownIcon from "~/assets/icons/caret-down.svg"
Expand All @@ -79,7 +51,7 @@ import closeSmallIcon from "~/assets/icons/close-small.svg"
*/
export default defineComponent({
name: "VExternalSourceList",
components: { VButton, VIcon, VIconButton, VPopover },
components: { VButton, VIcon, VIconButton },
props: {
/**
* the media type to use as the criteria for filtering additional sources
Expand Down
21 changes: 17 additions & 4 deletions src/components/VModal/VModalContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<div
ref="dialogRef"
v-bind="$attrs"
class="flex w-full flex-col"
class="flex flex-col"
:class="[$style[`modal-${variant}`], $style[`modal-${mode}`]]"
role="dialog"
aria-modal="true"
Expand Down Expand Up @@ -91,6 +91,7 @@ import closeIcon from "~/assets/icons/close.svg"
export default defineComponent({
name: "VModalContent",
components: { VTeleport, VButton, VIcon, VLogoButtonOld },
inheritAttrs: false,
props: {
visible: {
type: Boolean,
Expand Down Expand Up @@ -193,12 +194,23 @@ export default defineComponent({
.top-bar-two-thirds {
@apply bg-tx;
}
.modal-backdrop-fit-content,
.modal-backdrop-two-thirds {
@apply bg-dark-charcoal bg-opacity-75;
}
.modal-backdrop-centered {
@apply flex-col items-center;
}
.modal-default {
@apply md:max-w-[768px] lg:w-[768px] xl:w-[1024px] xl:max-w-[1024px];
@apply w-full md:max-w-[768px] lg:w-[768px] xl:w-[1024px] xl:max-w-[1024px];
}
.modal-full {
@apply w-full;
}
.modal-two-thirds {
@apply mt-auto h-2/3 w-full rounded-t-lg bg-white;
}
.modal-dark {
Expand All @@ -207,11 +219,12 @@ export default defineComponent({
.modal-light {
@apply bg-white text-dark-charcoal;
}
.modal-content-default {
@apply text-left align-bottom md:rounded-t-md;
}
.modal-two-thirds {
@apply mt-auto h-2/3 w-full rounded-t-lg bg-white;
.modal-content-centered {
@apply w-auto;
}
.modal-fit-content {
@apply mt-auto w-full rounded-t-lg bg-white;
Expand Down
2 changes: 2 additions & 0 deletions src/components/VPopover/VPopoverContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
:class="[`z-${zIndex}`, width]"
:style="heightProperties"
:tabindex="-1"
v-bind="$attrs"
@blur="onBlur"
>
<slot />
Expand Down Expand Up @@ -44,6 +45,7 @@ export const VPopoverContentContextKey = Symbol(
export default defineComponent({
name: "VPopoverContent",
inheritAttrs: false,
props: {
visible: {
type: Boolean,
Expand Down
7 changes: 7 additions & 0 deletions src/composables/use-dialog-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export function useDialogControl({
unlock = bodyScroll.unlock
}
const shouldLockBodyScroll = computed(() => unref(lockBodyScroll) ?? false)
watch(shouldLockBodyScroll, (shouldLock) => {
if (shouldLock) {
if (internalVisibleRef.value) lock()
} else {
unlock()
}
})

const open = () => (internalVisibleRef.value = true)

Expand Down
2 changes: 2 additions & 0 deletions src/types/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const buttonVariants = [
"plain",
"plain--avoid",
"full",
"dropdown-label",
"dropdown-label-pressed",
] as const
export type ButtonVariant = typeof buttonVariants[number]

Expand Down
7 changes: 6 additions & 1 deletion src/types/modal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Ref } from "@nuxtjs/composition-api"

export type ModalVariant = "default" | "full" | "two-thirds" | "fit-content"
export type ModalVariant =
| "default"
| "full"
| "two-thirds"
| "fit-content"
| "centered"
export type ModalColorMode = "dark" | "light"

export type DialogOptions = {
Expand Down
Loading

0 comments on commit 4fbfe92

Please sign in to comment.