Commit 2b18c185 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Remove unused local Dialog components and styles

parent 29bd7b79
import { LabeledButton } from '@hypothesis/frontend-shared';
import Dialog from './Dialog';
/**
* @typedef ConfirmDialogProps
* @prop {string} title - Title of the dialog
* @prop {string} message - Main text of the message
* @prop {string} confirmAction - Label for the "Confirm" button
* @prop {() => any} onConfirm - Callback invoked if the user clicks the "Confirm" button
* @prop {() => any} onCancel - Callback invoked if the user cancels
*/
/**
* A prompt asking the user to confirm an action.
*
* @param {ConfirmDialogProps} props
*/
export default function ConfirmDialog({
title,
message,
confirmAction,
onConfirm,
onCancel,
}) {
return (
<Dialog
title={title}
onCancel={onCancel}
buttons={[
<LabeledButton key="ok" onClick={onConfirm} variant="primary">
{confirmAction}
</LabeledButton>,
]}
>
<p>{message}</p>
</Dialog>
);
}
import {
LabeledButton,
useElementShouldClose,
} from '@hypothesis/frontend-shared';
import { Fragment } from 'preact';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import classNames from 'classnames';
let idCounter = 0;
/**
* Return an element ID beginning with `prefix` that is unique per component instance.
*
* This avoids different instances of a component re-using the same ID.
*
* @param {string} prefix
*/
function useUniqueId(prefix) {
const [id] = useState(() => {
++idCounter;
return `${prefix}-${idCounter}`;
});
return id;
}
/**
* @typedef {import("preact").ComponentChildren} Children
*
* @typedef DialogProps
* @prop {Children} children - The content of the dialog.
* @prop {import("preact/hooks").Ref<HTMLElement>} [initialFocus] -
* Child element to focus when the dialog is rendered.
* @prop {Children} [buttons] -
* Additional `Button` elements to display at the bottom of the dialog.
* A "Cancel" button is added automatically if the `onCancel` prop is set.
* @prop {string} [contentClass] - CSS class to apply to the dialog's content
* @prop {'dialog'|'alertdialog'} [role] - The aria role for the dialog (defaults to" dialog")
* @prop {string} title - The title of the dialog.
* @prop {() => any} [onCancel] -
* A callback to invoke when the user cancels the dialog. If provided, a
* "Cancel" button will be displayed.
* @prop {string} [cancelLabel] - Label for the cancel button
*/
/**
* HTML control that can be disabled.
*
* @typedef {HTMLElement & { disabled: boolean }} InputElement
*/
/**
* A modal dialog wrapper with a title. The wrapper sets initial focus to itself
* unless an element inside of it is specified with the `initialFocus` ref.
* Optional action buttons may be passed in with the `buttons` prop but the
* cancel button is automatically generated when the on `onCancel` function is
* passed.
*
* Canonical resources:
*
* https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html
* https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/alertdialog.html
*
* If the dialog's content, specified by the `children` prop, contains a paragraph
* (`<p>`) element, that element will be identified as the dialog's accessible
* description.
*
* @param {DialogProps} props
*/
export default function Dialog({
children,
contentClass,
initialFocus,
onCancel,
cancelLabel = 'Cancel',
role = 'dialog',
title,
buttons,
}) {
const dialogTitleId = useUniqueId('dialog-title');
const dialogDescriptionId = useUniqueId('dialog-description');
const rootEl = useRef(/** @type {HTMLDivElement | null} */ (null));
useElementShouldClose(rootEl, true, () => {
if (onCancel) {
onCancel();
}
});
useEffect(() => {
const focusEl = /** @type {InputElement|undefined} */ (
initialFocus?.current
);
if (focusEl && !focusEl.disabled) {
focusEl.focus();
} else {
// Modern accessibility guidance is to focus the dialog itself rather than
// trying to be smart about focusing a particular control within the
// dialog. See resources above.
rootEl.current.focus();
}
// We only want to run this effect once when the dialog is mounted.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Try to assign the dialog an accessible description, using the content of
// the first paragraph of text in it.
//
// A limitation of this approach is that it doesn't update if the dialog's
// content changes after the initial render.
useLayoutEffect(() => {
const description = rootEl.current.querySelector('p');
if (description) {
description.id = dialogDescriptionId;
rootEl.current.setAttribute('aria-describedby', dialogDescriptionId);
}
}, [dialogDescriptionId]);
return (
<Fragment>
<div className="Dialog__background" />
<div className="Dialog__container">
<div
tabIndex={-1}
ref={rootEl}
role={role}
aria-labelledby={dialogTitleId}
aria-modal={true}
className={classNames('Dialog__content', contentClass)}
>
<h1 className="Dialog__title" id={dialogTitleId}>
{title}
<span className="u-stretch" />
{onCancel && (
<button
aria-label="Close"
className="Dialog__cancel-btn"
onClick={onCancel}
>
</button>
)}
</h1>
{children}
<div className="u-stretch" />
<div className="Dialog__actions">
{onCancel && (
<LabeledButton icon="cancel" onClick={onCancel}>
{cancelLabel}
</LabeledButton>
)}
{buttons}
</div>
</div>
</div>
</Fragment>
);
}
import { mount } from 'enzyme';
import mockImportedComponents from '../../../test-util/mock-imported-components';
import ConfirmDialog, { $imports } from '../ConfirmDialog';
describe('ConfirmDialog', () => {
beforeEach(() => {
$imports.$mock(mockImportedComponents());
});
afterEach(() => {
$imports.$restore();
});
it('renders dialog', () => {
const confirm = sinon.stub();
const cancel = sinon.stub();
const wrapper = mount(
<ConfirmDialog
title="Delete annotation?"
message="Do you want to delete this annotation?"
confirmAction="Do it!"
onConfirm={confirm}
onCancel={cancel}
/>
);
const dialog = wrapper.find('Dialog');
assert.equal(dialog.prop('title'), 'Delete annotation?');
assert.equal(
dialog.children().text(),
'Do you want to delete this annotation?'
);
assert.equal(dialog.prop('onCancel'), cancel);
assert.equal(mount(dialog.prop('buttons')[0]).prop('onClick'), confirm);
});
});
import { mount } from 'enzyme';
import { createRef } from 'preact';
import Dialog from '../Dialog';
import { checkAccessibility } from '../../../test-util/accessibility';
describe('Dialog', () => {
it('renders content', () => {
const wrapper = mount(
<Dialog>
<span>content</span>
</Dialog>
);
assert.isTrue(wrapper.contains(<span>content</span>));
});
it('adds `contentClass` value to class list', () => {
const wrapper = mount(
<Dialog contentClass="foo">
<span>content</span>
</Dialog>
);
assert.isTrue(wrapper.find('.Dialog__content').hasClass('foo'));
});
it('renders buttons', () => {
const wrapper = mount(
<Dialog
buttons={[
<button key="foo" name="foo" />,
<button key="bar" name="bar" />,
]}
/>
);
assert.isTrue(wrapper.contains(<button key="foo" name="foo" />));
assert.isTrue(wrapper.contains(<button key="bar" name="bar" />));
});
it('renders the title', () => {
const wrapper = mount(<Dialog title="Test dialog" />);
const header = wrapper.find('h1');
assert.equal(header.text().indexOf('Test dialog'), 0);
});
it('closes when Escape key is pressed', () => {
const onCancel = sinon.stub();
const container = document.createElement('div');
document.body.appendChild(container);
mount(<Dialog title="Test dialog" onCancel={onCancel} />, {
attachTo: container,
});
const event = new Event('keydown');
event.key = 'Escape';
document.body.dispatchEvent(event);
assert.called(onCancel);
container.remove();
});
it('closes when close button is clicked', () => {
const onCancel = sinon.stub();
const wrapper = mount(<Dialog title="Test dialog" onCancel={onCancel} />);
wrapper.find('.Dialog__cancel-btn').simulate('click');
assert.called(onCancel);
});
it(`defaults cancel button's label to "Cancel"`, () => {
const wrapper = mount(<Dialog onCancel={sinon.stub()} />);
assert.equal(wrapper.find('LabeledButton').text().trim(), 'Cancel');
});
it('adds a custom label to the cancel button', () => {
const wrapper = mount(
<Dialog onCancel={sinon.stub()} cancelLabel="hello" />
);
assert.equal(wrapper.find('LabeledButton').text().trim(), 'hello');
});
describe('initial focus', () => {
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
container.remove();
});
it('focuses the `initialFocus` element', () => {
const inputRef = createRef();
mount(
<Dialog initialFocus={inputRef}>
<input ref={inputRef} />
</Dialog>,
{ attachTo: container }
);
assert.equal(document.activeElement, inputRef.current);
});
it('focuses the dialog if `initialFocus` prop is missing', () => {
const wrapper = mount(
<Dialog>
<div>Test</div>
</Dialog>,
{ attachTo: container }
);
assert.equal(
document.activeElement,
wrapper.find('[role="dialog"]').getDOMNode()
);
});
it('focuses the dialog if `initialFocus` ref is `null`', () => {
const wrapper = mount(
<Dialog initialFocus={{ current: null }}>
<div>Test</div>
</Dialog>,
{ attachTo: container }
);
assert.equal(
document.activeElement,
wrapper.find('[role="dialog"]').getDOMNode()
);
});
it('focuses the dialog if `initialFocus` element is disabled', () => {
const inputRef = createRef();
const wrapper = mount(
<Dialog initialFocus={inputRef}>
<button ref={inputRef} disabled={true} />
</Dialog>,
{ attachTo: container }
);
assert.equal(
document.activeElement,
wrapper.find('[role="dialog"]').getDOMNode()
);
});
});
it("marks the first `<p>` in the dialog's content as the accessible description", () => {
const wrapper = mount(
<Dialog>
<p>Enter a URL</p>
</Dialog>
);
const content = wrapper.find('[role="dialog"]').getDOMNode();
const paragraphEl = wrapper.find('p').getDOMNode();
assert.ok(content.getAttribute('aria-describedby'));
assert.equal(content.getAttribute('aria-describedby'), paragraphEl.id);
});
it("does not set an accessible description if the dialog's content does not have a `<p>`", () => {
const wrapper = mount(
<Dialog>
<button>Click me</button>
</Dialog>
);
const content = wrapper.find('[role="dialog"]').getDOMNode();
assert.isNull(content.getAttribute('aria-describedby'));
});
it(
'should pass a11y checks',
checkAccessibility({
// eslint-disable-next-line react/display-name
content: () => (
<Dialog title="Test dialog">
<div>test</div>
</Dialog>
),
})
);
});
......@@ -10,9 +10,6 @@
// -----------------
@use '@hypothesis/frontend-shared/styles';
// Shared button styles (TEMPORARY)
@use '../shared';
// Annotator-specific components.
@use './components/AdderToolbar';
@use './components/Buckets';
......
@use './components/Dialog';
@use "@hypothesis/frontend-shared/styles/mixins/focus";
@use "../variables" as var;
$title-font-size: 19px;
.Dialog__container {
display: flex;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.Dialog__background {
background-color: black;
bottom: 0;
left: 0;
opacity: 0.5;
position: fixed;
right: 0;
top: 0;
}
.Dialog__content {
@include focus.outline-on-keyboard-focus;
display: flex;
flex-direction: column;
max-height: 90vh;
max-width: 700px; // default for older browsers
max-width: min(700px, 90vw);
background-color: white;
border-radius: 3px;
margin: auto;
padding: 20px;
}
.Dialog__title {
display: flex;
align-items: center;
font-size: $title-font-size;
}
.Dialog__cancel-btn {
@include focus.outline-on-keyboard-focus;
border: 0;
background: none;
color: var.$color-grey-6;
// Given the button a large hit target and ensure the 'X' label is large
// enough to see easily and aligned with the right edge of the dialog.
// Add negative margins so that the button does not force the dialog to
// grow in height.
font-size: $title-font-size;
padding: 5px;
margin: -10px 0px;
&:hover {
cursor: pointer;
color: var.$color-brand;
}
}
.Dialog__actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 10px;
& > *:not(:first-child) {
margin-left: 5px;
}
}
......@@ -11,10 +11,6 @@
// -----------------
@use '@hypothesis/frontend-shared/styles';
// Temporary shared styles
// FIXME: Currently needed for Dialog styling
@use '../shared';
// Custom button styling for the application
@use './buttons';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment