From e0cc62c3cd86c030d1a930536ac7c27fa2e1ed6e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 3 Mar 2022 23:59:41 +0100 Subject: [PATCH 1/4] Fix `Dialog` cycling (#553) * add tests to verify that tabbing around when using `initialFocus` works * add nesting example to `playground-vue` * fix nested dialog and initialFocus cycling * make React dialog consistent - Disable FocusLock on leaf Dialog's * update changelog --- CHANGELOG.md | 1 + .../src/components/dialog/dialog.test.tsx | 77 +++++-- .../src/components/dialog/dialog.tsx | 3 +- .../src/hooks/use-focus-trap.ts | 15 +- .../src/components/dialog/dialog.test.ts | 88 ++++++-- .../src/components/dialog/dialog.ts | 84 +++++--- .../src/components/focus-trap/focus-trap.ts | 17 +- .../src/components/portal/portal.ts | 4 - .../src/hooks/use-focus-trap.ts | 192 +++++++++++------- .../src/internal/stack-context.ts | 39 ++-- .../src/components/dialog/dialog.vue | 59 +++++- 11 files changed, 403 insertions(+), 176 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d23205c7..1c42adf341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reset Combobox Input when the value gets reset ([#1181](https://github.com/tailwindlabs/headlessui/pull/1181)) - Adjust active {item,option} index ([#1184](https://github.com/tailwindlabs/headlessui/pull/1184)) - Fix re-focusing element after close ([#1186](https://github.com/tailwindlabs/headlessui/pull/1186)) +- Fix `Dialog` cycling ([#553](https://github.com/tailwindlabs/headlessui/pull/553)) ## [@headlessui/react@v1.5.0] - 2022-02-17 diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx index 908023b600..a69e75b8b6 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx @@ -1,4 +1,4 @@ -import React, { createElement, useState } from 'react' +import React, { createElement, useRef, useState } from 'react' import { render } from '@testing-library/react' import { Dialog } from './dialog' @@ -515,6 +515,57 @@ describe('Keyboard interactions', () => { }) ) }) + + describe('`Tab` key', () => { + it( + 'should be possible to tab around when using the initialFocus ref', + suppressConsoleLogs(async () => { + function Example() { + let [isOpen, setIsOpen] = useState(false) + let initialFocusRef = useRef(null) + return ( + <> + + + Contents + + + + + ) + } + render() + + assertDialog({ state: DialogState.InvisibleUnmounted }) + + // Open dialog + await click(document.getElementById('trigger')) + + // Verify it is open + assertDialog({ + state: DialogState.Visible, + attributes: { id: 'headlessui-dialog-1' }, + }) + + // Verify that the input field is focused + assertActiveElement(document.getElementById('b')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('a')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('b')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('a')) + }) + ) + }) }) describe('Mouse interactions', () => { @@ -762,19 +813,17 @@ describe('Nesting', () => { let [showChild, setShowChild] = useState(false) return ( - <> - - - -
-

Level: {level}

- - - -
- {showChild && } -
- + + + +
+

Level: {level}

+ + + +
+ {showChild && } +
) } diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index 72ff1d6554..029ef5ed63 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -165,6 +165,7 @@ let DialogRoot = forwardRefWithAs(function Dialog< `You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${onClose}` ) } + let dialogState = open ? DialogStates.Open : DialogStates.Closed let visible = (() => { if (usesOpenClosedState !== null) { @@ -200,7 +201,7 @@ let DialogRoot = forwardRefWithAs(function Dialog< enabled ? match(position, { parent: FocusTrapFeatures.RestoreFocus, - leaf: FocusTrapFeatures.All, + leaf: FocusTrapFeatures.All & ~FocusTrapFeatures.FocusLock, }) : FocusTrapFeatures.None, { initialFocus, containers } diff --git a/packages/@headlessui-react/src/hooks/use-focus-trap.ts b/packages/@headlessui-react/src/hooks/use-focus-trap.ts index fd99685552..14e2f0109f 100644 --- a/packages/@headlessui-react/src/hooks/use-focus-trap.ts +++ b/packages/@headlessui-react/src/hooks/use-focus-trap.ts @@ -41,9 +41,7 @@ export function useFocusTrap( containers?: MutableRefObject>> } = {} ) { - let restoreElement = useRef( - typeof window !== 'undefined' ? (document.activeElement as HTMLElement) : null - ) + let restoreElement = useRef(null) let previousActiveElement = useRef(null) let mounted = useIsMounted() @@ -54,7 +52,9 @@ export function useFocusTrap( useEffect(() => { if (!featuresRestoreFocus) return - restoreElement.current = document.activeElement as HTMLElement + if (!restoreElement.current) { + restoreElement.current = document.activeElement as HTMLElement + } }, [featuresRestoreFocus]) // Restore the focus when we unmount the component. @@ -70,7 +70,8 @@ export function useFocusTrap( // Handle initial focus useEffect(() => { if (!featuresInitialFocus) return - if (!container.current) return + let containerElement = container.current + if (!containerElement) return let activeElement = document.activeElement as HTMLElement @@ -79,7 +80,7 @@ export function useFocusTrap( previousActiveElement.current = activeElement return // Initial focus ref is already the active element } - } else if (container.current.contains(activeElement)) { + } else if (containerElement.contains(activeElement)) { previousActiveElement.current = activeElement return // Already focused within Dialog } @@ -88,7 +89,7 @@ export function useFocusTrap( if (initialFocus?.current) { focusElement(initialFocus.current) } else { - if (focusIn(container.current, Focus.First) === FocusResult.Error) { + if (focusIn(containerElement, Focus.First) === FocusResult.Error) { console.warn('There are no focusable elements inside the ') } } diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts index 2eedd73252..3aee469583 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.test.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.test.ts @@ -637,6 +637,68 @@ describe('Keyboard interactions', () => { }) ) }) + + describe('`Tab` key', () => { + it( + 'should be possible to tab around when using the initialFocus ref', + suppressConsoleLogs(async () => { + renderTemplate({ + template: ` +
+ + + Contents + + + +
+ `, + setup() { + let isOpen = ref(false) + let initialFocusRef = ref(null) + return { + isOpen, + initialFocusRef, + setIsOpen(value: boolean) { + isOpen.value = value + }, + toggleOpen() { + isOpen.value = !isOpen.value + }, + } + }, + }) + + assertDialog({ state: DialogState.InvisibleUnmounted }) + + // Open dialog + await click(document.getElementById('trigger')) + + // Verify it is open + assertDialog({ + state: DialogState.Visible, + attributes: { id: 'headlessui-dialog-1' }, + }) + + // Verify that the input field is focused + assertActiveElement(document.getElementById('b')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('a')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('b')) + + // Verify that we can tab around + await press(Keys.Tab) + assertActiveElement(document.getElementById('a')) + }) + ) + }) }) describe('Mouse interactions', () => { @@ -950,31 +1012,13 @@ describe('Nesting', () => { return () => { let level = props.level ?? 1 - return h(Dialog, { open: true, onClose: onClose }, () => [ + return h(Dialog, { open: true, onClose }, () => [ h(DialogOverlay), h('div', [ h('p', `Level: ${level}`), - h( - 'button', - { - onClick: () => (showChild.value = true), - }, - `Open ${level + 1} a` - ), - h( - 'button', - { - onClick: () => (showChild.value = true), - }, - `Open ${level + 1} b` - ), - h( - 'button', - { - onClick: () => (showChild.value = true), - }, - `Open ${level + 1} c` - ), + h('button', { onClick: () => (showChild.value = true) }, `Open ${level + 1} a`), + h('button', { onClick: () => (showChild.value = true) }, `Open ${level + 1} b`), + h('button', { onClick: () => (showChild.value = true) }, `Open ${level + 1} c`), ]), showChild.value && h(Nested, { diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index 93d7c21bde..63bc16f3da 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -7,7 +7,6 @@ import { nextTick, onMounted, onUnmounted, - onUpdated, provide, ref, watchEffect, @@ -21,7 +20,7 @@ import { import { render, Features } from '../../utils/render' import { Keys } from '../../keyboard' import { useId } from '../../hooks/use-id' -import { useFocusTrap } from '../../hooks/use-focus-trap' +import { useFocusTrap, Features as FocusTrapFeatures } from '../../hooks/use-focus-trap' import { useInertOthers } from '../../hooks/use-inert-others' import { useWindowEvent } from '../../hooks/use-window-event' import { Portal, PortalGroup } from '../portal/portal' @@ -76,7 +75,7 @@ export let Dialog = defineComponent({ }, emits: { close: (_close: boolean) => true }, setup(props, { emit, attrs, slots }) { - let containers = ref>(new Set()) + let nestedDialogCount = ref(0) let usesOpenClosedState = useOpenClosed() let open = computed(() => { @@ -90,6 +89,9 @@ export let Dialog = defineComponent({ return props.open }) + let containers = ref>>(new Set()) + let internalDialogRef = ref(null) + // Validations let hasOpen = props.open !== Missing || usesOpenClosedState !== null @@ -105,7 +107,7 @@ export let Dialog = defineComponent({ ) } - let dialogState = computed(() => (props.open ? DialogStates.Open : DialogStates.Closed)) + let dialogState = computed(() => (open.value ? DialogStates.Open : DialogStates.Closed)) let visible = computed(() => { if (usesOpenClosedState !== null) { return usesOpenClosedState.value === State.Open @@ -113,27 +115,52 @@ export let Dialog = defineComponent({ return dialogState.value === DialogStates.Open }) - let internalDialogRef = ref(null) - let enabled = ref(dialogState.value === DialogStates.Open) - - onUpdated(() => { - enabled.value = dialogState.value === DialogStates.Open - }) - let id = `headlessui-dialog-${useId()}` - let focusTrapOptions = computed(() => ({ initialFocus: props.initialFocus })) - - useFocusTrap(containers, enabled, focusTrapOptions) - useInertOthers(internalDialogRef, enabled) - useStackProvider((message, element) => { - return match(message, { - [StackMessage.AddElement]() { - containers.value.add(element) - }, - [StackMessage.RemoveElement]() { - containers.value.delete(element) - }, - }) + let enabled = computed(() => dialogState.value === DialogStates.Open) + + let hasNestedDialogs = computed(() => nestedDialogCount.value > 1) // 1 is the current dialog + let hasParentDialog = inject(DialogContext, null) !== null + + // If there are multiple dialogs, then you can be the root, the leaf or one + // in between. We only care abou whether you are the top most one or not. + let position = computed(() => (!hasNestedDialogs.value ? 'leaf' : 'parent')) + + useFocusTrap( + internalDialogRef, + computed(() => { + return enabled.value + ? match(position.value, { + parent: FocusTrapFeatures.RestoreFocus, + leaf: FocusTrapFeatures.All & ~FocusTrapFeatures.FocusLock, + }) + : FocusTrapFeatures.None + }), + computed(() => ({ + initialFocus: ref(props.initialFocus), + containers, + })) + ) + useInertOthers( + internalDialogRef, + computed(() => (hasNestedDialogs.value ? enabled.value : false)) + ) + useStackProvider({ + type: 'Dialog', + element: internalDialogRef, + onUpdate: (message, type, element) => { + if (type !== 'Dialog') return + + return match(message, { + [StackMessage.Add]() { + containers.value.add(element) + nestedDialogCount.value += 1 + }, + [StackMessage.Remove]() { + containers.value.delete(element) + nestedDialogCount.value -= 1 + }, + }) + }, }) let describedby = useDescriptions({ @@ -141,6 +168,8 @@ export let Dialog = defineComponent({ slot: computed(() => ({ open: open.value })), }) + let id = `headlessui-dialog-${useId()}` + let titleId = ref(null) let api = { @@ -158,9 +187,9 @@ export let Dialog = defineComponent({ provide(DialogContext, api) // Handle outside click - useOutsideClick(containers.value, (_event, target) => { + useOutsideClick(internalDialogRef, (_event, target) => { if (dialogState.value !== DialogStates.Open) return - if (containers.value.size !== 1) return + if (hasNestedDialogs.value) return api.close() nextTick(() => target?.focus()) @@ -170,7 +199,7 @@ export let Dialog = defineComponent({ useWindowEvent('keydown', (event) => { if (event.key !== Keys.Escape) return if (dialogState.value !== DialogStates.Open) return - if (containers.value.size > 1) return // 1 is myself, otherwise other elements in the Stack + if (hasNestedDialogs.value) return event.preventDefault() event.stopPropagation() api.close() @@ -179,6 +208,7 @@ export let Dialog = defineComponent({ // Scroll lock watchEffect((onInvalidate) => { if (dialogState.value !== DialogStates.Open) return + if (hasParentDialog) return let overflow = document.documentElement.style.overflow let paddingRight = document.documentElement.style.paddingRight diff --git a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts index dd9bc87eb1..c9de90e9c3 100644 --- a/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts +++ b/packages/@headlessui-vue/src/components/focus-trap/focus-trap.ts @@ -1,8 +1,6 @@ import { computed, defineComponent, - onMounted, - onUnmounted, ref, // Types @@ -18,21 +16,10 @@ export let FocusTrap = defineComponent({ initialFocus: { type: Object as PropType, default: null }, }, setup(props, { attrs, slots }) { - let containers = ref(new Set()) let container = ref(null) - let enabled = ref(true) - let focusTrapOptions = computed(() => ({ initialFocus: props.initialFocus })) - onMounted(() => { - if (!container.value) return - containers.value.add(container.value) - - useFocusTrap(containers, enabled, focusTrapOptions) - }) - - onUnmounted(() => { - enabled.value = false - }) + let focusTrapOptions = computed(() => ({ initialFocus: ref(props.initialFocus) })) + useFocusTrap(container, FocusTrap.All, focusTrapOptions) return () => { let slot = {} diff --git a/packages/@headlessui-vue/src/components/portal/portal.ts b/packages/@headlessui-vue/src/components/portal/portal.ts index de77c71613..88cdf6e539 100644 --- a/packages/@headlessui-vue/src/components/portal/portal.ts +++ b/packages/@headlessui-vue/src/components/portal/portal.ts @@ -14,7 +14,6 @@ import { PropType, } from 'vue' import { render } from '../../utils/render' -import { useElemenStack, useStackProvider } from '../../internal/stack-context' import { usePortalRoot } from '../../internal/portal-force-root' // --- @@ -51,7 +50,6 @@ export let Portal = defineComponent({ }) let element = ref(null) - useElemenStack(element) onUnmounted(() => { let root = document.getElementById('headlessui-portal-root') @@ -63,8 +61,6 @@ export let Portal = defineComponent({ } }) - useStackProvider() - return () => { if (myTarget.value === null) return null diff --git a/packages/@headlessui-vue/src/hooks/use-focus-trap.ts b/packages/@headlessui-vue/src/hooks/use-focus-trap.ts index 9032256e38..ad7b84fc1c 100644 --- a/packages/@headlessui-vue/src/hooks/use-focus-trap.ts +++ b/packages/@headlessui-vue/src/hooks/use-focus-trap.ts @@ -1,8 +1,8 @@ import { - onUnmounted, - onUpdated, + computed, + onMounted, ref, - watchEffect, + watch, // Types Ref, @@ -11,89 +11,132 @@ import { import { Keys } from '../keyboard' import { focusElement, focusIn, Focus, FocusResult } from '../utils/focus-management' import { useWindowEvent } from '../hooks/use-window-event' -import { contains } from '../internal/dom-containers' +// import { contains } from '../internal/dom-containers' + +export enum Features { + /** No features enabled for the `useFocusTrap` hook. */ + None = 1 << 0, + + /** Ensure that we move focus initially into the container. */ + InitialFocus = 1 << 1, + + /** Ensure that pressing `Tab` and `Shift+Tab` is trapped within the container. */ + TabLock = 1 << 2, + + /** Ensure that programmatically moving focus outside of the container is disallowed. */ + FocusLock = 1 << 3, + + /** Ensure that we restore the focus when unmounting the component that uses this `useFocusTrap` hook. */ + RestoreFocus = 1 << 4, + + /** Enable all features. */ + All = InitialFocus | TabLock | FocusLock | RestoreFocus, +} export function useFocusTrap( - containers: Ref>, - enabled: Ref = ref(true), - options: Ref<{ initialFocus?: HTMLElement | null }> = ref({}) + container: Ref, + features: Ref = ref(Features.All), + options: Ref<{ + initialFocus?: Ref + containers?: Ref>> + }> = ref({}) ) { let restoreElement = ref(null) let previousActiveElement = ref(null) + // Deliberately not using a ref, we don't want to trigger re-renders. + let mounted = { value: false } - function handleFocus() { - if (!enabled.value) return - if (containers.value.size !== 1) return - - let { initialFocus } = options.value - let activeElement = document.activeElement as HTMLElement + let featuresRestoreFocus = computed(() => Boolean(features.value & Features.RestoreFocus)) + let featuresInitialFocus = computed(() => Boolean(features.value & Features.InitialFocus)) - if (initialFocus) { - if (initialFocus === activeElement) { - return // Initial focus ref is already the active element - } - } else if (contains(containers.value, activeElement)) { - return // Already focused within Dialog - } + onMounted(() => { + // Capture the currently focused element, before we enable the focus trap. + watch( + featuresRestoreFocus, + (newValue, prevValue) => { + if (newValue === prevValue) return + if (!featuresRestoreFocus.value) return - if (!restoreElement.value) { - // We already have a restore element - restoreElement.value = activeElement - } + mounted.value = true - // Try to focus the initialFocus ref - if (initialFocus) { - focusElement(initialFocus) - } else { - let couldFocus = false - for (let container of containers.value) { - let result = focusIn(container, Focus.First) - if (result === FocusResult.Success) { - couldFocus = true - break + if (!restoreElement.value) { + restoreElement.value = document.activeElement as HTMLElement + } + }, + { immediate: true } + ) + + // Restore the focus when we unmount the component. + watch( + featuresRestoreFocus, + (newValue, prevValue, onInvalidate) => { + if (newValue === prevValue) return + if (!featuresRestoreFocus.value) return + + onInvalidate(() => { + if (mounted.value === false) return + mounted.value = false + + focusElement(restoreElement.value) + restoreElement.value = null + }) + }, + { immediate: true } + ) + + // Handle initial focus + watch( + [container, options, options.value.initialFocus, featuresInitialFocus], + (newValues, prevValues) => { + if (newValues.every((value, idx) => prevValues?.[idx] === value)) return + if (!featuresInitialFocus.value) return + + let containerElement = container.value + if (!containerElement) return + + let activeElement = document.activeElement as HTMLElement + + if (options.value.initialFocus?.value) { + if (options.value.initialFocus?.value === activeElement) { + previousActiveElement.value = activeElement + return // Initial focus ref is already the active element + } + } else if (containerElement.contains(activeElement)) { + previousActiveElement.value = activeElement + return // Already focused within Dialog } - } - - if (!couldFocus) console.warn('There are no focusable elements inside the ') - } - - previousActiveElement.value = document.activeElement as HTMLElement - } - - // Restore when `enabled` becomes false - function restore() { - focusElement(restoreElement.value) - restoreElement.value = null - previousActiveElement.value = null - } - // Handle initial focus - watchEffect(handleFocus) + // Try to focus the initialFocus ref + if (options.value.initialFocus?.value) { + focusElement(options.value.initialFocus.value) + } else { + if (focusIn(containerElement, Focus.First) === FocusResult.Error) { + console.warn('There are no focusable elements inside the ') + } + } - onUpdated(() => { - enabled.value ? handleFocus() : restore() + previousActiveElement.value = document.activeElement as HTMLElement + }, + { immediate: true } + ) }) - onUnmounted(restore) // Handle Tab & Shift+Tab keyboard events useWindowEvent('keydown', (event) => { - if (!enabled.value) return + if (!(features.value & Features.TabLock)) return + + if (!container.value) return if (event.key !== Keys.Tab) return - if (!document.activeElement) return - if (containers.value.size !== 1) return event.preventDefault() - for (let element of containers.value) { - let result = focusIn( - element, + if ( + focusIn( + container.value, (event.shiftKey ? Focus.Previous : Focus.Next) | Focus.WrapAround - ) - - if (result === FocusResult.Success) { - previousActiveElement.value = document.activeElement as HTMLElement - break - } + ) === FocusResult.Success + ) { + previousActiveElement.value = document.activeElement as HTMLElement } }) @@ -101,16 +144,21 @@ export function useFocusTrap( useWindowEvent( 'focus', (event) => { - if (!enabled.value) return - if (containers.value.size !== 1) return + if (!(features.value & Features.FocusLock)) return + + let allContainers = new Set(options.value.containers?.value) + allContainers.add(container) + + if (!allContainers.size) return let previous = previousActiveElement.value if (!previous) return + if (!mounted.value) return let toElement = event.target as HTMLElement | null if (toElement && toElement instanceof HTMLElement) { - if (!contains(containers.value, toElement)) { + if (!contains(allContainers, toElement)) { event.preventDefault() event.stopPropagation() focusElement(previous) @@ -125,3 +173,11 @@ export function useFocusTrap( true ) } + +function contains(containers: Set>, element: HTMLElement) { + for (let container of containers) { + if (container.value?.contains(element)) return true + } + + return false +} diff --git a/packages/@headlessui-vue/src/internal/stack-context.ts b/packages/@headlessui-vue/src/internal/stack-context.ts index 595a8756fd..ada634ea77 100644 --- a/packages/@headlessui-vue/src/internal/stack-context.ts +++ b/packages/@headlessui-vue/src/internal/stack-context.ts @@ -1,39 +1,36 @@ import { inject, provide, - watchEffect, + onMounted, + onUnmounted, // Types InjectionKey, Ref, } from 'vue' -type OnUpdate = (message: StackMessage, element: HTMLElement) => void +type OnUpdate = (message: StackMessage, type: string, element: Ref) => void let StackContext = Symbol('StackContext') as InjectionKey export enum StackMessage { - AddElement, - RemoveElement, + Add, + Remove, } export function useStackContext() { return inject(StackContext, () => {}) } -export function useElemenStack(element: Ref | null) { - let notify = useStackContext() - - watchEffect((onInvalidate) => { - let domElement = element?.value - if (!domElement) return - - notify(StackMessage.AddElement, domElement) - onInvalidate(() => notify(StackMessage.RemoveElement, domElement!)) - }) -} - -export function useStackProvider(onUpdate?: OnUpdate) { +export function useStackProvider({ + type, + element, + onUpdate, +}: { + type: string + element: Ref + onUpdate?: OnUpdate +}) { let parentUpdate = useStackContext() function notify(...args: Parameters) { @@ -44,5 +41,13 @@ export function useStackProvider(onUpdate?: OnUpdate) { parentUpdate(...args) } + onMounted(() => { + notify(StackMessage.Add, type, element) + + onUnmounted(() => { + notify(StackMessage.Remove, type, element) + }) + }) + provide(StackContext, notify) } diff --git a/packages/playground-vue/src/components/dialog/dialog.vue b/packages/playground-vue/src/components/dialog/dialog.vue index 0fed689c9b..df77e1f499 100644 --- a/packages/playground-vue/src/components/dialog/dialog.vue +++ b/packages/playground-vue/src/components/dialog/dialog.vue @@ -7,6 +7,9 @@ Toggle! + + +
@@ -177,7 +180,7 @@