Skip to content

Commit

Permalink
refactor: use radix-ui components for modals and dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
kark committed Jan 23, 2025
1 parent ea1d0cb commit 707e25e
Show file tree
Hide file tree
Showing 13 changed files with 664 additions and 269 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"lint": "jest --projects jest.{eslint,stylelint}.config.js",
"lint:js": "jest --config jest.eslint.config.js",
"lint:css": "jest --config jest.stylelint.config.js",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit --skipLibCheck",
"typecheck:cypress": "pnpm typecheck -p cypress",
"typecheck:starter:custom-applications": "pnpm typecheck -p application-templates/starter-typescript",
"typecheck:starter:custom-views": "pnpm typecheck -p custom-views-templates/starter-typescript",
Expand Down Expand Up @@ -184,8 +184,7 @@
},
"patchedDependencies": {
"babel-plugin-typescript-to-proptypes@1.4.2": "patches/babel-plugin-typescript-to-proptypes@1.4.2.patch",
"vite-bundle-analyzer@0.12.1": "patches/vite-bundle-analyzer@0.12.1.patch",
"react-modal@3.16.1": "patches/react-modal@3.16.1.patch"
"vite-bundle-analyzer@0.12.1": "patches/vite-bundle-analyzer@0.12.1.patch"
}
},
"engines": {
Expand Down
6 changes: 3 additions & 3 deletions packages/application-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,20 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@flopflip/react-broadcast": "14.0.2",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-visually-hidden": "1.1.1",
"@react-hook/latest": "1.0.3",
"@react-hook/resize-observer": "1.2.6",
"@types/history": "^4.7.11",
"@types/lodash": "^4.14.198",
"@types/prop-types": "^15.7.5",
"@types/react": "^19.0.3",
"@types/react-dom": "^19.0.2",
"@types/react-modal": "^3.16.0",
"@types/react-router-dom": "^5.3.3",
"history": "4.10.1",
"lodash": "4.17.21",
"prop-types": "15.8.1",
"raf-schd": "^4.0.3",
"react-modal": "3.16.1"
"raf-schd": "^4.0.3"
},
"devDependencies": {
"@apollo/client": "3.7.14",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ describe('CustomViewLoader', () => {
<CustomViewLoader customView={TEST_CUSTOM_VIEW} onClose={onCloseMock} />
);

const overlay = baseElement.querySelector('[data-role="modal-overlay"]');
const overlay = baseElement.querySelector(
'[data-role="modal-overlay-clickable"]'
);
fireEvent.click(overlay!);

await waitFor(() => expect(onCloseMock).toHaveBeenCalled());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SyntheticEvent, ReactNode } from 'react';
import type { ReactNode, SyntheticEvent } from 'react';
import DialogContainer from '../internals/dialog-container';
import DialogContent from '../internals/dialog-content';
import DialogHeader, { TextTitle } from '../internals/dialog-header';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type { ReactNode, SyntheticEvent } from 'react';
import { css, ClassNames } from '@emotion/react';
import { useState, useEffect, type ReactNode, SyntheticEvent } from 'react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import Modal, { type Props as ModalProps } from 'react-modal';
import * as Dialog from '@radix-ui/react-dialog';
import type { DialogContentProps } from '@radix-ui/react-dialog';
import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
import { PORTALS_CONTAINER_ID } from '@commercetools-frontend/constants';
import Card from '@commercetools-uikit/card';
import { designTokens as uiKitDesignTokens } from '@commercetools-uikit/design-system';
import { useWarning } from '@commercetools-uikit/utils';
import { getOverlayStyles, getModalContentStyles } from './dialog.styles';
import {
DialogOverlay,
DialogContent,
ClickableDialogContent,
} from './dialog.styles';

// When running tests, we don't render the AppShell. Instead we mock the
// application context to make the data available to the application under
Expand All @@ -28,17 +34,6 @@ const getDefaultParentSelector = () =>
`#${PORTALS_CONTAINER_ID}`
) as HTMLElement);

const getOverlayElement: ModalProps['overlayElement'] = (
props,
contentElement
) => (
// Assign the `data-role` to the overlay container, which is used as
// the CSS selector in the `<PortalsContainer>`.
<div {...props} data-role="dialog-overlay">
{contentElement}
</div>
);

type Props = {
isOpen: boolean;
onClose?: (event: SyntheticEvent) => void;
Expand All @@ -53,6 +48,7 @@ type Props = {
type GridAreaProps = {
name: string;
};

const GridArea = styled.div<GridAreaProps>`
grid-area: ${(props) => props.name};
`;
Expand All @@ -68,73 +64,93 @@ const DialogContainer = ({
'app-kit/DialogHeader: "aria-label" prop is required when the "title" prop is not a string.'
);

return (
<ClassNames>
{({ css: makeClassName }) => (
<Modal
isOpen={props.isOpen}
onRequestClose={props.onClose}
shouldCloseOnOverlayClick={Boolean(props.onClose)}
shouldCloseOnEsc={Boolean(props.onClose)}
overlayElement={getOverlayElement}
overlayClassName={makeClassName(getOverlayStyles({ size, ...props }))}
className={makeClassName(getModalContentStyles({ size, ...props }))}
contentLabel={
typeof props.title === 'string' ? props.title : props['aria-label']
}
parentSelector={getParentSelector}
ariaHideApp={false}
>
<GridArea name="top" />
<GridArea name="left" />
<GridArea name="right" />
<GridArea name="bottom" />
<GridArea
name="main"
css={css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
overflow: hidden;
`}
>
<Card
// 1. For the min-height: https://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox/28639686#28639686
// 2. For the actual "> div" container with the content, we need to use normal pointer events so that clicking on it does not close the dialog.
css={css`
min-height: 0;
padding: ${uiKitDesignTokens.spacing20}
${uiKitDesignTokens.spacing30};
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
null
);

> div {
display: flex;
flex-direction: column;
height: 100%;
pointer-events: auto;
min-height: 0;
}
`}
useEffect(() => {
const container = getParentSelector();
if (container) {
setPortalContainer(container);
}
}, [getParentSelector]);

return (
<Dialog.Root open={props.isOpen} modal={false}>
<Dialog.Portal container={portalContainer}>
<>
<DialogOverlay zIndex={props.zIndex} data-role="dialog-overlay" />
<DialogContent data-role="dialog-content">
<ClickableDialogContent
size={size}
onEscapeKeyDown={
props.onClose as DialogContentProps['onEscapeKeyDown']
}
onPointerDownOutside={
props.onClose as DialogContentProps['onPointerDownOutside']
}
aria-describedby={undefined}
>
<div
<VisuallyHidden.Root asChild>
<Dialog.Title>
{typeof props.title === 'string'
? props.title
: props['aria-label']}
</Dialog.Title>
</VisuallyHidden.Root>
<GridArea name="top" />
<GridArea name="left" />
<GridArea name="right" />
<GridArea name="bottom" />
<GridArea
name="main"
css={css`
display: flex;
flex-direction: column;
align-items: stretch;
align-items: center;
justify-content: center;
height: 100%;
min-height: 0;
overflow: hidden;
`}
>
{props.children}
</div>
</Card>
</GridArea>
</Modal>
)}
</ClassNames>
<Card
// 1. For the min-height: https://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox/28639686#28639686
// 2. For the actual "> div" container with the content, we need to use normal pointer events so that clicking on it does not close the dialog.
css={css`
min-height: 0;
padding: ${uiKitDesignTokens.spacing20}
${uiKitDesignTokens.spacing30};
> div {
display: flex;
flex-direction: column;
height: 100%;
pointer-events: auto;
min-height: 0;
}
`}
>
<div
css={css`
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
min-height: 0;
`}
>
{props.children}
</div>
</Card>
</GridArea>
</ClickableDialogContent>
</DialogContent>
</>
</Dialog.Portal>
</Dialog.Root>
);
};

DialogContainer.displayName = 'DialogContainer';

export default DialogContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const createDialogValidator =
expect(screen.queryByText(expectedTitle)).not.toBeInTheDocument();

fireEvent.click(screen.getByLabelText(/Open Info Dialog/));
await screen.findByText(expectedTitle);
await screen.findByRole('heading', { name: expectedTitle, level: 3 });
expect(screen.getByText(/Hello/)).toBeInTheDocument();

if (expectedAriaTitle) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { css, type SerializedStyles } from '@emotion/react';
import styled from '@emotion/styled';
import { Content } from '@radix-ui/react-dialog';
import { designTokens as uiKitDesignTokens } from '@commercetools-uikit/design-system';

type StyleProps = {
Expand Down Expand Up @@ -75,23 +77,40 @@ export const getModalContentStyles = (props: StyleProps): SerializedStyles => {
height: 100%;
width: 100%;
outline: none;
position: relative;
pointer-events: none;
z-index: ${typeof props.zIndex === 'number'
? // Use `!important` to overwrite the default value assigned by the Stacking Layer System.
// We're assigning value 1 unit higher than the overlay to ensure the content is on top.
// It's safe to do that since the modal is topmost in the stacking layer.
`${props.zIndex + 1} !important`
: 'auto'};
${gridStyle};
`;
return baseStyles;
};

export const getOverlayStyles = (props: StyleProps): SerializedStyles => css`
export const ClickableDialogContent = styled(Content)<StyleProps>`
${(props) => getModalContentStyles(props)}
`;

export const DialogOverlay = styled.div<Pick<StyleProps, 'zIndex'>>`
display: flex;
position: absolute;
z-index: ${typeof props.zIndex === 'number'
? // Use `!important` to overwrite the default value assigned by the Stacking Layer System.
`${props.zIndex} !important`
: 'auto'};
z-index: ${({ zIndex }) =>
// Use `!important` to overwrite the default value assigned by the Stacking Layer System.
typeof zIndex === 'number' ? `${zIndex} !important` : 'auto'};
top: 0;
width: 100%;
height: 100%;
background-color: rgba(32, 62, 72, 0.5);
opacity: 1;
`;

export const DialogContent = styled.div`
position: absolute;
width: 100%;
height: 100%;
top: 0;
`;
Loading

0 comments on commit 707e25e

Please sign in to comment.