diff --git a/.changeset/hovercard-second-document.md b/.changeset/hovercard-second-document.md new file mode 100644 index 0000000000000..c8be5013b5223 --- /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. The card now also ranks on the shared dismissal stack by its trigger's place, so a card whose trigger sits inside a focus-trap container takes Escape before that container, as the topmost layer. + +@cixzhang @duytbp diff --git a/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx b/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx new file mode 100644 index 0000000000000..818b1bcffcc91 --- /dev/null +++ b/apps/storybook/stories/HoverCardSecondDocumentA11y.stories.tsx @@ -0,0 +1,117 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file HoverCardSecondDocumentA11y.stories.tsx + * @input HoverCard portalled into a same-origin iframe and into a Document + * Picture-in-Picture window + * @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}> +