Skip to content

Commit 8db1447

Browse files
committed
Write docs for PopoverNext and add examples
1 parent c3bce1c commit 8db1447

File tree

12 files changed

+1113
-2
lines changed

12 files changed

+1113
-2
lines changed

packages/core/src/components/components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,6 @@
6464
@page dialog
6565
@page drawer
6666
@page popover
67+
@page popover-next
6768
@page toast
6869
@page tooltip

packages/core/src/components/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ export type { Panel, PanelProps } from "./panel-stack/panelTypes";
8080
export { Popover } from "./popover/popover";
8181
export { PopoverInteractionKind, type PopoverProps } from "./popover/popoverProps";
8282
export { PopoverPosition } from "./popover/popoverPosition";
83-
export type { PopoverNextProps, FloatingPlacement } from "./popover-next/popoverNextProps";
83+
export type {
84+
FloatingBoundary,
85+
FloatingPlacement,
86+
MiddlewareConfig,
87+
PopoverNextProps,
88+
} from "./popover-next/popoverNextProps";
8489
export { PopoverNext, type PopoverNextRef } from "./popover-next/popoverNext";
8590
export type {
8691
DefaultPopoverTargetHTMLProps,

packages/core/src/components/popover-next/popover-next.md

Lines changed: 369 additions & 0 deletions
Large diffs are not rendered by default.

packages/core/src/components/popover/popover.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
1+
---
2+
tag: deprecated
3+
---
4+
15
@# Popover
26

7+
<div class="@ns-callout @ns-intent-danger @ns-icon-error @ns-callout-has-body-content">
8+
<h5 class="@ns-heading">
9+
10+
Deprecated: use [**PopoverNext**](#core/components/popover-next)
11+
12+
</h5>
13+
14+
This component is **deprecated** in favor of the new **PopoverNext** component which is built on the modern Floating UI library instead of Popper.js. You should migrate to the new API which will become the standard in a future major version of Blueprint.
15+
16+
</div>
17+
318
Popovers display floating content next to a target element.
419

520
The **Popover** component is built on top of the [**Popper.js**](https://popper.js.org) library.

packages/docs-app/src/examples/core-examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ export * from "./popoverMinimalExample";
7272
export * from "./popoverPortalExample";
7373
export * from "./popoverPlacementExample";
7474
export * from "./popoverSizingExample";
75+
export * from "./popoverNextExample";
76+
export * from "./popoverNextInteractionKindExample";
77+
export * from "./popoverNextMinimalExample";
78+
export * from "./popoverNextPortalExample";
79+
export * from "./popoverNextPlacementExample";
80+
export * from "./popoverNextSizingExample";
7581
export * from "./progressExample";
7682
export * from "./rangeSliderExample";
7783
export * from "./radioExample";
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/*
2+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { useCallback, useRef, useState } from "react";
18+
19+
import {
20+
AnchorButton,
21+
Button,
22+
Classes,
23+
Code,
24+
Divider,
25+
type FloatingPlacement,
26+
FormGroup,
27+
H5,
28+
HTMLSelect,
29+
Intent,
30+
Menu,
31+
MenuDivider,
32+
MenuItem,
33+
PopoverInteractionKind,
34+
PopoverNext,
35+
RadioGroup,
36+
RangeSlider,
37+
Slider,
38+
Switch,
39+
} from "@blueprintjs/core";
40+
import {
41+
Example,
42+
type ExampleProps,
43+
handleBooleanChange,
44+
handleNumberChange,
45+
handleValueChange,
46+
} from "@blueprintjs/docs-theme";
47+
import { FilmSelect } from "@blueprintjs/select/examples";
48+
49+
const FLOATING_UI_DOCS_URL = "https://floating-ui.com/docs/tutorial";
50+
51+
const INTERACTION_KINDS = [
52+
{ label: "Click", value: "click" },
53+
{ label: "Click (target only)", value: "click-target" },
54+
{ label: "Hover", value: "hover" },
55+
{ label: "Hover (target only)", value: "hover-target" },
56+
];
57+
58+
const PLACEMENTS: FloatingPlacement[] = [
59+
"top",
60+
"top-start",
61+
"top-end",
62+
"right",
63+
"right-start",
64+
"right-end",
65+
"bottom",
66+
"bottom-start",
67+
"bottom-end",
68+
"left",
69+
"left-start",
70+
"left-end",
71+
];
72+
73+
export const PopoverNextExample: React.FC<ExampleProps> = props => {
74+
const [buttonText, setButtonText] = useState("Popover target");
75+
const [canEscapeKeyClose, setCanEscapeKeyClose] = useState(true);
76+
const [exampleIndex, setExampleIndex] = useState(0);
77+
const [hasBackdrop, setHasBackdrop] = useState(false);
78+
const [inheritDarkTheme, setInheritDarkTheme] = useState(true);
79+
const [interactionKind, setInteractionKind] = useState<PopoverInteractionKind>(
80+
PopoverInteractionKind.CLICK,
81+
);
82+
const [isControlled, setIsControlled] = useState(false);
83+
const [isOpen, setIsOpen] = useState(false);
84+
const [matchTargetWidth, setMatchTargetWidth] = useState(false);
85+
const [minimal, setMinimal] = useState(false);
86+
87+
const [openOnTargetFocus, setOpenOnTargetFocus] = useState(true);
88+
const [placement, setPlacement] = useState<FloatingPlacement | undefined>(undefined);
89+
const [rangeSliderValue, setRangeSliderValue] = useState<[number, number]>([0, 10]);
90+
const [shouldReturnFocusOnClose, setShouldReturnFocusOnClose] = useState(false);
91+
const [sliderValue, setSliderValue] = useState(5);
92+
const [usePortal, setUsePortal] = useState(true);
93+
94+
const scrollParentElement = useRef<HTMLElement | null>(null);
95+
96+
const isHoverInteractionKind =
97+
interactionKind === "hover" || interactionKind === "hover-target";
98+
99+
const handleInteractionChange = handleValueChange(
100+
(newInteractionKind: PopoverInteractionKind) => {
101+
setInteractionKind(newInteractionKind);
102+
setHasBackdrop(hasBackdrop && newInteractionKind === "click");
103+
},
104+
);
105+
106+
const toggleMatchTargetWidth = handleBooleanChange(newMatchTargetWidth => {
107+
setButtonText(
108+
newMatchTargetWidth ? "(Slightly wider) popover target" : "PopoverNext target",
109+
);
110+
setMatchTargetWidth(newMatchTargetWidth);
111+
});
112+
113+
const toggleShouldReturnFocusOnClose = handleBooleanChange(newShouldReturnFocusOnClose => {
114+
setOpenOnTargetFocus(newShouldReturnFocusOnClose ? false : true);
115+
setShouldReturnFocusOnClose(newShouldReturnFocusOnClose);
116+
});
117+
118+
const toggleUsePortal = handleBooleanChange(newUsePortal => {
119+
if (!newUsePortal) {
120+
setHasBackdrop(false);
121+
setInheritDarkTheme(true);
122+
}
123+
setUsePortal(newUsePortal);
124+
});
125+
126+
const options = (
127+
<>
128+
<H5>Appearance</H5>
129+
<FormGroup
130+
helperText="May be overridden to prevent overflow"
131+
label="Position when opened"
132+
labelFor="position"
133+
>
134+
<HTMLSelect onChange={handleValueChange(setPlacement)} value={placement ?? "auto"}>
135+
<option value="auto">auto (use autoPlacement)</option>
136+
{PLACEMENTS.map(p => (
137+
<option key={p} value={p}>
138+
{p}
139+
</option>
140+
))}
141+
</HTMLSelect>
142+
</FormGroup>
143+
<FormGroup label="Example content">
144+
<HTMLSelect onChange={handleNumberChange(setExampleIndex)} value={exampleIndex}>
145+
<option value="0">Text</option>
146+
<option value="1">Input</option>
147+
<option value="2">Sliders</option>
148+
<option value="3">Menu</option>
149+
<option value="4">Select</option>
150+
<option value="5">Empty</option>
151+
</HTMLSelect>
152+
</FormGroup>
153+
<Switch checked={usePortal} onChange={toggleUsePortal}>
154+
Use <Code>Portal</Code>
155+
</Switch>
156+
<Switch
157+
checked={minimal}
158+
label="Minimal appearance"
159+
onChange={handleBooleanChange(setMinimal)}
160+
/>
161+
162+
<H5>Control</H5>
163+
<Switch
164+
checked={isControlled}
165+
label="Is controlled"
166+
onChange={handleBooleanChange(setIsControlled)}
167+
/>
168+
<Switch
169+
checked={isOpen}
170+
disabled={!isControlled}
171+
label="Open"
172+
onChange={handleBooleanChange(setIsOpen)}
173+
/>
174+
175+
<H5>Interactions</H5>
176+
<RadioGroup
177+
label="Interaction kind"
178+
onChange={handleInteractionChange}
179+
options={INTERACTION_KINDS}
180+
selectedValue={interactionKind.toString()}
181+
/>
182+
<Divider />
183+
<Switch
184+
checked={canEscapeKeyClose}
185+
label="Can escape key close"
186+
onChange={handleBooleanChange(setCanEscapeKeyClose)}
187+
/>
188+
<Switch
189+
checked={openOnTargetFocus}
190+
disabled={!isHoverInteractionKind}
191+
label="Open on target focus"
192+
onChange={handleBooleanChange(setOpenOnTargetFocus)}
193+
/>
194+
<Switch
195+
checked={isHoverInteractionKind ? false : shouldReturnFocusOnClose}
196+
disabled={isHoverInteractionKind}
197+
label="Should return focus on close"
198+
onChange={toggleShouldReturnFocusOnClose}
199+
/>
200+
201+
<Switch
202+
checked={matchTargetWidth}
203+
label="Match target width"
204+
onChange={toggleMatchTargetWidth}
205+
/>
206+
207+
<FormGroup>
208+
<AnchorButton
209+
endIcon="share"
210+
fill={true}
211+
href={FLOATING_UI_DOCS_URL}
212+
intent={Intent.PRIMARY}
213+
style={{ marginTop: 20 }}
214+
target="_blank"
215+
variant="minimal"
216+
>
217+
Visit Floating UI docs
218+
</AnchorButton>
219+
</FormGroup>
220+
</>
221+
);
222+
223+
const getContents = useCallback(
224+
(index: number): React.JSX.Element => {
225+
return [
226+
<div key="text">
227+
<H5>Confirm deletion</H5>
228+
<p>
229+
Are you sure you want to delete these items? You won't be able to recover
230+
them.
231+
</p>
232+
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}>
233+
<Button className={Classes.POPOVER_DISMISS} style={{ marginRight: 10 }}>
234+
Cancel
235+
</Button>
236+
<Button className={Classes.POPOVER_DISMISS} intent={Intent.DANGER}>
237+
Delete
238+
</Button>
239+
</div>
240+
</div>,
241+
<div key="input">
242+
<label className={Classes.LABEL}>
243+
Enter some text
244+
<input autoFocus={true} className={Classes.INPUT} type="text" />
245+
</label>
246+
</div>,
247+
<div key="sliders">
248+
<Slider max={10} min={0} onChange={setSliderValue} value={sliderValue} />
249+
<RangeSlider
250+
max={10}
251+
min={0}
252+
onChange={setRangeSliderValue}
253+
value={rangeSliderValue}
254+
/>
255+
</div>,
256+
<Menu key="menu">
257+
<MenuDivider title="Edit" />
258+
<MenuItem icon="cut" label="⌘X" text="Cut" />
259+
<MenuItem icon="duplicate" label="⌘C" text="Copy" />
260+
<MenuItem disabled={true} icon="clipboard" label="⌘V" text="Paste" />
261+
<MenuDivider title="Text" />
262+
<MenuItem icon="align-left" text="Alignment">
263+
<MenuItem icon="align-left" text="Left" />
264+
<MenuItem icon="align-center" text="Center" />
265+
<MenuItem icon="align-right" text="Right" />
266+
<MenuItem icon="align-justify" text="Justify" />
267+
</MenuItem>
268+
<MenuItem icon="style" text="Style">
269+
<MenuItem icon="bold" text="Bold" />
270+
<MenuItem icon="italic" text="Italic" />
271+
<MenuItem icon="underline" text="Underline" />
272+
</MenuItem>
273+
</Menu>,
274+
<div key="filmselect" style={{ padding: 20 }}>
275+
<FilmSelect popoverProps={{ captureDismiss: true }} />
276+
</div>,
277+
][index];
278+
},
279+
[rangeSliderValue, sliderValue],
280+
);
281+
282+
const centerScroll = useCallback((overflowingDiv: HTMLDivElement) => {
283+
scrollParentElement.current = overflowingDiv?.parentElement;
284+
285+
if (overflowingDiv != null) {
286+
// if we don't requestAnimationFrame, this function apparently executes
287+
// before styles are applied to the page, so the centering is way off.
288+
requestAnimationFrame(() => {
289+
const container = overflowingDiv.parentElement;
290+
if (container) {
291+
container.scrollLeft =
292+
overflowingDiv.clientWidth / 2 - container.clientWidth / 2;
293+
container.scrollTop =
294+
overflowingDiv.clientHeight / 2 - container.clientHeight / 2;
295+
}
296+
});
297+
}
298+
}, []);
299+
300+
return (
301+
<Example options={options} {...props}>
302+
<div className="docs-popover-example-scroll" ref={centerScroll}>
303+
<PopoverNext
304+
canEscapeKeyClose={canEscapeKeyClose}
305+
content={getContents(exampleIndex)}
306+
hasBackdrop={hasBackdrop}
307+
inheritDarkTheme={inheritDarkTheme}
308+
interactionKind={interactionKind}
309+
isOpen={isControlled ? isOpen : undefined}
310+
matchTargetWidth={matchTargetWidth}
311+
minimal={minimal}
312+
openOnTargetFocus={openOnTargetFocus}
313+
placement={placement}
314+
popoverClassName={exampleIndex <= 2 ? Classes.POPOVER_CONTENT_SIZING : ""}
315+
portalClassName="docs-popover-example-portal"
316+
shouldReturnFocusOnClose={shouldReturnFocusOnClose}
317+
usePortal={usePortal}
318+
>
319+
<Button intent={Intent.PRIMARY} tabIndex={0} text={buttonText} />
320+
</PopoverNext>
321+
<p>Scroll around this container</p>
322+
</div>
323+
</Example>
324+
);
325+
};

0 commit comments

Comments
 (0)