From cf5ab9d80f9c7a04ff75bd3ac9b071b4b7db1342 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Mon, 2 Nov 2026 10:09:22 -0800 Subject: [PATCH 1/3] fix(HoverCard): Escape and focus work in the card's own document component:HoverCard FR6: Escape closes a visible card through the shared dismissal stack. The stack listens in each layer's own document, found through getContainer, but useHoverCard passed none and read the page's document for presence, the focus check, and the focus-into-card check. A card rendered into an iframe or a Picture-in-Picture window therefore never closed on Escape there (WCAG 1.4.13), and focus moving into it closed it. useHoverCard now registers with getContainer: () => triggerRef.current and reads the trigger's ownerDocument wherever it read the page's. Tests: three jsdom cases in a real second document (red before), a Chromium spec over a new no-visual iframe story. Record: FR6 loses its gap sentence, AR2 and Current-state impact drop the other-document gap, and the verification row names the cases. --- .changeset/hovercard-second-document.md | 7 + .../HoverCardSecondDocumentA11y.stories.tsx | 81 +++++++++++ .../components/HoverCard/HoverCard.spec.md | 31 ++-- .../core/src/HoverCard/HoverCard.test.tsx | 135 ++++++++++++++++++ ...erCardSecondDocument.a11y.chromium.spec.ts | 73 ++++++++++ packages/core/src/HoverCard/useHoverCard.tsx | 40 ++++-- 6 files changed, 334 insertions(+), 33 deletions(-) create mode 100644 .changeset/hovercard-second-document.md create mode 100644 apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx create mode 100644 packages/core/src/HoverCard/__tests__/HoverCardSecondDocument.a11y.chromium.spec.ts diff --git a/.changeset/hovercard-second-document.md b/.changeset/hovercard-second-document.md new file mode 100644 index 0000000000000..079bf59a52002 --- /dev/null +++ b/.changeset/hovercard-second-document.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] A `HoverCard` rendered into another document, such as an iframe or a Picture-in-Picture window, now closes when Escape is pressed there, and Escape from inside the card returns focus to its trigger. Moving focus from the trigger into such a card also keeps it open. Before, the card read only the page's document, so Escape in the other document never closed it. + +@cixzhang @duytbp diff --git a/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx b/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx new file mode 100644 index 0000000000000..683c3088dfc62 --- /dev/null +++ b/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx @@ -0,0 +1,81 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file HoverCardSecondDocumentA11y.stories.tsx + * @input HoverCard rendered in the page and portalled into a same-origin + * iframe + * @output A fixture for the Chromium check that Escape and focus work in the + * card's own document; excluded from the visual matrix + * @position Browser fixture for component:HoverCard FR6 in a second document + */ + +import {useCallback, useState, type SyntheticEvent} from 'react'; +import {createPortal} from 'react-dom'; +import type {Meta, StoryObj} from '@storybook/react'; +import {Button} from '@astryxdesign/core/Button'; +import {HoverCard} from '@astryxdesign/core/HoverCard'; + +const meta: Meta = { + title: 'a11y/HoverCard in a second document', + tags: ['no-visual'], +}; + +export default meta; + +const FRAME_DOCUMENT = ''; + +/** Copy the page's style sheets into the frame, as an app does. */ +function adoptPageStyles(target: Document) { + for (const sheet of Array.from(document.styleSheets)) { + if ( + sheet.href != null && + new URL(sheet.href).origin !== window.location.origin + ) { + continue; + } + const style = target.createElement('style'); + style.textContent = Array.from(sheet.cssRules, rule => rule.cssText).join( + '\n', + ); + target.head.append(style); + } +} + +function Card() { + return ( + } + delay={0} + hideDelay={0}> +