diff --git a/.changeset/neat-moose-dress.md b/.changeset/neat-moose-dress.md new file mode 100644 index 00000000000..fc732c2f69c --- /dev/null +++ b/.changeset/neat-moose-dress.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Dialog: dynamically switch footer button layout based on available height. diff --git a/packages/react/src/Dialog/Dialog.module.css b/packages/react/src/Dialog/Dialog.module.css index c87b9321e56..0532fed9f22 100644 --- a/packages/react/src/Dialog/Dialog.module.css +++ b/packages/react/src/Dialog/Dialog.module.css @@ -389,11 +389,11 @@ Add a border between the body and footer if: padding: var(--base-size-16); gap: var(--base-size-8); flex-shrink: 0; +} - @media (max-height: 325px) { - flex-wrap: nowrap; - overflow-x: scroll; - flex-direction: row; - justify-content: unset; - } +.Dialog[data-footer-button-layout='scroll'] .Footer { + flex-wrap: nowrap; + overflow-x: scroll; + flex-direction: row; + justify-content: unset; } diff --git a/packages/react/src/Dialog/Dialog.tsx b/packages/react/src/Dialog/Dialog.tsx index 4ef73390514..5d0ed137ddf 100644 --- a/packages/react/src/Dialog/Dialog.tsx +++ b/packages/react/src/Dialog/Dialog.tsx @@ -16,6 +16,7 @@ import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../uti import classes from './Dialog.module.css' import {clsx} from 'clsx' import {useSlots} from '../hooks/useSlots' +import {useResizeObserver} from '../hooks/useResizeObserver' /* Dialog Version 2 */ @@ -239,6 +240,8 @@ const defaultPosition = { } const defaultFooterButtons: Array = [] +// Minimum room needed for body content before forcing footer buttons into horizontal scroll. +const MIN_BODY_HEIGHT = 48 // useful to determine whether we're inside a Dialog from a nested component export const DialogContext = React.createContext(undefined) @@ -273,6 +276,7 @@ const _Dialog = React.forwardRef(false) + const [footerButtonLayout, setFooterButtonLayout] = useState<'scroll' | 'wrap'>('wrap') const defaultedProps = {...props, title, subtitle, role, dialogLabelId, dialogDescriptionId} const onBackdropClick = useCallback( (e: SyntheticEvent) => { @@ -339,6 +343,35 @@ const _Dialog = React.forwardRef { + if (!hasFooter) { + return + } + + const dialogElement = dialogRef.current + if (!(dialogElement instanceof HTMLElement)) { + return + } + const bodyWrapper = dialogElement.querySelector(`.${classes.DialogOverflowWrapper}`) + if (!(bodyWrapper instanceof HTMLElement)) { + return + } + + // We temporarily force "wrap" the footer layout so that the browser can calculate the body height - + // when the footer is wrapping. This is instantaneous with what we set below (`dialogElement.setAttribute('data-footer-button-layout', newLayout)`). + dialogElement.setAttribute('data-footer-button-layout', 'wrap') + const bodyHeight = bodyWrapper.clientHeight + + const newLayout = bodyHeight >= MIN_BODY_HEIGHT ? 'wrap' : 'scroll' + dialogElement.setAttribute('data-footer-button-layout', newLayout) + + setFooterButtonLayout(newLayout) + }, [hasFooter]) + + useResizeObserver(updateFooterButtonLayout, backdropRef) + const positionDataAttributes = typeof position === 'string' ? {'data-position-regular': position} @@ -371,7 +404,8 @@ const _Dialog = React.forwardRef