Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): add dialog component #9496

Merged
Binary file not shown.
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"lodash.throttle": "^4.1.1",
"react-is": "^16.8.6",
"use-resize-observer": "^6.0.0",
"wicg-inert": "^3.1.1",
"window-or-global": "^1.0.1"
},
"devDependencies": {
Expand Down
152 changes: 152 additions & 0 deletions packages/react/src/components/Dialog/Dialog-story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as React from 'react';
import { FocusScope } from '../FocusScope';
import { Dialog } from '../Dialog';
import { useId } from '../../internal/useId';
import { Portal } from '../Portal';

export default {
title: 'Experimental/unstable_Dialog',
includeStories: [],
};

export const Default = () => {
function DemoComponent() {
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);

return (
<div
style={{
border: '1px solid black',
background: 'rgba(0, 0, 0, 0.1)',
padding: '1rem',
}}>
<button
type="button"
onClick={() => {
setOpen(true);
}}>
Open
</button>
{open ? (
<FocusScope>
<div>
<p>
Elit hic at labore culpa itaque fugiat. Consequuntur iure autem
autem officiis dolores facilis nulla earum! Neque quia nemo
sequi assumenda ratione officia Voluptate beatae eligendi
placeat nemo laborum, ratione.
</p>
<DemoComponent />
<button
ref={ref}
type="button"
onClick={() => {
setOpen(false);
}}>
Close
</button>
</div>
</FocusScope>
) : null}
</div>
);
}
return (
<>
<DemoComponent />
<button type="button">Hello</button>
</>
);
};

export const DialogExample = () => {
function Example() {
const [open, setOpen] = React.useState(false);
const id = useId();

return (
<div>
<div>
<button type="button">First</button>
</div>
<button
type="button"
onClick={() => {
setOpen(true);
}}>
Open
</button>
{open ? (
<Portal
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 9999,
}}>
<FullPage />
<Dialog
aria-labelledby={id}
onDismiss={() => {
setOpen(false);
}}
style={{
position: 'relative',
zIndex: 9999,
padding: '1rem',
background: 'white',
}}>
<div>
<span id={id}>Hello</span>
</div>
<div>
<Example />
</div>
<button
type="button"
onClick={() => {
setOpen(false);
}}>
Close
</button>
</Dialog>
</Portal>
) : null}

<div>
<button type="button">Last</button>
</div>
</div>
);
}

return <Example />;
};

const FullPage = React.forwardRef(function FullPage(props, ref) {
return (
<div
ref={ref}
style={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
transform: 'translateZ(0)',
background: 'rgba(0, 0, 0, 0.5)',
}}
{...props}
/>
);
});
153 changes: 153 additions & 0 deletions packages/react/src/components/Dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* Copyright IBM Corp. 2016, 2018
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import 'wicg-inert';
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import { FocusScope } from '../FocusScope';
import { useMergedRefs } from '../../internal/useMergedRefs';
import { useSavedCallback } from '../../internal/useSavedCallback';
import { match, keys } from '../../internal/keyboard';

/**
* @see https://www.tpgi.com/the-current-state-of-modal-dialog-accessibility/
*/
const Dialog = React.forwardRef(function Dialog(props, forwardRef) {
const { 'aria-labelledby': labelledBy, children, onDismiss, ...rest } = props;
const dialogRef = useRef(null);
const ref = useMergedRefs([dialogRef, forwardRef]);
const savedOnDismiss = useSavedCallback(onDismiss);

function onKeyDown(event) {
if (match(event, keys.Escape)) {
event.stopPropagation();
savedOnDismiss();
}
}

useEffect(() => {
const changes = hide(document.body, dialogRef.current);
return () => {
show(changes);
};
}, []);

return (
<FocusScope
{...rest}
aria-labelledby={labelledBy}
aria-modal="true"
initialFocusRef={dialogRef}
onKeyDown={onKeyDown}
ref={ref}
role="dialog"
tabIndex="-1">
{children}
</FocusScope>
);
});

Dialog.propTypes = {
/**
* Provide the associated element that labels the Dialog
*/
'aria-labelledby': PropTypes.string.isRequired,

/**
* Provide children to be rendered inside of the Dialog
*/
children: PropTypes.node,

/**
* Provide a handler that is called when the Dialog is requesting to be closed
*/
onDismiss: PropTypes.func.isRequired,
};

if (__DEV__) {
Dialog.displayName = 'Dialog';
}

function hide(root, dialog) {
const changes = [];
const queue = Array.from(root.childNodes);

while (queue.length !== 0) {
const node = queue.shift();

if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}

// If a node is the dialog, do nothing
if (node === dialog) {
continue;
}

// If a tree contains our dialog, traverse its children
if (node.contains(dialog)) {
queue.push(...Array.from(node.childNodes));
continue;
}

// If a node is a bumper, do nothing
if (
node.hasAttribute('data-carbon-focus-scope') &&
(dialog.previousSibling === node || dialog.nextSibling === node)
) {
continue;
}

if (node.getAttribute('aria-hidden') === 'true') {
continue;
}

if (node.hasAttribute('inert')) {
continue;
}

if (node.getAttribute('aria-hidden') === 'false') {
node.setAttribute('aria-hidden', 'true');
node.setAttribute('inert', '');
changes.push({
node,
attributes: {
'aria-hidden': 'false',
},
});
continue;
}

// Otherwise, set it to inert and set aria-hidden to true
node.setAttribute('aria-hidden', 'true');
node.setAttribute('inert', '');

changes.push({
node,
});
}

return changes;
}

function show(changes) {
changes.forEach(({ node, attributes }) => {
node.removeAttribute('inert');
// This mutation needs to be asynchronous to allow the polyfill time to
// observe the change and allow mutations to occur
// https://github.com/WICG/inert#performance-and-gotchas
setTimeout(() => {
if (attributes && attributes['aria-hidden']) {
node.setAttribute('aria-hidden', attributes['aria-hidden']);
} else {
node.removeAttribute('aria-hidden');
}
}, 0);
});
}

export { Dialog };
Loading