Skip to content

Commit

Permalink
fix(dialog): ensure height never exceeds 90% of the viewport height
Browse files Browse the repository at this point in the history
Tweaks how an opened `Dialog` is centred in the middle of the screen, while
ensuring it never exceeds 90% of the viewport height.
  • Loading branch information
Parsium committed Oct 23, 2024
1 parent cb77fb7 commit 619a651
Showing 5 changed files with 111 additions and 254 deletions.
225 changes: 64 additions & 161 deletions src/components/dialog/dialog.component.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import React, {
useRef,
useEffect,
useLayoutEffect,
useCallback,
useImperativeHandle,
forwardRef,
useState,
} from "react";
import React, { useRef, useImperativeHandle, forwardRef } from "react";

import createGuid from "../../__internal__/utils/helpers/guid";
import Modal, { ModalProps } from "../modal";
import Heading from "../heading";
import tagComponent, { TagProps } from "../../__internal__/utils/helpers/tags";
import useResizeObserver from "../../hooks/__internal__/useResizeObserver";

import {
StyledDialog,
StyledDialogTitle,
StyledDialogContent,
DialogPositioner,
} from "./dialog.style";
import { DialogSizes, TOP_MARGIN } from "./dialog.config";
import { DialogSizes } from "./dialog.config";

import FocusTrap, { CustomRefObject } from "../../__internal__/focus-trap";
import IconButton from "../icon-button";
@@ -140,11 +132,6 @@ export const Dialog = forwardRef<DialogHandle, DialogProps>(
const containerRef = useRef<HTMLDivElement>(null);
const innerContentRef = useRef(null);
const titleRef = useRef(null);
const [breakpointOffset, setBreakpointOffset] = useState<
number | undefined
>(undefined);
const isDialogMaximised = size === "maximise";
const listenersAdded = useRef(false);
const { current: titleId } = useRef(createGuid());
const { current: subtitleId } = useRef(createGuid());

@@ -160,122 +147,41 @@ export const Dialog = forwardRef<DialogHandle, DialogProps>(
[]
);

const centerDialog = useCallback(() => {
/* istanbul ignore if */
if (!containerRef.current) {
return;
}

const {
width: dialogWidth,
height: dialogHeight,
} = containerRef.current.getBoundingClientRect();

let midPointY = window.innerHeight / 2;
let midPointX = window.innerWidth / 2;

midPointY -= dialogHeight / 2;
midPointX -= dialogWidth / 2;

if (midPointY < TOP_MARGIN) {
midPointY = TOP_MARGIN;
}

if (midPointX < 0) {
midPointX = 0;
}

if (isDialogMaximised) {
const breakPoint = window.innerWidth > 960 ? 32 : 16;
midPointX = breakPoint;
midPointY = breakPoint;
setBreakpointOffset(breakPoint);
}

containerRef.current.style.top = `${midPointY}px`;
containerRef.current.style.left = `${midPointX}px`;
}, [isDialogMaximised]);

useResizeObserver(innerContentRef, centerDialog, !open);

const addListeners = useCallback(() => {
/* istanbul ignore else */
if (!listenersAdded.current) {
window.addEventListener("resize", centerDialog);
listenersAdded.current = true;
}
}, [centerDialog]);

const removeListeners = useCallback(() => {
if (listenersAdded.current) {
window.removeEventListener("resize", centerDialog);
listenersAdded.current = false;
}
}, [centerDialog]);

useEffect(() => {
if (open) {
addListeners();
}

if (!open) {
removeListeners();
}

return () => {
removeListeners();
};
}, [open, addListeners, removeListeners]);

useLayoutEffect(() => {
if (open) {
centerDialog();
}
}, [centerDialog, open, height]);

const closeIcon = () => {
if (!showCloseIcon || !onCancel) return null;

return (
<IconButton
aria-label={locale.dialog.ariaLabels.close()}
onClick={onCancel}
disabled={disableClose}
{...tagComponent("close", {
"data-element": "close",
...closeButtonDataProps,
})}
>
<Icon type="close" />
</IconButton>
);
};

const dialogTitle = () => {
if (!title) return null;
const closeIcon = showCloseIcon && onCancel && (
<IconButton
aria-label={locale.dialog.ariaLabels.close()}
onClick={onCancel}
disabled={disableClose}
{...tagComponent("close", {
"data-element": "close",
...closeButtonDataProps,
})}
>
<Icon type="close" />
</IconButton>
);

return (
<StyledDialogTitle
showCloseIcon={showCloseIcon}
hasSubtitle={!!subtitle}
ref={titleRef}
>
{typeof title === "string" ? (
<Heading
data-element="dialog-title"
title={title}
titleId={titleId}
subheader={subtitle}
subtitleId={subtitleId}
divider={false}
help={help}
/>
) : (
title
)}
</StyledDialogTitle>
);
};
const dialogTitle = title && (
<StyledDialogTitle
showCloseIcon={showCloseIcon}
hasSubtitle={!!subtitle}
ref={titleRef}
>
{typeof title === "string" ? (
<Heading
data-element="dialog-title"
title={title}
titleId={titleId}
subheader={subtitle}
subtitleId={subtitleId}
divider={false}
help={help}
/>
) : (
title
)}
</StyledDialogTitle>
);

let dialogHeight = height;

@@ -313,38 +219,35 @@ export const Dialog = forwardRef<DialogHandle, DialogProps>(
isOpen={open}
additionalWrapperRefs={focusableContainers}
>
<StyledDialog
data-component={dataComponent}
data-element={dataElement}
data-role={dataRole}
aria-modal={isTopModal ? true : undefined}
ref={containerRef}
topMargin={
isDialogMaximised && breakpointOffset
? breakpointOffset * 2
: TOP_MARGIN
}
{...dialogProps}
role={role}
tabIndex={-1}
{...contentPadding}
backgroundColor={
greyBackground
? "var(--colorsUtilityMajor025)"
: "var(--colorsUtilityYang100)"
}
>
{dialogTitle()}
{closeIcon()}
<StyledDialogContent
{...contentPadding}
data-role="dialog-content"
<DialogPositioner>
<StyledDialog
data-component={dataComponent}
data-element={dataElement}
data-role={dataRole}
aria-modal={isTopModal ? true : undefined}
ref={containerRef}
{...dialogProps}
role={role}
tabIndex={-1}
ref={innerContentRef}
{...contentPadding}
backgroundColor={
greyBackground
? "var(--colorsUtilityMajor025)"
: "var(--colorsUtilityYang100)"
}
>
{children}
</StyledDialogContent>
</StyledDialog>
{dialogTitle}
{closeIcon}
<StyledDialogContent
{...contentPadding}
data-role="dialog-content"
tabIndex={-1}
ref={innerContentRef}
>
{children}
</StyledDialogContent>
</StyledDialog>
</DialogPositioner>
</FocusTrap>
</Modal>
);
1 change: 0 additions & 1 deletion src/components/dialog/dialog.config.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ export const DIALOG_SIZES = [
"extra-large",
"maximise",
] as const;
export const TOP_MARGIN = 20;
export const CONTENT_TOP_PADDING = 24;
export const HORIZONTAL_PADDING = 32;
export const CONTENT_BOTTOM_PADDING = 30;
4 changes: 3 additions & 1 deletion src/components/dialog/dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -142,7 +142,9 @@ export const MaxSize: Story = () => {
);
};
MaxSize.storyName = "With Max Size";
MaxSize.parameters = { chromatic: { disableSnapshot: true } };
MaxSize.parameters = {
chromatic: { viewports: [1200, 320] },
};

export const Editable: Story = () => {
const [isOpen, setIsOpen] = useState(defaultOpenState);
63 changes: 36 additions & 27 deletions src/components/dialog/dialog.style.ts
Original file line number Diff line number Diff line change
@@ -10,9 +10,8 @@ import {
HORIZONTAL_PADDING,
CONTENT_TOP_PADDING,
CONTENT_BOTTOM_PADDING,
DialogSizes,
} from "./dialog.config";
import { ContentPaddingInterface } from "./dialog.component";
import { ContentPaddingInterface, DialogProps } from "./dialog.component";
import resolvePaddingSides from "../../style/utils/resolve-padding-sides";
import {
StyledFormContent,
@@ -31,29 +30,43 @@ const dialogSizes = {
"extra-large": "1080px",
};

type StyledDialogProps = {
topMargin: number;
size?: DialogSizes;
type StyledDialogProps = Required<Pick<DialogProps, "size">> & {
dialogHeight?: string;
backgroundColor: string;
};

const StyledDialog = styled.div.attrs(({ size }: StyledDialogProps) => {
const isDialogMaximised = size === "maximise";
return {
isDialogMaximised,
};
})<StyledDialogProps & ContentPaddingInterface>`
const DialogPositioner = styled.div`
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: ${({ theme }) => theme.zIndex.modal};
`;

const StyledDialog = styled.div<StyledDialogProps & ContentPaddingInterface>`
box-shadow: var(--boxShadow300);
display: flex;
flex-direction: column;
position: relative;
border-radius: var(--borderRadius200);
position: fixed;
top: 50%;
z-index: ${({ theme }) => theme.zIndex.modal};
max-height: ${({ topMargin }) => `calc(100vh - ${topMargin}px)`};
${({ isDialogMaximised }) => isDialogMaximised && "height: 100%"};
${({ size }) =>
size === "maximise"
? css`
height: calc(100% - var(--spacing400));
width: calc(100% - var(--spacing400));
@media screen and (min-width: 960px) {
height: calc(100% - var(--spacing800));
width: calc(100% - var(--spacing800));
}
`
: css`
max-height: 90vh;
max-width: ${dialogSizes[size]};
width: 100%;
`};
&:focus {
outline: none;
@@ -64,15 +77,6 @@ const StyledDialog = styled.div.attrs(({ size }: StyledDialogProps) => {
background-color: ${backgroundColor};
`}
${({ size, topMargin }) =>
size &&
css`
max-width: ${size === "maximise"
? `calc(100vw - ${topMargin}px)`
: dialogSizes[size]};
width: 100%;
`}
${({ dialogHeight }) =>
dialogHeight &&
css`
@@ -161,12 +165,17 @@ const StyledDialogContent = styled.div<ContentPaddingInterface>((props) => {
`;
});

StyledDialog.defaultProps = {
DialogPositioner.defaultProps = {
theme: baseTheme,
};

StyledDialogContent.defaultProps = {
theme: baseTheme,
};

export { StyledDialog, StyledDialogTitle, StyledDialogContent };
export {
DialogPositioner,
StyledDialog,
StyledDialogTitle,
StyledDialogContent,
};
Loading

0 comments on commit 619a651

Please sign in to comment.