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

add Popover docs #1598

Merged
merged 10 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions apps/website/src/pages/docs/popover.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Popover
description: An overlay dialog placed next to a trigger element.
layout: ./_layout.astro
group: utilities
thumbnail: #TODO
---

import PropsTable from '~/components/PropsTable.astro';
import LiveExample from '~/components/LiveExample.astro';
import * as AllExamples from 'examples';

Popover is a utility component for displaying overlay content in a dialog that is placed relative to a trigger element.

<LiveExample src='Popover.main.tsx'>
<AllExamples.PopoverMainExample client:load />
</LiveExample>

By default, Popover does not add any styling. The `applyBackground` prop can be used to add the recommended background, box-shadow, border, etc.

## Usage

The content shown inside the Popover is passed using the `content` prop. The trigger element is specified by the child element that Popover wraps around.

For everything to work correctly, the trigger element must:

- be a button
- forward its ref
- delegate (spread) any arbitrary props

If you use a native `<button>` or iTwinUI's [`<Button>`](button) as the trigger, then all of this should be handled for you. Passing a non-interactive element (like `<div>`) is not advised, as it will break some [accessibility](#accessibility) expectations.

## Positioning

Popover handles positioning using an external library called [Floating UI](floating-ui.com/). To control which side the popover should be placed relative to its trigger, use the `placement` prop. If not enough space is available, then it will flip to the opposite side.

<LiveExample src='Popover.placement.tsx'>
<AllExamples.PopoverPlacementExample client:load />
</LiveExample>

### Portals

It is important to know that before calculating the position, the popover gets [portaled](https://react.dev/reference/react-dom/createPortal) into the nearest `ThemeProvider` to avoid [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) issues. This behavior can be controlled using the Popover's `portal` prop or the ThemeProvider's `portalContainer` prop. Using portals can often lead to issues with keyboard accessibility, so Popover adds some additional logic (described below).

## Accessibility

Semantically speaking, popovers are dialogs that follow the [disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/). The popover is opened by clicking on the trigger element or by pressing <kbd>Enter</kbd> or <kbd>Space</kbd> when the trigger has focus. The trigger element should almost always be a `<button>` underneath (rather than a non-interactive element such as `<div>`).

The popover should generally be labeled using [`aria-labelledby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby). This label can be located inside the popover as a visible heading or hidden text. If `aria-labelledby` or `aria-label` is not passed to the Popover, then the trigger element will be used as the label by default. Additionally, an optional [`aria-describedby`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) can be used for any supplementary text.

When the popover opens, keyboard focus will be moved to the popover. When it closes, keyboard focus will move back to the trigger element. Additionally, keyboard focus will move back to the trigger when tabbing out of it.

If you have a good candidate for receiving focus, then you can manually `focus()` it using a [ref](https://react.dev/learn/manipulating-the-dom-with-refs) or use `autoFocus` where possible. This should be done thoughtfully, and the element receiving focus should usually be located near the beginning of the popover, with not too much content preceding it.

The following example shows how you can focus an input when the popover opens. This input is preceded by a heading associated with the popover using `aria-labelledby`. As a result, when the popover opens, the heading is automatically announced to a screen reader user, ensuring they didn't miss any content located before the input.

<LiveExample src='Popover.focus.tsx'>
<AllExamples.PopoverFocusExample client:load />
</LiveExample>

## Props

<PropsTable path='@itwin/itwinui-react/esm/core/Popover/Popover.d.ts' />
60 changes: 60 additions & 0 deletions examples/Popover.focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import {
Button,
Flex,
IconButton,
LabeledInput,
Popover,
Surface,
Text,
} from '@itwin/itwinui-react';
import { SvgSettings } from '@itwin/itwinui-icons-react';

export default () => {
const headingId = `${React.useId()}-label`;

const [isOpen, setIsOpen] = React.useState(false);

return (
<Popover
applyBackground
aria-labelledby={headingId}
visible={isOpen}
onVisibleChange={setIsOpen}
style={{ maxWidth: '45ch', border: 'none' }}
content={
<Surface elevation={0}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to add border: none override because Surface always insists on bringing its own border.

Also, for some reason Surface.Body only brings horizontal padding not vertical.

@FlyersPh9 Is this intentional?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data-iui-padded="true" does have vertical padding, it's just very little.
Screenshot 2023-09-28 at 4 28 04 PM
The examples on the surface demo page look nice, but the agree button in your screenshot does not look so great. Was the padding changed when we implemented scrollbar-gutter?

Btw, just now found out scrollbar-gutter doesn't work in Safari. 😓

<Surface.Header>
<Text as='h3' id={headingId} variant='leading'>
Settings
</Text>
</Surface.Header>
<Surface.Body isPadded>
<Flex flexDirection='column' alignItems='flex-end'>
{/* this will be focused when popover opens */}
<LabeledInput label='Quality' autoFocus />

<LabeledInput label='Grain' />
<LabeledInput label='Saturation' />

<Button
styleType='high-visibility'
onClick={() => setIsOpen(false)}
>
Apply
</Button>
</Flex>
</Surface.Body>
</Surface>
}
>
<IconButton label='Adjust settings'>
<SvgSettings />
</IconButton>
</Popover>
);
};
18 changes: 18 additions & 0 deletions examples/Popover.main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { Button, Popover } from '@itwin/itwinui-react';

export default () => {
return (
<Popover
content='This is a popover!'
applyBackground
style={{ padding: 'var(--iui-size-xs)' }}
>
<Button>Toggle</Button>
</Popover>
);
};
46 changes: 46 additions & 0 deletions examples/Popover.placement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from 'react';
import { Button, LabeledSelect, Popover } from '@itwin/itwinui-react';

export default () => {
const [placement, setPlacement] =
React.useState<(typeof placements)[number]>('bottom-start');

return (
<Popover
content={
<div style={{ padding: 'var(--iui-size-xs)' }}>
<LabeledSelect
label='Placement'
options={placements.map((p) => ({ value: p, label: p }))}
value={placement}
onChange={setPlacement}
style={{ minWidth: '20ch' }}
/>
</div>
}
applyBackground
placement={placement}
>
<Button>Adjust placement</Button>
</Popover>
);
};

const placements = [
'bottom',
'bottom-start',
'bottom-end',
'top',
'top-start',
'top-end',
'left',
'left-start',
'left-end',
'right',
'right-start',
'right-end',
] as const;
13 changes: 13 additions & 0 deletions examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,19 @@ export { OverlaySubExample };

// ----------------------------------------------------------------------------

import { default as PopoverMainExampleRaw } from './Popover.main';
export const PopoverMainExample = withThemeProvider(PopoverMainExampleRaw);

import { default as PopoverPlacementExampleRaw } from './Popover.placement';
export const PopoverPlacementExample = withThemeProvider(
PopoverPlacementExampleRaw,
);

import { default as PopoverFocusExampleRaw } from './Popover.focus';
export const PopoverFocusExample = withThemeProvider(PopoverFocusExampleRaw);

// ----------------------------------------------------------------------------

import { default as ProgressLinearMainExampleRaw } from './ProgressLinear.main';
const ProgressLinearMainExample = withThemeProvider(
ProgressLinearMainExampleRaw,
Expand Down