Skip to content

Commit 2135364

Browse files
committed
docs(react-headless-components-preview): add Popover Storybook stories
1 parent 9e7bc39 commit 2135364

17 files changed

Lines changed: 526 additions & 1 deletion

packages/react-components/react-headless-components-preview/stories/.storybook/preview-head.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,22 @@
44
interpolate-size: allow-keywords;
55
}
66
</style>
7+
<style>
8+
/* Entrance / exit transition for native-popover surfaces used by stories */
9+
[popover] {
10+
opacity: 1;
11+
transform: scale(1);
12+
transition: opacity 150ms ease-out, transform 150ms ease-out, display 150ms allow-discrete,
13+
overlay 150ms allow-discrete;
14+
}
15+
[popover]:not(:popover-open) {
16+
opacity: 0;
17+
transform: scale(0.96);
18+
}
19+
@starting-style {
20+
[popover]:popover-open {
21+
opacity: 0;
22+
transform: scale(0.96);
23+
}
24+
}
25+
</style>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
outer: 'p-16 min-h-[320px]',
6+
container: 'flex items-start gap-10',
7+
column: 'flex flex-col items-start gap-2',
8+
label: 'text-xs font-semibold text-gray-500 uppercase tracking-wide',
9+
trigger:
10+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
11+
target:
12+
'px-4 py-2 rounded-md bg-purple-600 text-white font-medium hover:bg-purple-700 focus-visible:outline-2 focus-visible:outline-purple-500 focus-visible:outline-offset-2 cursor-pointer border-none',
13+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs flex flex-col gap-2',
14+
};
15+
16+
export const AnchorToCustomTarget = (): React.ReactNode => {
17+
const [target, setTarget] = React.useState<HTMLElement | null>(null);
18+
19+
return (
20+
<div className={classes.outer}>
21+
<div className={classes.container}>
22+
<div className={classes.column}>
23+
<span className={classes.label}>Custom anchor (target)</span>
24+
<button ref={setTarget} className={classes.target}>
25+
Anchor
26+
</button>
27+
</div>
28+
29+
<div className={classes.column}>
30+
<span className={classes.label}>Popover trigger (unrelated)</span>
31+
<Popover positioning={{ target, position: 'below', offset: 4 }}>
32+
<PopoverTrigger>
33+
<button className={classes.trigger}>Open popover</button>
34+
</PopoverTrigger>
35+
<PopoverSurface className={classes.surface}>
36+
<h3 className="text-sm font-semibold text-gray-900 m-0">Popover content</h3>
37+
<p className="text-sm text-gray-600">
38+
Clicking <em>Open popover</em> toggles this surface, but <code>positioning.target</code> makes it anchor
39+
to the purple <em>Anchor</em> button instead of the trigger. It appears to the right of the anchor
40+
regardless of where the trigger sits.
41+
</p>
42+
</PopoverSurface>
43+
</Popover>
44+
</div>
45+
</div>
46+
</div>
47+
);
48+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Best practices
2+
3+
### Do
4+
5+
- Use the `trapFocus` prop when focusable elements are in the `Popover`.
6+
- Create nested `Popover`s as separate components.
7+
- If there are no interactive items in the `Popover` content, set `tabIndex={-1}` on the `PopoverSurface`.
8+
- Use `Popover` to reduce screen clutter and host non-essential information.
9+
10+
### Don't
11+
12+
- Don't use more than 2 levels of nested `Popover`s.
13+
- Don't use `Popover`s to display too much content; consider if that content belongs on the main page.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
trigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs',
8+
checkbox: 'flex items-center gap-2 mb-4 text-sm text-gray-700',
9+
};
10+
11+
export const Controlled = (): React.ReactNode => {
12+
const [open, setOpen] = React.useState(false);
13+
14+
return (
15+
<div className="flex flex-col gap-4">
16+
<label className={classes.checkbox}>
17+
<input type="checkbox" checked={open} onChange={e => setOpen(e.target.checked)} />
18+
Popover open
19+
</label>
20+
<Popover open={open} onOpenChange={(_e, data) => setOpen(data.open)}>
21+
<PopoverTrigger>
22+
<button className={classes.trigger}>Controlled popover</button>
23+
</PopoverTrigger>
24+
<PopoverSurface className={classes.surface}>
25+
<p className="text-sm text-gray-600">
26+
This popover is controlled externally. Toggle the checkbox above or click the trigger to open and close it.
27+
</p>
28+
</PopoverSurface>
29+
</Popover>
30+
</div>
31+
);
32+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
trigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs',
8+
};
9+
10+
type CustomTriggerProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
11+
12+
const CustomTriggerButton = React.forwardRef<HTMLButtonElement, CustomTriggerProps>((props, ref) => (
13+
<button ref={ref} {...props} className={classes.trigger}>
14+
Custom trigger
15+
</button>
16+
));
17+
18+
export const CustomTrigger = (): React.ReactNode => (
19+
<Popover>
20+
<PopoverTrigger>
21+
<CustomTriggerButton />
22+
</PopoverTrigger>
23+
<PopoverSurface className={classes.surface}>
24+
<h3 className="text-sm font-semibold text-gray-900 mb-1">Custom trigger</h3>
25+
<p className="text-sm text-gray-600">
26+
Native elements and Fluent components have first-class support as children of <code>PopoverTrigger</code>. To
27+
use your own component, forward its ref with <code>React.forwardRef</code> so the popover can wire up the
28+
trigger ref and aria attributes.
29+
</p>
30+
</PopoverSurface>
31+
</Popover>
32+
);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
trigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs',
8+
};
9+
10+
export const Default = (): React.ReactNode => (
11+
<Popover>
12+
<PopoverTrigger>
13+
<button className={classes.trigger}>Show popover</button>
14+
</PopoverTrigger>
15+
<PopoverSurface className={classes.surface}>
16+
<h3 className="text-sm font-semibold text-gray-900 mb-1">Popover title</h3>
17+
<p className="text-sm text-gray-600">
18+
This is the content of the popover. Click the trigger again or press Escape to close.
19+
</p>
20+
</PopoverSurface>
21+
</Popover>
22+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A popover displays lightweight content anchored to a trigger element.
2+
3+
Popovers are used for transient UI that appears on user interaction, such as additional information, forms, or menus. They support click, hover, and context-menu triggers, optional focus trapping for dialog-like behavior, and can render with an arrow pointing to the trigger.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
trigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs',
8+
};
9+
10+
export const Inline = (): React.ReactNode => (
11+
<div className="p-16">
12+
<Popover inline positioning="below">
13+
<PopoverTrigger>
14+
<button className={classes.trigger}>Inline popover</button>
15+
</PopoverTrigger>
16+
<PopoverSurface className={classes.surface}>
17+
<h3 className="text-sm font-semibold text-gray-900 mb-1">Inline rendering</h3>
18+
<p className="text-sm text-gray-600">
19+
This popover renders without the native HTML <code>popover</code> top-layer. It is still positioned via CSS
20+
Anchor Positioning, but stacks with regular z-index rather than being auto-elevated above siblings.
21+
</p>
22+
</PopoverSurface>
23+
</Popover>
24+
</div>
25+
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
trigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 w-80',
8+
action:
9+
'px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 cursor-pointer border-none',
10+
link: 'text-blue-600 hover:text-blue-700 underline',
11+
};
12+
13+
export const InternalUpdateContent = (): React.ReactNode => {
14+
const [revealed, setRevealed] = React.useState(false);
15+
const linkRef = React.useRef<HTMLAnchorElement>(null);
16+
17+
React.useEffect(() => {
18+
if (revealed) {
19+
linkRef.current?.focus();
20+
}
21+
}, [revealed]);
22+
23+
return (
24+
<Popover onOpenChange={(_, data) => !data.open && setRevealed(false)}>
25+
<PopoverTrigger>
26+
<button className={classes.trigger}>Popover trigger</button>
27+
</PopoverTrigger>
28+
<PopoverSurface className={classes.surface}>
29+
<h3 className="text-sm font-semibold text-gray-900 mb-2">First panel</h3>
30+
<p className="text-sm text-gray-600 mb-3">
31+
Popover content can change while the popover is open. When new focusable content is revealed, move focus to it
32+
so keyboard users can continue interacting.
33+
</p>
34+
35+
{revealed ? (
36+
<div className="text-sm text-gray-700">
37+
Revealed content with{' '}
38+
<a ref={linkRef} href="#" className={classes.link}>
39+
a focusable link
40+
</a>
41+
.
42+
</div>
43+
) : (
44+
<button className={classes.action} onClick={() => setRevealed(true)}>
45+
Reveal more
46+
</button>
47+
)}
48+
</PopoverSurface>
49+
</Popover>
50+
);
51+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as React from 'react';
2+
import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview';
3+
4+
const classes = {
5+
rootTrigger:
6+
'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none',
7+
nestedTrigger:
8+
'px-3 py-1.5 rounded-md bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 data-[open]:bg-indigo-700 focus-visible:outline-2 focus-visible:outline-indigo-500 focus-visible:outline-offset-2 cursor-pointer border-none',
9+
deepTrigger:
10+
'px-3 py-1.5 rounded-md bg-purple-600 text-white text-sm font-medium hover:bg-purple-700 data-[open]:bg-purple-700 focus-visible:outline-2 focus-visible:outline-purple-500 focus-visible:outline-offset-2 cursor-pointer border-none',
11+
actionButton:
12+
'px-3 py-1.5 rounded-md bg-gray-200 text-gray-900 text-sm font-medium hover:bg-gray-300 cursor-pointer border-none',
13+
surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[240px] max-w-xs flex flex-col gap-3',
14+
heading: 'text-sm font-semibold text-gray-900 m-0',
15+
body: 'text-sm text-gray-600',
16+
row: 'flex flex-wrap items-center gap-2',
17+
};
18+
19+
const SecondNestedPopover = (): React.ReactNode => (
20+
<Popover trapFocus>
21+
<PopoverTrigger>
22+
<button className={classes.deepTrigger}>Second nested trigger</button>
23+
</PopoverTrigger>
24+
<PopoverSurface className={classes.surface}>
25+
<h3 className={classes.heading}>Popover content</h3>
26+
<div className={classes.body}>This is some popover content.</div>
27+
<button className={classes.actionButton}>Second nested button</button>
28+
</PopoverSurface>
29+
</Popover>
30+
);
31+
32+
const FirstNestedPopover = (): React.ReactNode => (
33+
<Popover trapFocus>
34+
<PopoverTrigger>
35+
<button className={classes.nestedTrigger}>First nested trigger</button>
36+
</PopoverTrigger>
37+
<PopoverSurface className={classes.surface}>
38+
<h3 className={classes.heading}>Popover content</h3>
39+
<div className={classes.body}>This is some popover content.</div>
40+
<button className={classes.actionButton}>First nested button</button>
41+
<div className={classes.row}>
42+
<SecondNestedPopover />
43+
<SecondNestedPopover />
44+
</div>
45+
</PopoverSurface>
46+
</Popover>
47+
);
48+
49+
export const Nested = (): React.ReactNode => (
50+
<Popover trapFocus>
51+
<PopoverTrigger>
52+
<button className={classes.rootTrigger}>Root trigger</button>
53+
</PopoverTrigger>
54+
<PopoverSurface className={classes.surface}>
55+
<h3 className={classes.heading}>Popover content</h3>
56+
<div className={classes.body}>This is some popover content.</div>
57+
<div className={classes.row}>
58+
<button className={classes.actionButton}>Root button</button>
59+
<FirstNestedPopover />
60+
</div>
61+
</PopoverSurface>
62+
</Popover>
63+
);

0 commit comments

Comments
 (0)