diff --git a/src/tedi/components/overlays/overlay/overlay-content.tsx b/src/tedi/components/overlays/overlay/overlay-content.tsx
index 64ab6fb4c..9b49ec2b8 100644
--- a/src/tedi/components/overlays/overlay/overlay-content.tsx
+++ b/src/tedi/components/overlays/overlay/overlay-content.tsx
@@ -5,20 +5,49 @@ import { OverlayContext } from './overlay';
export interface OverlayContentProps {
/**
- * Content.
+ * Overlay content.
+ * Can contain any valid React nodes (text, elements, components).
*/
children: ReactNode | ReactNode[];
+
/**
- * Additional class names.
+ * Additional class names for styling overlay elements.
*/
classNames?: {
+ /**
+ * Class name applied to the floating content container.
+ */
content: string;
+
+ /**
+ * Class name applied to the overlay arrow element.
+ */
arrow: string;
};
+
+ /**
+ * ID of the element that labels the overlay content.
+ *
+ * This is used to set the `aria-labelledby` attribute on the overlay container,
+ * providing an accessible name for screen readers.
+ *
+ * Typically points to a heading element inside the overlay (e.g. a title).
+ */
+ labelledBy?: string;
+
+ /**
+ * ID of the element that describes the overlay content.
+ *
+ * This is used to set the `aria-describedby` attribute on the overlay container,
+ * allowing screen readers to announce additional descriptive text.
+ *
+ * Useful for longer explanations or supporting content that complements the title.
+ */
+ describedBy?: string;
}
export const OverlayContent = (props: OverlayContentProps) => {
- const { children, classNames } = props;
+ const { children, classNames, labelledBy, describedBy } = props;
const {
open,
x,
@@ -58,6 +87,9 @@ export const OverlayContent = (props: OverlayContentProps) => {
: children}
);
};
diff --git a/src/tedi/components/overlays/popover/popover.stories.tsx b/src/tedi/components/overlays/popover/popover.stories.tsx
index cd7becbaa..a77658489 100644
--- a/src/tedi/components/overlays/popover/popover.stories.tsx
+++ b/src/tedi/components/overlays/popover/popover.stories.tsx
@@ -109,7 +109,7 @@ const ContentExamplesTemplate: StoryFn = (args) => {
The polar bear (Ursus maritimus) is a large bear native to the Arctic and nearby areas.
-
+
Read more
@@ -434,6 +434,20 @@ export const NotDismissible: Story = {
args: {
dismissible: false,
},
+ parameters: {
+ docs: {
+ description: {
+ story: `
+ Accessibility warning
+
+ When \`dismissible=false\`:
+ - A visible close button MUST be present
+ - Keyboard users must have a clear exit path
+ - This pattern should only be used when content does not obscure critical information
+ `,
+ },
+ },
+ },
};
export const ScrollLocked: Story = {
@@ -483,15 +497,116 @@ export const FocusLocked: Story = {
docs: {
description: {
story: `
- This story demonstrates a Popover with a “locked” focus behavior, where keyboard navigation (Tab) is confined
- to the Popover content until the user clicks an action like "Cancel" or "Submit".
-
- Key points:
- - Keyboard focus is restricted inside the Popover until it is closed.
- - \`focusManager.modal\` ensures focus stays within the Popover content.
- - \`initialFocus\` sets the first element to receive focus when opening.
- - This setup covers mostly edge cases; the default focus trap is false.
-`,
+ This story demonstrates a Popover with a “locked” focus behavior, where keyboard navigation (Tab) is confined
+ to the Popover content until the user clicks an action like "Cancel" or "Submit".
+
+ Key points:
+ - Keyboard focus is restricted inside the Popover until it is closed.
+ - \`focusManager.modal\` ensures focus stays within the Popover content.
+ - \`initialFocus\` sets the first element to receive focus when opening.
+ - This setup covers mostly edge cases; the default focus trap is false.
+ `,
+ },
+ },
+ },
+};
+
+export const AccessibilityBaseline: Story = {
+ render: () => (
+
+
+
+
+
+
+ This popover contains text, a link, and buttons. Screen readers should announce roles correctly.
+
+
+ Read more
+
+
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: `
+ Accessibility baseline test
+
+ Use this story to verify:
+ - Dialog role is announced
+ - Title is used as the accessible name
+ - Buttons and links announce their roles
+ - Focus moves into the popover on open
+ - Escape closes the popover
+ - Close button is reachable via keyboard
+ `,
+ },
+ },
+ },
+};
+
+export const NoTitleAccessibleName: Story = {
+ render: () => (
+
+
+
+
+
+
This popover has no title. The accessible name will fall back to the trigger label.
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: `
+ No-title popover
+
+ Expected behavior:
+ - Dialog is announced
+ - Accessible name is inherited from trigger
+ - Roles of buttons are still announced
+ `,
+ },
+ },
+ },
+};
+
+export const ReadAllStressTest: Story = {
+ render: () => (
+
+
+
+
+
+
+ Paragraph one with inline formatting.
+
+
+ Paragraph two with a link.
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: `
+ Use VoiceOver "Read All" inside the popover.
+
+ Verify:
+ - Content is not flattened
+ - Buttons are announced as buttons
+ - Links are announced as links
+ - Content is not duplicated
+ `,
},
},
},
diff --git a/src/tedi/components/overlays/popover/popover.tsx b/src/tedi/components/overlays/popover/popover.tsx
index 68f8de888..0b933c717 100644
--- a/src/tedi/components/overlays/popover/popover.tsx
+++ b/src/tedi/components/overlays/popover/popover.tsx
@@ -30,6 +30,7 @@ export const Popover = (props: PopoverProps) => {
height: ARROW_HEIGHT,
}}
openWith={openWith}
+ role="dialog"
{...rest}
/>
);