|
6 | 6 | <slot name="trigger"></slot> |
7 | 7 | </div> |
8 | 8 | <Teleport to="body"> |
| 9 | + <Transition |
| 10 | + enter-from-class="af-modal-hidden opacity-0" |
| 11 | + enter-active-class="transition-opacity duration-150 ease-out motion-reduce:transition-none" |
| 12 | + leave-active-class="transition-opacity duration-100 ease-in motion-reduce:transition-none" |
| 13 | + leave-to-class="af-modal-hidden opacity-0" |
| 14 | + > |
9 | 15 | <div |
10 | 16 | v-show="isModalOpen" |
11 | 17 | v-if="!removeFromDom" |
12 | 18 | @click="backdropClick" |
13 | | - class="bg-zinc-900/40 dark:bg-black/60 overflow-y-auto overscroll-contain overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full h-full md:inset-0 h-1rem max-h-full flex" |
| 19 | + class="afcl-modal-backdrop group/modal fixed inset-0 z-50 flex justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-lightScrim dark:bg-darkScrim max-sm:pt-12" |
14 | 20 | :class="props.backgroundCustomClasses" |
15 | 21 | > |
16 | 22 | <!-- Modal content --> |
17 | | - <div v-bind="$attrs" class="relative bg-lightDialogBackgorund rounded-xl shadow-popover dark:shadow-darkPopover dark:bg-darkDialogBackgorund mx-4 sm:mx-0"> |
| 23 | + <div |
| 24 | + ref="panelRef" |
| 25 | + role="dialog" |
| 26 | + aria-modal="true" |
| 27 | + tabindex="-1" |
| 28 | + v-bind="$attrs" |
| 29 | + class="afcl-modal focus:outline-none transition-transform duration-150 ease-out motion-reduce:transition-none group-[.af-modal-hidden]/modal:translate-y-1 group-[.af-modal-hidden]/modal:scale-[.98] max-sm:group-[.af-modal-hidden]/modal:translate-y-full max-sm:group-[.af-modal-hidden]/modal:scale-100" |
| 30 | + :class="dialogPanelClasses" |
| 31 | + > |
18 | 32 |
|
19 | 33 | <!-- Modal body --> |
20 | 34 | <div class="text-lightDialogBodyText dark:text-darkDialogBodyText"> |
|
24 | 38 | <!-- Confirmation Modal --> |
25 | 39 | <div |
26 | 40 | v-if="showConfirmationOnClose" |
27 | | - class="fixed inset-0 flex items-center justify-center bg-zinc-900/40 dark:bg-black/60 z-[60]" |
| 41 | + class="fixed inset-0 z-[60] flex justify-center overflow-y-auto bg-lightScrim dark:bg-darkScrim max-sm:pt-12" |
28 | 42 | :class="props.modalCustomClasses" |
29 | 43 | > |
30 | | - <div class="bg-lightDialogBackgorund dark:bg-darkDialogBackgorund p-5 rounded-xl shadow-popover dark:shadow-darkPopover max-w-sm w-full"> |
31 | | - <h2 class="text-base font-semibold mb-2 text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ t('Confirm Close') }}</h2> |
32 | | - <p class="mb-5 text-sm text-lightDialogBodyText dark:text-darkDialogBodyText">{{ props.closeConfirmationText }}</p> |
33 | | - <div class="flex justify-end gap-2"> |
| 44 | + <div |
| 45 | + role="alertdialog" |
| 46 | + aria-modal="true" |
| 47 | + :aria-labelledby="confirmationTitleId" |
| 48 | + class="w-full sm:max-w-sm" |
| 49 | + :class="dialogPanelClasses" |
| 50 | + > |
| 51 | + <div class="px-5 py-[18px]"> |
| 52 | + <h2 :id="confirmationTitleId" class="mb-1.5 text-base font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ t('Confirm Close') }}</h2> |
| 53 | + <p class="text-sm text-lightDialogBodyText dark:text-darkDialogBodyText">{{ props.closeConfirmationText }}</p> |
| 54 | + </div> |
| 55 | + <div :class="dialogFooterClasses"> |
34 | 56 | <Button |
35 | 57 | variant="secondary" |
36 | 58 | @click="showConfirmationOnClose = false" |
|
50 | 72 | </div> |
51 | 73 | </div> |
52 | 74 | </div> |
| 75 | + </Transition> |
53 | 76 | </Teleport> |
54 | 77 | </template> |
55 | 78 |
|
56 | 79 | <script setup lang="ts"> |
57 | 80 | import Button from "./Button.vue"; |
58 | | -import { ref, onMounted, nextTick, onUnmounted, computed, type Ref } from 'vue'; |
| 81 | +import { ref, onMounted, nextTick, onUnmounted, computed, watch, useId, type Ref } from 'vue'; |
59 | 82 | import { useI18n } from 'vue-i18n'; |
| 83 | +import { dialogPanelClasses, dialogFooterClasses } from './dialogStyles'; |
60 | 84 |
|
61 | 85 | const isModalOpen = defineModel<boolean>('open', { default: false }); |
62 | 86 | const { t } = useI18n(); |
@@ -92,6 +116,23 @@ const props = withDefaults(defineProps<DialogProps>(), { |
92 | 116 | }) |
93 | 117 |
|
94 | 118 | const showConfirmationOnClose = ref(false); |
| 119 | +const confirmationTitleId = useId(); |
| 120 | +
|
| 121 | +const panelRef = ref<HTMLElement>(); |
| 122 | +let focusBeforeOpen: HTMLElement | null = null; |
| 123 | +
|
| 124 | +// focus goes into the dialog on open (unless the content already took it) and back to the trigger on close |
| 125 | +watch(isModalOpen, async (open) => { |
| 126 | + if (open) { |
| 127 | + focusBeforeOpen = document.activeElement as HTMLElement | null; |
| 128 | + await nextTick(); |
| 129 | + if (!panelRef.value!.contains(document.activeElement)) { |
| 130 | + panelRef.value!.focus({ preventScroll: true }); |
| 131 | + } |
| 132 | + } else { |
| 133 | + focusBeforeOpen?.focus({ preventScroll: true }); |
| 134 | + } |
| 135 | +}); |
95 | 136 |
|
96 | 137 |
|
97 | 138 | async function open() { |
|
0 commit comments