Skip to content

Commit

Permalink
Merge pull request #1084 from primer/overlay
Browse files Browse the repository at this point in the history
Overlay
  • Loading branch information
emplums authored Mar 30, 2021
2 parents 79e3381 + c368635 commit f970ada
Show file tree
Hide file tree
Showing 21 changed files with 958 additions and 2 deletions.
84 changes: 84 additions & 0 deletions docs/content/Overlay.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Overlay
---

An `Overlay` is a flexible floating surface, used to display transient content such as menus, selection options, dialogs, and more. Overlays use shadows to express elevation. The `Overlay` component handles all behaviors needed by overlay UIs as well as the common styles that all overlays should have. `Overlay` is the base component for many of our overlay-type components.

Behaviors include:

- Rendering the overlay in a React Portal so that it always renders on top of other content on the page
- Positioning the overlay according to passed in settings, using our context-aware positioning algorithms
- Trapping focus
- Calling a user provided function when the user presses `Escape`
- Calling a user provided function when the user clicks outside of the container
- Focusing either user provided element, or the first focusable element in the container when it is opened
- Returning focus to an element when container is closed

## Accessibility considerations

- The `Overlay` must either have:
- A value set for the `aria-labelledby` attribute that refers to a visible title.
- An `aria-label` attribute
- If the `Overlay` should also have a longer description, use `aria-describedby`
- The `Overlay` component has a `role="dialog"` set on it, if you are using `Overlay` for alerts, you can pass in `role="alertdialog"` instead. Please read the [W3C guidelines](https://www.w3.org/TR/wai-aria-1.1/#alertdialog) to determine which role is best for your use case
- The `Overlay` component has `aria-modal` set to `true` by default and should not be overridden as all `Overlay`s behave as modals.

See the W3C accessibility recommendations for modals [here](https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_roles_states_props).

## Default example

```javascript live noinline
const Demo = () => {
// you must manage your own open state
const [isOpen, setIsOpen] = React.useState(false)
const noButtonRef = React.useRef(null)
const anchorRef = React.useRef(null)
return (
<>
<Button ref={anchorRef} onClick={() => setIsOpen(!isOpen)}>
open overlay
</Button>
{/* be sure to conditionally render the Overlay. This helps with performance and is required. */}
{isOpen &&
<Overlay
anchorRef={anchorRef}
initialFocusRef={noButtonRef}
returnFocusRef={anchorRef}
ignoreClickRefs={[anchorRef]}
onEscape={() => setIsOpen(!isOpen)}
onClickOutside={() => setIsOpen(false)}
aria-labelledby="title"
>
<Flex flexDirection="column" p={2}>
<Text id="title">Are you sure you would like to delete this item?</Text>
<Button >yes</Button>
<Button ref={noButtonRef}>no</Button>
</Flex>
</Overlay>
}

</>
)
}

render(<Demo/>)
```
## System props
`Overlay` gets `COMMON` system props. Read the [System Props](/system-props) doc page for a full list of available props.
## Component props
| Name | Type | Default | Description |
| :--- | :----- | :-----: | :---------------------------------- |
| positionSettings | See the [`PositionSettings interface`]() section of the `anchoredPosition` docs | `{side: 'outside-bottom', align: 'start', anchorOffset: 4, alignmentOffset: 4, allowOutOfBounds: false }` | Optional. Settings used to position the `Overlay`. If none are provided, `Overlay` is positioned on the bottom left of the `anchorRef`. |
| positionDeps | `React.DependencyList` | `undefined` | Optional. If defined, the position of the `Overlay` will only be recalulated when one of the dependencies in this array changes. |
| ignoreClickRefs | `React.RefObject<HTMLElement> []` | `undefined` | Optional. An array of ref objects to ignore clicks on in the `onOutsideClick` behavior. This is often used to ignore clicking on the element that toggles the open/closed state for the `Overlay` to prevent the `Overlay` from being toggled twice. |
| initialFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Optional. Ref for the element to focus when the `Overlay` is opened. If nothing is provided, the first focusable element in the `Overlay` body is focused. |
| anchorRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Element the `Overlay` should be anchored to. |
| returnFocusRef | `React.RefObject<HTMLElement>` | `undefined` | Required. Ref for the element to focus when the `Overlay` is closed. |
| onClickOutside | `function` | `undefined` | Required. Function to call when clicking outside of the `Overlay`. Typically this function sets the `Overlay` visibility state to `false`. |
| onEscape | `function` | `undefined` | Required. Function to call when user presses `Escape`. Typically this function sets the `Overlay` visibility state to `false`. |
| width | `'sm', 'md', 'lg', 'xl', 'auto'` | `auto` | Sets the width of the `Overlay`, pick from our set list of widths, or pass `auto` to automatically set the width based on the content of the `Overlay`. `sm` corresponds to `256px`, `md` corresponds to `320px`, `lg` corresponds to `480px`, and `xl` corresponds to `640px`. |
| height | `'sm', 'md', 'auto'` | `auto` | Sets the height of the `Overlay`, pick from our set list of heights, or pass `auto` to automatically set the height based on the content of the `Overlay`. `sm` corresponds to `480px` and `md` corresponds to `640px`. |
4 changes: 2 additions & 2 deletions docs/content/anchoredPosition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,5 @@ export const AnchoredPositionExample = () => {
| Name | Type | Default | Description |
| :- | :- | :-: | :- |
| floatingElementRef | `React.RefObject` | `undefined` | If provided, this will be the ref used to access the element that will be used for the floating element. Its size measurements are needed by the underlying `useAnchoredPosition` behavior. Otherwise, this hook will create the ref for you and return it. In both cases, the ref must be provided to the floating element's JSX. |
| anchorElementRef | `React.RefObject` | `undefined` | If provided, this will be the ref used to access the element that will be used for the anchor element. Its position and size measurements are needed by the underlying `useAnchoredPosition` behavior. Otherwise, this hook will create the ref for you and return it. In both cases, the ref must be provided to the anchor element's JSX. |
| floatingElementRef | `React.RefObject<HTMLElement>` | `undefined` | If provided, this will be the ref used to access the element that will be used for the floating element. Its size measurements are needed by the underlying `useAnchoredPosition` behavior. Otherwise, this hook will create the ref for you and return it. In both cases, the ref must be provided to the floating element's JSX. |
| anchorElementRef | `React.RefObject<HTMLElement>` | `undefined` | If provided, this will be the ref used to access the element that will be used for the anchor element. Its position and size measurements are needed by the underlying `useAnchoredPosition` behavior. Otherwise, this hook will create the ref for you and return it. In both cases, the ref must be provided to the anchor element's JSX. |
41 changes: 41 additions & 0 deletions docs/content/useOnEscapePress.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: useOnEscapePress
---

`useOnEscapePress` is a simple utility Hook that calls a user provided function when the `Escape` key is pressed.

### Usage

```javascript live noinline
const OverlayDemo = ({onEscape, children}) => {
useOnEscapePress({onEscape})
return (
<Box height="200px">
{children}
</Box>
)
}

function DemoComponent() {
const [isOpen, setIsOpen] = React.useState(false)
return (
<>
<Button onClick={() => setIsOpen(!isOpen)}>toggle</Button>
{isOpen &&
<OverlayDemo onEscape={() => setIsOpen(false)}>
<Button>Button One</Button>
<Button>Button Two</Button>
</OverlayDemo>}
</>
)
}

render(<DemoComponent/>)
```


#### useOnEscapePress settings

| Name | Type | Default | Description |
| :- | :- | :-: | :- |
| onEscape | `function` | | Function to call when user presses the Escape key |
49 changes: 49 additions & 0 deletions docs/content/useOnOutsideClick.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: useOnOutsideClick
---

`useOnOutsideClick` is a utility Hook that calls a user provided callback function when the user clicks outside of the provided container.

You can also pass an array of `ignoredRefs` to prevent calling the callback function on additional elements on the page. This can be handy for ignoring clicks on trigger buttons that already manage the open/closed state of content.


### Usage

```jsx live
<State>
{([isOpen, setIsOpen]) => {
const containerRef = React.useRef(null)
const triggerRef = React.useRef(null)

const closeOverlay = React.useCallback(() => {
setIsOpen(false)
}, [setIsOpen])

const toggleOverlay = React.useCallback(() => {
setIsOpen(!isOpen)
}, [setIsOpen, isOpen])

useOnOutsideClick({onClickOutside: closeOverlay, containerRef, ignoreClickRefs: [triggerRef]})

return (
<>
<Button ref={triggerRef} onClick={toggleOverlay}>toggle</Button>
{isOpen &&
<BorderBox height="200px" bg="green.4" ref={containerRef}>
content
</BorderBox>
}
</>
)
}}
</State>
```


#### useOnOutsideClick settings

| Name | Type | Default | Description |
| :- | :- | :-: | :- |
| onOutsideClick | `function` | | Function to call when user clicks outside of the container. Usually this manages the state of the visibilitiy of the container. |
| ignoredRefs| `React.RefObject<HTMLElement> []` | | Elements outside of the container to ignore clicks on. |
| containerRef | `React.RefObject<HTMLElement>` | | Required. A ref for the containing element. |
49 changes: 49 additions & 0 deletions docs/content/useOpenAndCloseFocus.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: useOpenAndCloseFocus
---

`useOpenAndCloseFocus` is a utility Hook that manages focusing an element when a component is first mounted, and returns focus to an element on the page when that component unmounts.

If no ref is passed to `inititalFocusRef` , the hook focuses the first focusable element inside of the container.


### Usage

```javascript live noinline
const Overlay = ({returnFocusRef, initialFocusRef, children}) => {
const containerRef = React.useRef(null)
useOpenAndCloseFocus({containerRef, returnFocusRef, initialFocusRef})
return (
<Box height="200px" ref={containerRef}>
{children}
</Box>
)
}

function Component() {
const returnFocusRef = React.useRef(null)
const initialFocusRef = React.useRef(null)
const [isOpen, setIsOpen] = React.useState(false)
return (
<Box sx={{'*': { ':focus' : { backgroundColor: 'red.5'}}}}>
<Button ref={returnFocusRef} onClick={() => setIsOpen(!isOpen)}>toggle</Button>
{isOpen &&
<Overlay returnFocusRef={returnFocusRef} initialFocusRef={initialFocusRef}>
<Button>Button One</Button>
<Button ref={initialFocusRef}>Button Two</Button>
</Overlay>}
</Box>
)
}

render(<Component/>)
```


#### useOpenAndCloseFocus settings

| Name | Type | Default | Description |
| :- | :- | :-: | :- |
| initialFocusRef | `React.RefObject<HTMLElement>` | | Optional. The element to focus when the container is mounted on the page. |
| returnFocusRef | `React.RefObject<HTMLElement>` | | Required. The element to focus when the container is unmounted. |
| containerRef | `React.RefObject<HTMLElement>` | | Required. A ref for the containing element. |
62 changes: 62 additions & 0 deletions docs/content/useOverlay.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: useOverlay
---

`useOverlay` calls all of the relevant behavior Hooks that all `Overlay` components & composite components should have and returns a ref to be passed down to the overlay's container.

These behaviors include:

- Correctly positioning the component based on the values provided to `positionSettings` and `positionDeps`.
- Trapping focus
- Calling a user provided function when the user presses `Escape`
- Calling a user provided function when the user clicks outside of the container
- Focusing the either a user provided element, or the first focusable element in the container when it is opened.
- Returning focus to an element when container is closed

**Note:** `useOverlay` and `Overlay` do not manage the open state of the overlay. We leave control of the open state up to the user. All behaviors are built with the assumption that the overlay will not be rendered on the page & fully unmounted when it is not visible. See the examples for details on how to conditionally render a component in JSX.

### Usage

```javascript live noinline

const DemoOverlay = ({onClickOutside, initialFocusRef, returnFocusRef, ignoreClickRefs, onEscape, ...rest}) => {
const overlayProps = useOverlay({returnFocusRef, onEscape, ignoreClickRefs, onClickOutside, initialFocusRef})
return <Box height="200px" bg="green.4" {...overlayProps} {...rest}/>
}

const DemoComponent = () => {
const returnFocusRef = React.useRef(null)
const initialFocusRef = React.useRef(null)
const [isOpen, setIsOpen] = React.useState(false)
const closeOverlay = () => setIsOpen(false)
return (
<>
<Button ref={returnFocusRef} onClick={() => setIsOpen(!isOpen)}>toggle</Button>
{isOpen &&
<DemoOverlay
returnFocusRef={returnFocusRef}
ignoreClickRefs={[returnFocusRef]}
initialFocusRef={initialFocusRef}
onEscape={closeOverlay}
onClickOutside={closeOverlay}
>
<Button>Button One</Button>
<Button ref={initialFocusRef}>Button Two</Button>
</DemoOverlay>}
</>
)
}

render(<DemoComponent/>)
```


#### useOnEscapePress settings

| Name | Type | Required | Description |
| :- | :- | :-: | :- |
| onEscapePress | `function` | required | Function to call when user presses the Escape key |
| onOutsideClick | `function` | required | Function to call when user clicks outside of the overlay |
| ignoreClickRefs | `React.RefObject<HTMLElement> []` | optional | Refs to click clicks on in the `onOutsideClick` function, useful for ignoring clicks on elements that trigger the overlay visibility. |
| initialFocusRef | `React.RefObject<HTMLElement>` | optional | Ref to focus when overlay is mounted. |
| returnFocusRef | `React.RefObject<HTMLElement>` | required | Ref to focus when overlay is unmounted. Important for accessibility. |
10 changes: 10 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
children:
- title: useSafeTimeout
url: /useSafeTimeout
# - title: useOnOutsideClick
# url: /useOnOutsideClick
# - title: useOpenAndCloseFocus
# url: /useOpenAndCloseFocus
# - title: useOnEscapePress
# url: /useOnEscapePress
# - title: useOverlay
# url: /useOverlay
- title: Components
children:
- title: Avatar
Expand Down Expand Up @@ -75,6 +83,8 @@
url: /LabelGroup
- title: Link
url: /Link
# - title: Overlay
# url: /Overlay
- title: Pagehead
url: /Pagehead
- title: Pagination
Expand Down
47 changes: 47 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,48 @@ declare module '@primer/components' {

export const Link: React.FunctionComponent<LinkProps>

export interface AnchoredPositionHookSettings extends Partial<PositionSettings> {
floatingElementRef?: React.RefObject<Element>
anchorElementRef?: React.RefObject<Element>
}
export type AnchorAlignment = 'start' | 'center' | 'end'
export type AnchorSide =
| 'inside-top'
| 'inside-bottom'
| 'inside-left'
| 'inside-right'
| 'inside-center'
| 'outside-top'
| 'outside-bottom'
| 'outside-left'
| 'outside-right'

export interface PositionSettings {
side: AnchorSide
align: AnchorAlignment
anchorOffset: number
alignmentOffset: number
allowOutOfBounds: boolean
}

export type OverlayProps = {
ignoreClickRefs: React.RefObject<HTMLElement>[]
initialFocusRef?: React.RefObject<HTMLElement>
returnFocusRef: React.RefObject<HTMLElement>
anchorRef: React.RefObject<HTMLElement>
onClickOutside: (e: MouseEvent | TouchEvent) => void
onEscape: (e: KeyboardEvent) => void
positionSettings?: AnchoredPositionHookSettings
positionDeps?: React.DependencyList
width?: 'sm' | 'md' | 'lg' | 'xl' | 'auto'
height?: 'sm' | 'md' | 'auto'
}

/**
* Overlay is in beta and not intended for public use. Use with caution, API may change.
*/
export const Overlay: React.FunctionComponent<OverlayProps>

export interface PageheadProps extends CommonProps, Omit<React.HTMLAttributes<HTMLDivElement>, 'color'> {}

export const Pagehead: React.FunctionComponent<PageheadProps>
Expand Down Expand Up @@ -815,6 +857,11 @@ declare module '@primer/components/lib/Fixed' {
export default Fixed
}

declare module '@primer/components/lib/Overlay' {
import {Overlay} from '@primer/components'
export default Overlay
}

declare module '@primer/components/lib/Pagehead' {
import {Pagehead} from '@primer/components'
export default Pagehead
Expand Down
Loading

0 comments on commit f970ada

Please sign in to comment.