diff --git a/packages/components/src/components/popover/model.ts b/packages/components/src/components/popover/model.ts index d65307977d7..30a1f27e527 100644 --- a/packages/components/src/components/popover/model.ts +++ b/packages/components/src/components/popover/model.ts @@ -28,7 +28,12 @@ export type DBPopoverProps = DBPopoverDefaultProps & GapProps & PopoverProps; -export interface DBPopoverDefaultState {} +export interface DBPopoverDefaultState { + isExpanded?: boolean; + getTrigger?: () => Element | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handleLeave?: (event: any) => void; +} export type DBPopoverState = DBPopoverDefaultState & GlobalState & diff --git a/packages/components/src/components/popover/popover.lite.tsx b/packages/components/src/components/popover/popover.lite.tsx index d315eff3d6d..8915cb40b4e 100644 --- a/packages/components/src/components/popover/popover.lite.tsx +++ b/packages/components/src/components/popover/popover.lite.tsx @@ -18,11 +18,45 @@ export default function DBPopover(props: DBPopoverProps) { // jscpd:ignore-start const state = useStore({ initialized: false, + isExpanded: false, handleAutoPlacement: () => { + state.isExpanded = true; if (!ref) return; const article = ref.querySelector('article'); if (!article) return; handleDataOutside(article); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handleLeave: (event: any) => { + const element = event.target as HTMLElement; + const parent = element.parentNode; + if ( + !parent || + (element.parentNode.querySelector(':focus') !== element && + element.parentNode.querySelector(':focus-within') !== + element && + element.parentNode.querySelector(':hover') !== element) + ) { + state.isExpanded = false; + } + }, + getTrigger: () => { + if (ref) { + const children: Element[] = Array.from(ref.children); + if (children.length >= 2) { + const firstChild = children[0]; + if (firstChild.tagName.includes('-')) { + // this is a workaround for custom angular components + return firstChild.children?.length > 0 + ? firstChild.children[0] + : undefined; + } else { + return firstChild; + } + } + } + + return undefined; } }); @@ -32,14 +66,23 @@ export default function DBPopover(props: DBPopoverProps) { onUpdate(() => { if (ref && state.initialized) { - const children: Element[] = Array.from(ref.children); - if (children.length >= 2) { - children[0].ariaHasPopup = 'true'; + const child = state.getTrigger(); + if (child) { + child.ariaHasPopup = 'true'; } state.initialized = false; } }, [ref, state.initialized]); + onUpdate(() => { + if (ref) { + const child = state.getTrigger(); + if (child) { + child.ariaExpanded = state.isExpanded.toString(); + } + } + }, [ref, state.isExpanded]); + // jscpd:ignore-end return ( @@ -48,7 +91,9 @@ export default function DBPopover(props: DBPopoverProps) { id={props.id} class={cls('db-popover', props.className)} onFocus={() => state.handleAutoPlacement()} - onMouseEnter={() => state.handleAutoPlacement()}> + onBlur={(event: FocusEvent) => state.handleLeave(event)} + onMouseEnter={() => state.handleAutoPlacement()} + onMouseLeave={(event: MouseEvent) => state.handleLeave(event)}>
@@ -35,6 +37,8 @@ const cleanSpeakInstructions = (phraseLog: string[]): string[] => !standardPhrases.some((string) => sPhrase.includes(string)) ) .join('. ') + // We need to replace specific phrases, as they are being reported differently on localhost and within CI/CD + .replaceAll('pop-up', 'pop up') ); export const generateSnapshot = async ( diff --git a/showcases/screen-reader/tests/popover.spec.ts b/showcases/screen-reader/tests/popover.spec.ts new file mode 100644 index 00000000000..d50726e80ec --- /dev/null +++ b/showcases/screen-reader/tests/popover.spec.ts @@ -0,0 +1,33 @@ +import { getTest, testDefault } from '../default'; + +const test = getTest(); +test.describe('DBPopover', () => { + testDefault({ + test, + title: 'opened', + description: 'should open the popover', + url: './#/01/popover?page=density', + async testFn(voiceOver, nvda) { + if (nvda) { + await nvda?.act(); // Opening first popover + await nvda?.press('Tab'); // Tab to button inside popover + await nvda?.next(); // Navigating to default button + await nvda?.clearSpokenPhraseLog(); + await nvda?.act(); // Read button + opening second popover -> should jump to article + await nvda?.next(); // Navigating to first item of list within popover + await nvda?.next(); // Navigating to section item of list within popover + await nvda?.next(); // Navigating to button within popover + await nvda?.next(); // Navigating to next button + } else if (voiceOver) { + await voiceOver?.next(); // Opening first popover and navigating to the included "article" + await voiceOver?.next(); // Navigating to list within popover + await voiceOver?.next(); // Navigating to first item of list within popover + await voiceOver?.next(); // Navigating to section item of list within popover + await voiceOver?.next(); // Navigating to end of list within popover + await voiceOver?.next(); // Navigating to button within popover + await voiceOver?.next(); // Navigating to end of article + await voiceOver?.next(); // Navigating to next button and open next popover + } + } + }); +});