From daba88c13826c67c2b7a97890e22d0258d107ab3 Mon Sep 17 00:00:00 2001 From: Stephen Hailey Date: Tue, 23 Sep 2025 17:09:07 +0100 Subject: [PATCH] [IRN-5393] SPIKE Drawer a11y title fix --- .../bpk-component-drawer/src/BpkDrawer.tsx | 139 ++++++++++++------ 1 file changed, 95 insertions(+), 44 deletions(-) diff --git a/packages/bpk-component-drawer/src/BpkDrawer.tsx b/packages/bpk-component-drawer/src/BpkDrawer.tsx index faf970f72b..8da98cc090 100644 --- a/packages/bpk-component-drawer/src/BpkDrawer.tsx +++ b/packages/bpk-component-drawer/src/BpkDrawer.tsx @@ -18,15 +18,21 @@ /* @flow strict */ -import type { ReactNode } from 'react'; -import { useState, useEffect } from 'react'; +import type { CSSProperties, ReactNode, SyntheticEvent } from 'react'; +import { useEffect, useRef, useState } from 'react'; -import { Portal, isDeviceIpad, isDeviceIphone } from '../../bpk-react-utils'; -import { withScrim } from '../../bpk-scrim-utils'; +import { animations } from '@skyscanner/bpk-foundations-web/tokens/base.es6'; -import BpkDrawerContent from './BpkDrawerContent'; +// @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`. +import BpkCloseButton from '../../bpk-component-close-button'; +// @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`. +import { BpkButtonLink } from '../../bpk-component-link'; +import { isDeviceIpad, isDeviceIphone, BpkDialogWrapper, cssModules } from '../../bpk-react-utils'; -const BpkScrimDrawerContent = withScrim(BpkDrawerContent); +// Reuse Drawer content styles for layout and animations +import STYLES from './BpkDrawerContent.module.scss'; + +const getClassName = cssModules(STYLES); export type Props = { id: string, @@ -53,74 +59,119 @@ export type Props = { padded?: boolean, mobileModalDisplay?: boolean, containerClassName?: string, + // New optional flags to better align with dialog-based behavior + closeOnEscPressed?: boolean, + closeOnScrimClick?: boolean, }; const BpkDrawer = ({ children, className = undefined, closeLabel = null, + closeOnEscPressed = true, + closeOnScrimClick = true, closeText = undefined, containerClassName = undefined, contentClassName = undefined, dialogRef, + // Deprecated/no-op with dialog-based implementation, kept for backwards compatibility getApplicationElement, hideTitle = false, id, - isIpad = isDeviceIpad(), - isIphone = isDeviceIphone(), + isIpad = isDeviceIpad(), // Unused but preserved for backwards compatibility + isIphone = isDeviceIphone(), // Unused but preserved for backwards compatibility isOpen, mobileModalDisplay = false, onClose, padded = true, + // Deprecated/no-op with dialog-based implementation, kept for backwards compatibility renderTarget = null, title, width = '90%', -}: Props) => { +}: Props) => { + + const [exiting, setExiting] = useState(false); + const contentRef = useRef(null); + + // Keep previous behavior: call consumer onClose after exit animation completes + const animationTimeout = parseInt(animations.durationSm, 10) || 240; - const [isDrawerShown, setIsDrawerShown] = useState(true); useEffect(() => { if (isOpen) { - setIsDrawerShown(true); + setExiting(false); } }, [isOpen]); - const onCloseAnimationComplete = () => { - if (onClose){ - onClose(); + useEffect(() => { + if (dialogRef) { + dialogRef(contentRef.current); } - }; + // run only when ref changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contentRef.current]); - const hide = () => { - setIsDrawerShown(false) + const handleClose = ( + e?: TouchEvent | MouseEvent | KeyboardEvent | SyntheticEvent, + meta?: { source: 'ESCAPE' | 'DOCUMENT_CLICK' } + ) => { + setExiting(true); + window.setTimeout(() => { + onClose && onClose(e as any, meta); + setExiting(false); + }, animationTimeout); }; - return( - - - {children} - - + const headingId = `bpk-drawer-heading-${id}`; + + const drawerClassNames = [getClassName('bpk-drawer')]; + const headerClassNames = [getClassName('bpk-drawer__heading')]; + const contentsClassNames = [getClassName('bpk-drawer__content')]; + + if (className) drawerClassNames.push(className); + if (hideTitle) headerClassNames.push(getClassName('bpk-drawer__heading--visually-hidden')); + if (padded) contentsClassNames.push(getClassName('bpk-drawer__content--padded')); + if (contentClassName) contentsClassNames.push(contentClassName); + + // Mirror previous transition states for exit only + const statusClass = exiting ? 'bpk-drawer--exiting' : 'bpk-drawer--entered'; + const mobileModalStatus = exiting ? 'bpk-drawer__modal-mobile-view--exiting' : 'bpk-drawer__modal-mobile-view--entered'; + + return ( + handleClose(arg0 as any, arg1)} + exiting={exiting} + dialogClassName={containerClassName} + closeOnEscPressed={closeOnEscPressed} + closeOnScrimClick={closeOnScrimClick} + timeout={{ appear: 0, exit: animationTimeout }} + > +
{ contentRef.current = el; }} + > +
+

+ {title} +

+   + {closeText ? ( + handleClose()}>{closeText} + ) : ( +
+ handleClose()} /> +
+ )} +
+
{children}
+
+
); }