Skip to content

Commit 430f4b8

Browse files
committed
refactor: update popover animation
1 parent ce8b2b6 commit 430f4b8

File tree

11 files changed

+99
-57
lines changed

11 files changed

+99
-57
lines changed
Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Popover
3-
description: Popover is a container component that can overlay other components on page.
3+
description: Popover is an overlay container that displays content relative to a trigger.
44
component: popover
55
---
66

@@ -14,13 +14,19 @@ import { Popover } from '@primereact/ui/popover';
1414

1515
```tsx
1616
<Popover.Root>
17-
<Popover.Trigger></Popover.Trigger>
17+
<Popover.Trigger />
1818
<Popover.Portal>
19-
<Popover.Positioner sideOffset={12} side="bottom" align="start">
19+
<Popover.Positioner>
2020
<Popover.Popup>
21-
<Popover.Content></Popover.Content>
22-
<Popover.Close></Popover.Close>
2321
<Popover.Arrow />
22+
<Popover.Header>
23+
<Popover.Title />
24+
<Popover.Close />
25+
</Popover.Header>
26+
<Popover.Content>
27+
<Popover.Description />
28+
</Popover.Content>
29+
<Popover.Footer />
2430
</Popover.Popup>
2531
</Popover.Positioner>
2632
</Popover.Portal>
@@ -31,30 +37,30 @@ import { Popover } from '@primereact/ui/popover';
3137

3238
### Controlled
3339

34-
Use the `open` and `onOpenChange` props to control the popover state.
40+
Control popover state from outside with the `open` and `onOpenChange` props.
3541

3642
<DocDemoViewer name="popover:controlled-demo" />
3743

3844
### Alignment
3945

40-
Use the `side` and `align` props to align the popover. To give an offset to the popover, use the `sideOffset` and `alignOffset` props.
46+
Use `side` and `align` to control placement. Use `sideOffset` and `alignOffset` to fine-tune spacing.
4147

4248
<DocDemoViewer name="popover:alignment-demo" />
4349

4450
## Accessibility
4551

4652
### Screen Reader
4753

48-
Popover component uses dialog role and since any attribute is passed to the root element, attributes like aria-label or aria-labelledby can be defined to describe the popup contents. In addition aria-modal is added since focus is kept within the popup.
54+
Popover uses the `dialog` role. Since attributes are passed to the root element, you can define `aria-label` or `aria-labelledby` to describe popup content. `aria-modal` is applied because focus is kept within the popup.
4955

50-
Popover adds aria-expanded state attribute and aria-controls to the trigger so that the relation between the trigger and the popup is defined.
56+
Popover adds `aria-expanded` and `aria-controls` to the trigger to define its relation with the popup.
5157

5258
### Popover Keyboard Support
5359

5460
When the popup gets opened, the first focusable element receives the focus and this can be customized by adding autofocus to an element within the popup.
5561

56-
| Key | Function |
57-
| ------------- | ------------------------------------------------------------------- |
58-
| `tab` | Moves focus to the next the focusable element within the popup. |
59-
| `shift + tab` | Moves focus to the previous the focusable element within the popup. |
60-
| `escape` | Closes the popup and moves focus to the trigger. |
62+
| Key | Function |
63+
| ------------- | --------------------------------------------------------------- |
64+
| `tab` | Moves focus to the next focusable element within the popup. |
65+
| `shift + tab` | Moves focus to the previous focusable element within the popup. |
66+
| `escape` | Closes the popup and moves focus to the trigger. |

apps/showcase/registry.json

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

packages/@primereact/headless/src/popover/usePopover.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,31 @@ export const usePopover = withHeadless({
88
name: 'usePopover',
99
defaultProps,
1010
setup: ({ props }) => {
11-
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
12-
const [positionerEl, setPositionerEl] = React.useState<HTMLDivElement | null>(null);
13-
const [arrowEl, setArrowEl] = React.useState<HTMLDivElement | null>(null);
1411
const [openState, setOpenState] = useControlledState({
1512
value: props.open,
1613
defaultValue: props.defaultOpen,
1714
onChange: props.onOpenChange
1815
});
16+
const [rendered, setRendered] = React.useState<boolean>(!!openState);
17+
18+
// elements
19+
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
20+
const [positionerEl, setPositionerEl] = React.useState<HTMLDivElement | null>(null);
21+
const [popupEl, setPopupEl] = React.useState<HTMLElement | null>(null);
22+
const [arrowEl, setArrowEl] = React.useState<HTMLDivElement | null>(null);
1923

2024
const state = {
2125
open: openState,
26+
rendered,
2227
anchorEl,
2328
positionerEl,
29+
popupEl,
2430
arrowEl
2531
};
2632

2733
const anchorRef = React.useRef<HTMLElement | null>(null);
2834
const positionerRef = React.useRef<HTMLDivElement | null>(null);
35+
const popupRef = React.useRef<HTMLElement | null>(null);
2936
const arrowRef = React.useRef<HTMLDivElement | null>(null);
3037

3138
const setAnchorRef = React.useCallback((node: HTMLElement | null) => {
@@ -42,6 +49,13 @@ export const usePopover = withHeadless({
4249
setPositionerEl(node);
4350
}, []);
4451

52+
const setPopupRef = React.useCallback((node: HTMLElement | null) => {
53+
if (node === popupRef.current) return;
54+
55+
popupRef.current = node;
56+
setPopupEl(node);
57+
}, []);
58+
4559
const setArrowRef = React.useCallback((node: HTMLDivElement | null) => {
4660
if (node === arrowRef.current) return;
4761

@@ -71,6 +85,13 @@ export const usePopover = withHeadless({
7185
}
7286
});
7387

88+
// effects
89+
React.useEffect(() => {
90+
if (openState) {
91+
setRendered(true);
92+
}
93+
}, [openState]);
94+
7495
React.useEffect(() => {
7596
if (openState) {
7697
bindOutsideClickListener();
@@ -82,8 +103,10 @@ export const usePopover = withHeadless({
82103
return {
83104
state,
84105
setOpen,
106+
setRendered,
85107
setArrowRef,
86108
setAnchorRef,
109+
setPopupRef,
87110
setPositionerRef
88111
};
89112
}

packages/@primereact/styles/src/popover/Popover.style.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,48 @@ import { createStyles } from '@primereact/styles/utils';
22
import type { PopoverRootInstance } from '@primereact/types/shared/popover';
33

44
const theme = /*css*/ `
5-
.p-popover-popup{
5+
.p-popover-popup {
66
position: relative;
77
background: light-dark(var(--p-surface-0), var(--p-surface-900));
88
border-radius: 0.5rem;
99
border: 1px solid var(--p-content-border-color);
1010
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 0.05);
11+
transform-origin: var(--transform-origin);
12+
will-change: transform, opacity;
1113
}
1214
13-
.p-popover-content{
15+
.p-popover-content {
1416
padding: 0.5rem 1rem 1rem 1rem;
1517
}
1618
17-
.p-popover-header{
19+
.p-popover-header {
1820
display: flex;
1921
justify-content: space-between;
2022
align-items: center;
2123
gap: 0.5rem;
2224
padding: 1rem 1rem 0 1rem;
2325
}
2426
25-
.p-popover-footer{
27+
.p-popover-footer {
2628
display: flex;
2729
justify-content: space-between;
2830
align-items: center;
2931
gap: 0.5rem;
3032
padding: 0 1rem 1rem 1rem;
3133
}
3234
33-
.p-popover-title{
35+
.p-popover-title {
3436
color: light-dark(var(--p-surface-900), var(--p-surface-0));
3537
font-size: 0.875rem;
3638
font-weight: 500;
3739
}
3840
39-
.p-popover-description{
41+
.p-popover-description {
4042
color: light-dark(var(--p-surface-500), var(--p-surface-400));
4143
font-size: 0.875rem;
4244
}
4345
44-
.p-popover-arrow{
46+
.p-popover-arrow {
4547
position: absolute;
4648
border: 1px solid var(--p-content-border-color);
4749
background: light-dark(var(--p-surface-0), var(--p-surface-900));

packages/@primereact/types/src/shared/popover/PopoverPopup.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111
import type { ComponentInstance } from '@primereact/types/core';
1212
import type { BaseComponentProps, PassThroughType } from '..';
13+
import type { useMotionProps } from '@primereact/types/shared/motion';
1314
import type { PopoverRootInstance } from './PopoverRoot.types';
1415

1516
/**
@@ -36,6 +37,10 @@ export interface PopoverPopupProps extends BaseComponentProps<PopoverPopupInstan
3637
* @default true
3738
*/
3839
autoFocus?: boolean;
40+
/**
41+
* Used to apply motion to the popup element. It accepts the same properties as useMotion hook.
42+
*/
43+
motionProps?: useMotionProps | undefined;
3944
}
4045

4146
/**

packages/@primereact/types/src/shared/popover/PopoverPositioner.types.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
*/
1111
import type { ComponentInstance } from '@primereact/types/core';
1212
import type { BaseComponentProps, PassThroughType } from '..';
13-
import type { useMotionProps } from '@primereact/types/shared/motion';
1413
import type { usePositionerProps } from '../positioner';
1514
import type { PopoverRootInstance } from './PopoverRoot.types';
1615

@@ -32,12 +31,7 @@ export interface PopoverPositionerPassThrough {
3231
/**
3332
* Defines valid properties in PopoverPositioner component.
3433
*/
35-
export interface PopoverPositionerProps extends BaseComponentProps<PopoverPositionerInstance, usePositionerProps, PopoverPositionerPassThrough> {
36-
/**
37-
* Used to apply motion to the positioner element. It accepts the same properties as useMotion hook.
38-
*/
39-
motionProps?: useMotionProps | undefined;
40-
}
34+
export interface PopoverPositionerProps extends BaseComponentProps<PopoverPositionerInstance, usePositionerProps, PopoverPositionerPassThrough> {}
4135

4236
/**
4337
* Defines valid state in PopoverPositioner component.

packages/@primereact/types/src/shared/popover/usePopover.types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export interface usePopoverState {
5353
* Whether the popover is open.
5454
*/
5555
open: boolean | undefined;
56+
/**
57+
* Whether the popover positioner is rendered (stays true during exit animation).
58+
*/
59+
rendered: boolean;
5660
/**
5761
* The anchor element.
5862
*/
@@ -61,6 +65,10 @@ export interface usePopoverState {
6165
* The positioner element.
6266
*/
6367
positionerEl: HTMLDivElement | null;
68+
/**
69+
* The popup element.
70+
*/
71+
popupEl: HTMLElement | null;
6472
/**
6573
* The arrow element.
6674
*/
@@ -91,6 +99,14 @@ export interface usePopoverExposes {
9199
* Sets the positioner element.
92100
*/
93101
setPositionerRef?: (node: HTMLDivElement | null) => void;
102+
/**
103+
* Sets the popup element.
104+
*/
105+
setPopupRef?: (node: HTMLElement | null) => void;
106+
/**
107+
* Sets the rendered state of the positioner.
108+
*/
109+
setRendered?: (rendered: boolean) => void;
94110
}
95111

96112
/**
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PopoverPopupProps } from '@primereact/types/shared/popover';
22

33
export const defaultPopupProps: PopoverPopupProps = {
4-
as: 'div'
4+
as: 'div',
5+
motionProps: undefined
56
};

packages/primereact/src/popover/popup/PopoverPopup.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use client';
22
import { Component, withComponent } from '@primereact/core/component';
3+
import { useMotion } from '@primereact/core/motion';
4+
import type { MotionEvent } from '@primeuix/motion';
35
import { mergeProps } from '@primeuix/utils';
46
import * as React from 'react';
57
import { usePopoverContext } from '../Popover.context';
@@ -8,10 +10,21 @@ import { defaultPopupProps } from './PopoverPopup.props';
810
export const PopoverPopup = withComponent({
911
name: 'Popover.Popup',
1012
defaultProps: defaultPopupProps,
11-
setup() {
13+
setup({ props }) {
1214
const popover = usePopoverContext();
1315

14-
return { popover };
16+
const motion = useMotion({
17+
name: 'p-anchored-overlay',
18+
...props.motionProps,
19+
elementRef: popover?.state.popupEl,
20+
visible: !!popover?.state?.open,
21+
onAfterLeave: (event?: MotionEvent) => {
22+
props.motionProps?.onAfterLeave?.(event);
23+
popover?.setRendered?.(false);
24+
}
25+
});
26+
27+
return { popover, motion };
1528
},
1629
render(instance) {
1730
const { props, ptmi, popover } = instance;
@@ -25,6 +38,6 @@ export const PopoverPopup = withComponent({
2538
ptmi('root')
2639
);
2740

28-
return <Component instance={instance} attrs={rootProps} children={props.children} />;
41+
return <Component instance={instance} attrs={rootProps} children={props.children} ref={popover?.setPopupRef} />;
2942
}
3043
});

packages/primereact/src/popover/positioner/PopoverPositioner.props.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ import type { PopoverPositionerProps } from '@primereact/types/shared/popover';
33

44
export const defaultPositionerProps: PopoverPositionerProps = {
55
...HeadlessPositioner.defaultProps,
6-
as: 'div',
7-
motionProps: undefined
6+
as: 'div'
87
};

0 commit comments

Comments
 (0)