Skip to content

Commit

Permalink
Support configuring the click behavoir on underlay of Modal
Browse files Browse the repository at this point in the history
  • Loading branch information
miafan23 committed Dec 2, 2024
1 parent d9d81fd commit 9aba8d7
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 58 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mapbox/mr-ui",
"version": "2.11.0",
"version": "2.12.0",
"description": "UI components for Mapbox projects",
"main": "index.js",
"homepage": "./",
Expand Down
33 changes: 19 additions & 14 deletions src/components/modal/__snapshots__/modal.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ exports[`Modal all options renders 1`] = `
<div
class="fixed top bottom left right bg-darken50"
data-state="open"
data-testid="modal-overlay"
style="pointer-events: auto; display: grid; overflow-y: auto; place-items: start center;"
>
<div
aria-describedby="radix-:r13:"
aria-labelledby="radix-:r12:"
aria-describedby="radix-:r17:"
aria-labelledby="radix-:r16:"
class="relative my120 wmax360 w-11/12 bg-white round"
data-state="open"
id="radix-:r11:"
id="radix-:r15:"
role="dialog"
style="pointer-events: auto;"
tabindex="-1"
Expand All @@ -39,7 +40,7 @@ exports[`Modal all options renders 1`] = `
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
>
<h2
id="radix-:r12:"
id="radix-:r16:"
>
All options
</h2>
Expand Down Expand Up @@ -120,6 +121,7 @@ exports[`Modal basic renders 1`] = `
class="fixed top bottom left right bg-darken50"
data-aria-hidden="true"
data-state="open"
data-testid="modal-overlay"
style="pointer-events: auto; display: grid; overflow-y: auto; place-items: start center;"
>
<div
Expand Down Expand Up @@ -157,6 +159,7 @@ exports[`Modal basic renders 1`] = `
<div
class="fixed top bottom left right bg-darken50"
data-state="open"
data-testid="modal-overlay"
style="pointer-events: auto; display: grid; overflow-y: auto; place-items: start center;"
>
<div
Expand Down Expand Up @@ -261,14 +264,15 @@ exports[`Modal basic, auto renders 1`] = `
<div
class="fixed top bottom left right bg-darken50"
data-state="open"
data-testid="modal-overlay"
style="pointer-events: auto; display: grid; overflow-y: auto; place-items: start center;"
>
<div
aria-describedby="radix-:rr:"
aria-labelledby="radix-:rq:"
aria-describedby="radix-:rv:"
aria-labelledby="radix-:ru:"
class="relative my12 my60-mm wmax-full bg-white round px36 py36"
data-state="open"
id="radix-:rp:"
id="radix-:rt:"
role="dialog"
style="pointer-events: auto;"
tabindex="-1"
Expand All @@ -277,7 +281,7 @@ exports[`Modal basic, auto renders 1`] = `
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
>
<h2
id="radix-:rq:"
id="radix-:ru:"
>
Auto modal title
</h2>
Expand All @@ -286,7 +290,7 @@ exports[`Modal basic, auto renders 1`] = `
No fixed width on the modal container. But very tall.
</div>
<button
aria-describedby="radix-:rs:"
aria-describedby="radix-:r10:"
aria-label="Close"
class="btn btn--transparent unround-t unround-br color-gray py12 px12 absolute top right"
data-state="instant-open"
Expand Down Expand Up @@ -350,7 +354,7 @@ exports[`Modal basic, auto renders 1`] = `
</svg>
</span>
<span
id="radix-:rs:"
id="radix-:r10:"
role="tooltip"
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
>
Expand Down Expand Up @@ -384,14 +388,15 @@ exports[`Modal basic, large renders 1`] = `
<div
class="fixed top bottom left right bg-darken50"
data-state="open"
data-testid="modal-overlay"
style="pointer-events: auto; display: grid; overflow-y: auto; place-items: start center;"
>
<div
aria-describedby="radix-:rj:"
aria-labelledby="radix-:ri:"
aria-describedby="radix-:rn:"
aria-labelledby="radix-:rm:"
class="relative my12 my60-mm wmax600 w-11/12 bg-white round px36 py36"
data-state="open"
id="radix-:rh:"
id="radix-:rl:"
role="dialog"
style="pointer-events: auto;"
tabindex="-1"
Expand All @@ -400,7 +405,7 @@ exports[`Modal basic, large renders 1`] = `
style="position: absolute; border: 0px; width: 1px; height: 1px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; word-wrap: normal;"
>
<h2
id="radix-:ri:"
id="radix-:rm:"
>
Large modal title
</h2>
Expand Down
2 changes: 2 additions & 0 deletions src/components/modal/examples/modal-b.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
A small modal, unpadded to allow for custom background coloring, with
initial focus set on an input. Also features its own action special button.
It does not exit on overlay clicked
*/
import React, { ReactElement, useState } from 'react';
import Modal from '../modal';
Expand All @@ -13,6 +14,7 @@ export default function Example(): ReactElement {
<Modal
accessibleTitle="Animals"
onExit={() => setModal(false)}
exitOnUnderlayClicked={false}
padding="none"
size="small"
initialFocus="#modal-input"
Expand Down
14 changes: 12 additions & 2 deletions src/components/modal/modal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import Modal from './modal';
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';

describe('Modal', () => {
describe('basic', () => {
Expand All @@ -16,6 +16,10 @@ describe('Modal', () => {
}
} as const;

afterEach(() => {
cleanup();
});

test('renders', () => {
const { baseElement } = render(<Modal {...props} />);
expect(baseElement).toMatchSnapshot();
Expand All @@ -27,14 +31,20 @@ describe('Modal', () => {
expect(mockedOnExit).toHaveBeenCalledTimes(1);
});

test('does not fire onExit when underlay is clicked if exitOnUnderlayClicked is false', () => {
render(<Modal {...props} exitOnUnderlayClicked={false}/>);
fireEvent.click(screen.getByTestId('modal-overlay'));
expect(mockedOnExit).toHaveBeenCalledTimes(0);
});

test('allowEventBubbling disables event trapping', () => {
const props = {
children: 'content',
accessibleTitle: 'Modal title'
};
const { rerender } = render(<Modal {...props} />);
expect(screen.getByTestId('event-trap')).toBeInTheDocument();
rerender(<Modal {...props } allowEventBubbling={true} />);
rerender(<Modal {...props} allowEventBubbling={true} />);
expect(screen.queryByTestId('event-trap')).not.toBeInTheDocument();
});
});
Expand Down
82 changes: 43 additions & 39 deletions src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,25 @@ interface Props {
padding?: 'large' | 'none';
margin?: 'large' | 'default';
onExit?: () => void;
exitOnUnderlayClicked?: boolean;
allowEventBubbling?: boolean;
initialFocus?: string;
primaryAction?: {
text: string;
callback: () => void;
destructive?: boolean;
disabled?: boolean;
},
};
secondaryAction?: {
text: string;
callback: () => void;
disabled?: boolean;
},
};
tertiaryAction?: {
text: string;
callback: () => void;
disabled?: boolean;
}
};
}

/**
Expand All @@ -53,13 +54,13 @@ export default function Modal({
padding = 'large',
margin = 'default',
allowEventBubbling = false,
exitOnUnderlayClicked = true,
initialFocus,
primaryAction,
secondaryAction,
tertiaryAction,
onExit
}: Props): ReactElement {

const renderActions = (): ReactElement => {
if (!primaryAction) {
return null;
Expand All @@ -74,7 +75,7 @@ export default function Modal({
/>
</div>
);
}
};

let widthClass = 'wmax-full';
if (size === 'small') {
Expand All @@ -83,89 +84,87 @@ export default function Modal({
widthClass = 'wmax600 w-11/12';
}

let marginClass = 'my12 my60-mm'
let marginClass = 'my12 my60-mm';
if (margin === 'large') {
marginClass = 'my120';
}

const overlayProps: {
className: string,
style: CSSProperties
className: string;
style: CSSProperties;
'data-testid': string;
} = {
className: 'fixed top bottom left right bg-darken50',
style: {
display: 'grid',
overflowY: 'auto',
placeItems: 'start center'
}
},
'data-testid': 'modal-overlay'
};

const rootProps: {
defaultOpen: true,
onOpenChange?: () => void
defaultOpen: true;
onOpenChange?: () => void;
} = {
defaultOpen: true
};

if (onExit) {
rootProps.onOpenChange = onExit
if (onExit && exitOnUnderlayClicked) {
rootProps.onOpenChange = onExit;
}

const contentProps: {
className: string,
onOpenAutoFocus?: (e) => void
className: string;
onOpenAutoFocus?: (e) => void;
} = {
className: classnames(
`relative ${marginClass} ${widthClass} bg-white round`,
{ 'px36 py36': padding === 'large' }
)
}
};

if (initialFocus) {
contentProps.onOpenAutoFocus = e => {
contentProps.onOpenAutoFocus = (e) => {
const el: HTMLElement | null = document.querySelector(initialFocus);
if (el !== null) {
e.preventDefault();
el.focus();
}
}
};
}

const modal = (
<DialogPrimitive.Root {...rootProps}>
<DialogPrimitive.Root {...rootProps} open={true}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay { ...overlayProps}>
<DialogPrimitive.Content { ...contentProps}>
<DialogPrimitive.Overlay {...overlayProps}>
<DialogPrimitive.Content {...contentProps}>
<VisuallyHidden.Root>
<DialogPrimitive.Title>
{accessibleTitle}
</DialogPrimitive.Title>
<DialogPrimitive.Title>{accessibleTitle}</DialogPrimitive.Title>
</VisuallyHidden.Root>
{children}
{renderActions()}
{onExit && <Tooltip content="Close">
<button
onClick={onExit}
type="button"
data-testid="modal-close"
aria-label="Close"
className="btn btn--transparent unround-t unround-br color-gray py12 px12 absolute top right"
{onExit && (
<Tooltip content="Close">
<button
onClick={onExit}
type="button"
data-testid="modal-close"
aria-label="Close"
className="btn btn--transparent unround-t unround-br color-gray py12 px12 absolute top right"
>
<Icon name="close" />
</button>
</Tooltip>}
<Icon name="close" />
</button>
</Tooltip>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Overlay>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);

if (!allowEventBubbling) {
return (
<EventTrap>
{modal}
</EventTrap>
);
return <EventTrap>{modal}</EventTrap>;
}

return modal;
Expand All @@ -190,6 +189,11 @@ Modal.propTypes = {
* to do something, instead of allowing them to sneak out of the modal.
*/
onExit: PropTypes.func,
/**
* If `onExit` is provided but this prop is set as false, a click on the underlay will
* not close the modal. The only way of closing the modal is clicking on the close button.
*/
exitOnUnderlayClicked: PropTypes.bool,
/**
* Modal container size. Options are `small`, `large`, or `auto`. If `auto`
* is provided, a width is not specified.
Expand Down

0 comments on commit 9aba8d7

Please sign in to comment.