-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Alert & RadioGroup components (#274)
* add Alert component * expose Alert * rename forgotten FLYOUT to POPOVER * use PopoverRenderPropArg * organize imports in a consistent way * ensure Portals behave as expected Portals can be nested from a React perspective, however in the DOM they are rendered as siblings, this is mostly fine. However, when they are rendered inside a Dialog, the Dialog itself is marked with `role="modal"` which makes all the other content inert. This means that rendering Menu.Items in a Portal or an Alert in a portal makes it non-interactable. Alerts are not even announced. To fix this, we ensure that we make the `root` of the Portal the actual dialog. This allows you to still interact with it, because an open modal is the "root" for the assistive technology. But there is a catch, a Dialog in a Dialog *can* render as a sibling, because you force the focus into the new Dialog. So we also ensured that Dialogs are always rendered in the portal root, and not inside another Dialog. * add dialog with alert example * add internal Description component * add internal Label component * add RadioGroup component * expose RadioGroup * add RadioGroup example * ensure to include tha RadioGroup.Option own id * update changelog * split documentation
- Loading branch information
1 parent
6faff6b
commit b99304a
Showing
43 changed files
with
5,230 additions
and
3,080 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
126 changes: 126 additions & 0 deletions
126
packages/@headlessui-react/pages/dialog/dialog-with-alert.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import React, { useState, Fragment } from 'react' | ||
import { Dialog, Portal, Transition, Alert } from '@headlessui/react' | ||
|
||
export default function Home() { | ||
let [isOpen, setIsOpen] = useState(false) | ||
let [deleted, setDeleted] = useState(false) | ||
|
||
function notifyUser() { | ||
setDeleted(true) | ||
setTimeout(() => { | ||
setDeleted(false) | ||
}, 10 * 1000) | ||
} | ||
|
||
return ( | ||
<> | ||
<button | ||
type="button" | ||
onClick={() => setIsOpen(v => !v)} | ||
className="m-12 px-4 py-2 text-base font-medium leading-6 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue sm:text-sm sm:leading-5" | ||
> | ||
Toggle! | ||
</button> | ||
|
||
<Transition show={isOpen} as={Fragment}> | ||
<Dialog open={isOpen} onClose={setIsOpen} static> | ||
<div className="fixed z-10 inset-0 overflow-y-auto"> | ||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> | ||
<Transition.Child | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0" | ||
> | ||
<Dialog.Overlay className="fixed inset-0 transition-opacity"> | ||
<div className="absolute inset-0 bg-gray-500 opacity-75"></div> | ||
</Dialog.Overlay> | ||
</Transition.Child> | ||
|
||
<Transition.Child | ||
enter="ease-out transform duration-300" | ||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
enterTo="opacity-100 translate-y-0 sm:scale-100" | ||
leave="ease-in transform duration-200" | ||
leaveFrom="opacity-100 translate-y-0 sm:scale-100" | ||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" | ||
> | ||
{/* This element is to trick the browser into centering the modal contents. */} | ||
<span | ||
className="hidden sm:inline-block sm:align-middle sm:h-screen" | ||
aria-hidden="true" | ||
> | ||
​ | ||
</span> | ||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> | ||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | ||
<div className="sm:flex sm:items-start"> | ||
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> | ||
{/* Heroicon name: exclamation */} | ||
<svg | ||
className="h-6 w-6 text-red-600" | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
stroke="currentColor" | ||
aria-hidden="true" | ||
> | ||
<path | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth={2} | ||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" | ||
/> | ||
</svg> | ||
</div> | ||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> | ||
<Dialog.Title | ||
as="h3" | ||
className="text-lg leading-6 font-medium text-gray-900" | ||
> | ||
Deactivate account | ||
</Dialog.Title> | ||
<div className="mt-2"> | ||
<p className="text-sm text-gray-500"> | ||
Are you sure you want to deactivate your account? All of your data will | ||
be permanently removed. This action cannot be undone. | ||
</p> | ||
{deleted && ( | ||
<Portal> | ||
<div className="fixed z-50 bg-blue-400 text-white font-bold text-lg p-4 py-6 top-0 left-0 right-0"> | ||
<Alert>I am now deleted!</Alert> | ||
</div> | ||
</Portal> | ||
)} | ||
<div className="relative inline-block text-left mt-10"></div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> | ||
<button | ||
type="button" | ||
onClick={() => notifyUser()} | ||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:shadow-outline-red sm:ml-3 sm:w-auto sm:text-sm" | ||
> | ||
Deactivate | ||
</button> | ||
<button | ||
type="button" | ||
onClick={() => setIsOpen(false)} | ||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:shadow-outline-indigo sm:mt-0 sm:w-auto sm:text-sm" | ||
> | ||
Cancel | ||
</button> | ||
</div> | ||
</div> | ||
</Transition.Child> | ||
</div> | ||
</div> | ||
</Dialog> | ||
</Transition> | ||
</> | ||
) | ||
} |
101 changes: 101 additions & 0 deletions
101
packages/@headlessui-react/pages/radio-group/radio-group.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import React, { useState } from 'react' | ||
import { RadioGroup } from '@headlessui/react' | ||
import { classNames } from '../../src/utils/class-names' | ||
|
||
export default function Home() { | ||
let access = [ | ||
{ | ||
id: 'access-1', | ||
name: 'Public access', | ||
description: 'This project would be available to anyone who has the link', | ||
}, | ||
{ | ||
id: 'access-2', | ||
name: 'Private to Project Members', | ||
description: 'Only members of this project would be able to access', | ||
}, | ||
{ | ||
id: 'access-3', | ||
name: 'Private to you', | ||
description: 'You are the only one able to access this project', | ||
}, | ||
] | ||
let [active, setActive] = useState() | ||
|
||
return ( | ||
<div className="p-12 max-w-xl"> | ||
<a href="/">Link before</a> | ||
<RadioGroup value={active} onChange={setActive}> | ||
<fieldset className="space-y-4"> | ||
<legend> | ||
<h2 className="text-xl">Privacy setting</h2> | ||
</legend> | ||
|
||
<div className="bg-white rounded-md -space-y-px"> | ||
{access.map(({ id, name, description }, i) => { | ||
return ( | ||
<RadioGroup.Option | ||
key={id} | ||
value={id} | ||
className={({ active }) => | ||
classNames( | ||
// Rounded corners | ||
i === 0 && 'rounded-tl-md rounded-tr-md', | ||
access.length - 1 === i && 'rounded-bl-md rounded-br-md', | ||
|
||
// Shared | ||
'relative border p-4 flex focus:outline-none', | ||
active ? 'bg-indigo-50 border-indigo-200 z-10' : 'border-gray-200' | ||
) | ||
} | ||
> | ||
{({ active, checked }) => ( | ||
<div className="flex justify-between items-center w-full"> | ||
<div className="ml-3 flex flex-col cursor-pointer"> | ||
<span | ||
className={classNames( | ||
'block text-sm leading-5 font-medium', | ||
active ? 'text-indigo-900' : 'text-gray-900' | ||
)} | ||
> | ||
{name} | ||
</span> | ||
<span | ||
className={classNames( | ||
'block text-sm leading-5', | ||
active ? 'text-indigo-700' : 'text-gray-500' | ||
)} | ||
> | ||
{description} | ||
</span> | ||
</div> | ||
<div> | ||
{checked && ( | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
stroke="currentColor" | ||
className="h-5 w-5 text-indigo-500" | ||
> | ||
<path | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth={2} | ||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" | ||
/> | ||
</svg> | ||
)} | ||
</div> | ||
</div> | ||
)} | ||
</RadioGroup.Option> | ||
) | ||
})} | ||
</div> | ||
</fieldset> | ||
</RadioGroup> | ||
<a href="/">Link after</a> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
## Alert | ||
|
||
A component for announcing information to screenreader/assistive technology users. | ||
|
||
- [Installation](#installation) | ||
- [Basic example](#basic-example) | ||
- [Component API](#component-api) | ||
|
||
### Installation | ||
|
||
```sh | ||
# npm | ||
npm install @headlessui/react | ||
|
||
# Yarn | ||
yarn add @headlessui/react | ||
``` | ||
|
||
### Basic example | ||
|
||
```jsx | ||
<Alert>Notifications have been enabled</Alert> | ||
``` | ||
|
||
### Component API | ||
|
||
#### Alert | ||
|
||
```jsx | ||
<Alert>Notifications have been enabled</Alert> | ||
``` | ||
|
||
##### Props | ||
|
||
| Prop | Type | Default | Description | | ||
| :----------- | :---------------------- | :------- | :------------------------------------------------------------------------------ | | ||
| `as` | String \| Component | `div` | The element or component the `Alert` should render as. | | ||
| `importance` | `polite` \| `assertive` | `polite` | The importance of the alert message when it is announced to screenreader users. | | ||
|
||
| Importance | Description | | ||
| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `polite` | Indicates that updates to the region should be presented at the next graceful opportunity, such as at the end of speaking the current sentence or when the user pauses typing. | | ||
| `assertive` | Indicates that updates to the region have the highest priority and should be presented the user immediately. | | ||
|
||
Source: https://www.w3.org/TR/wai-aria-1.2/#aria-live | ||
|
||
##### Render prop object | ||
|
||
- None |
31 changes: 31 additions & 0 deletions
31
packages/@headlessui-react/src/components/alert/alert.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react' | ||
import { render } from '@testing-library/react' | ||
import { getByText } from '../../test-utils/accessibility-assertions' | ||
|
||
import { Alert } from './alert' | ||
|
||
describe('Rendering', () => { | ||
it('should be possible to render an Alert', () => { | ||
render(<Alert>This is an alert</Alert>) | ||
|
||
expect(getByText('This is an alert')).toHaveAttribute('role', 'status') | ||
}) | ||
|
||
it('should be possible to render an Alert using a render prop', () => { | ||
render(<Alert>{() => 'This is an alert'}</Alert>) | ||
|
||
expect(getByText('This is an alert')).toHaveAttribute('role', 'status') | ||
}) | ||
|
||
it('should be possible to render an Alert with an explicit level of importance (polite)', () => { | ||
render(<Alert importance="polite">This is a polite message</Alert>) | ||
|
||
expect(getByText('This is a polite message')).toHaveAttribute('role', 'status') | ||
}) | ||
|
||
it('should be possible to render an Alert with an explicit level of importance (assertive)', () => { | ||
render(<Alert importance="assertive">This is a assertive message</Alert>) | ||
|
||
expect(getByText('This is a assertive message')).toHaveAttribute('role', 'alert') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
useMemo, | ||
|
||
// Types | ||
ElementType, | ||
} from 'react' | ||
|
||
import { Props } from '../../types' | ||
import { render } from '../../utils/render' | ||
import { match } from '../../utils/match' | ||
|
||
type Importance = | ||
/** | ||
* Indicates that updates to the region should be presented at the next | ||
* graceful opportunity, such as at the end of speaking the current sentence | ||
* or when the user pauses typing. | ||
*/ | ||
| 'polite' | ||
|
||
/** | ||
* Indicates that updates to the region have the highest priority and should | ||
* be presented the user immediately. | ||
*/ | ||
| 'assertive' | ||
|
||
// --- | ||
|
||
let DEFAULT_ALERT_TAG = 'div' as const | ||
interface AlertRenderPropArg { | ||
importance: Importance | ||
} | ||
type AlertPropsWeControl = 'role' | ||
|
||
export function Alert<TTag extends ElementType = typeof DEFAULT_ALERT_TAG>( | ||
props: Props<TTag, AlertRenderPropArg, AlertPropsWeControl> & { | ||
importance?: Importance | ||
} | ||
) { | ||
let { importance = 'polite', ...passThroughProps } = props | ||
let propsWeControl = match(importance, { | ||
polite: () => ({ role: 'status' }), | ||
assertive: () => ({ role: 'alert' }), | ||
}) | ||
|
||
let bag = useMemo<AlertRenderPropArg>(() => ({ importance }), [importance]) | ||
|
||
return render({ ...passThroughProps, ...propsWeControl }, bag, DEFAULT_ALERT_TAG) | ||
} |
Oops, something went wrong.