Skip to content

Commit e6aba4c

Browse files
committed
refactor: update popover components
1 parent d6a86eb commit e6aba4c

21 files changed

Lines changed: 273 additions & 219 deletions

File tree

packages/@primereact/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from '@primereact/core/api';
44
export * from '@primereact/core/base';
55
export * from '@primereact/core/component';
66
export * from '@primereact/core/config';
7+
export * from '@primereact/core/dnd';
78
export * from '@primereact/core/headless';
89
export * from '@primereact/core/icon';
910
export * from '@primereact/core/locale';

packages/@primereact/core/src/utils/combinedRefs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import * as React from 'react';
22

33
export const combinedRefs = <I = unknown>(innerRef?: React.Ref<I>, forwardRef?: React.Ref<unknown>) => {
4-
if (innerRef && forwardRef) {
4+
if (innerRef && forwardRef && forwardRef !== innerRef) {
55
if (typeof forwardRef === 'function') {
66
forwardRef(innerRef && 'current' in innerRef ? innerRef.current : null);
77
} else {
8-
if ('current' in forwardRef) {
8+
if ('current' in forwardRef && 'current' in innerRef && forwardRef.current !== innerRef.current) {
99
forwardRef.current = innerRef && 'current' in innerRef ? innerRef.current : null;
1010
}
1111
}

packages/@primereact/headless/src/listbox/useListbox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ export const useListbox = withHeadless({
626626

627627
scrollInView(index);
628628

629-
if (props.selectOnFocus && !props.multiple) {
629+
if (props.selectOnFocus && !props.multiple && index !== -1) {
630630
onOptionSelect(event, getOptions()[index]);
631631
}
632632
}
@@ -723,8 +723,8 @@ export const useListbox = withHeadless({
723723

724724
const listProps = {
725725
id: `${id}_list`,
726-
tabIndex: -1,
727726
role: 'listbox' as const,
727+
tabIndex: props.disabled ? -1 : 0,
728728
'aria-activedescendant': focusedState ? getFocusedOptionId() : undefined,
729729
'data-scope': 'listbox',
730730
'data-part': 'list',

packages/@primereact/headless/src/listbox/useListboxOption.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ export const useListboxOption = withHeadless({
4545
'data-scope': 'listbox',
4646
'data-part': 'option',
4747
onClick: (event: React.MouseEvent) => context?.onOptionSelect(event, option, index),
48-
onMouseDown: (event: React.MouseEvent) => context?.changeFocusedOptionIndex(event, index),
48+
onMouseDown: (event: React.MouseEvent) => {
49+
event.preventDefault();
50+
context?.changeFocusedOptionIndex(event, index);
51+
},
4952
onMouseMove: (event: React.MouseEvent) => {
50-
if (context?.props?.focusOnHover && context?.state?.focused) {
53+
if (context?.props?.focusOnHover) {
5154
context?.changeFocusedOptionIndex(event, index);
5255
}
5356
},
@@ -56,11 +59,13 @@ export const useListboxOption = withHeadless({
5659
}, [context, option, index, selected, disabled, focused, group]);
5760

5861
const groupProps = {
62+
role: 'presentation' as const,
5963
'data-scope': 'listbox',
6064
'data-part': 'optionGroup'
6165
};
6266

6367
const optionIndicatorProps = {
68+
'aria-hidden': true as const,
6469
'data-scope': 'listbox',
6570
'data-part': 'optionindicator',
6671
[selected ? 'data-selected' : 'data-unselected']: ''

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

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { withHeadless } from '@primereact/core/headless';
22
import { useFocusTrap } from '@primereact/headless/focustrap';
33
import { useControlledState } from '@primereact/hooks/use-controlled-state';
44
import { useEventListener } from '@primereact/hooks/use-event-listener';
5-
import { focus, getFirstFocusableElement } from '@primeuix/utils/dom';
5+
import { focus, getFirstFocusableElement, toElement } from '@primeuix/utils/dom';
66
import * as React from 'react';
77
import { defaultProps } from './usePopover.props';
88

@@ -18,58 +18,80 @@ export const usePopover = withHeadless({
1818
const [rendered, setRendered] = React.useState<boolean>(!!openState);
1919

2020
// elements
21-
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
22-
const [positionerEl, setPositionerEl] = React.useState<HTMLDivElement | null>(null);
23-
const [popupEl, setPopupEl] = React.useState<HTMLElement | null>(null);
24-
const [arrowEl, setArrowEl] = React.useState<HTMLDivElement | null>(null);
21+
const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(null);
22+
const [positionerElement, setPositionerElement] = React.useState<HTMLDivElement | null>(null);
23+
const [popupElement, setPopupElement] = React.useState<HTMLElement | null>(null);
24+
const [arrowElement, setArrowElement] = React.useState<HTMLDivElement | null>(null);
2525

2626
const state = {
2727
open: openState,
2828
rendered,
2929
trapped: !!props.trapped,
30-
anchorEl,
31-
positionerEl,
32-
popupEl,
33-
arrowEl
30+
anchorElement,
31+
positionerElement,
32+
popupElement,
33+
arrowElement
3434
};
3535

3636
const anchorRef = React.useRef<HTMLElement | null>(null);
37+
const anchorFallbackRef = React.useRef<HTMLElement | null>(null);
3738
const positionerRef = React.useRef<HTMLDivElement | null>(null);
3839
const popupRef = React.useRef<HTMLElement | null>(null);
3940
const arrowRef = React.useRef<HTMLDivElement | null>(null);
4041

4142
const focusTrap = useFocusTrap({
42-
trapped: !!props.trapped && !!popupEl,
43+
trapped: !!props.trapped && !!popupElement,
4344
autoFocus: false
4445
});
4546

4647
const setAnchorRef = React.useCallback((node: HTMLElement | null) => {
47-
if (node === anchorRef.current) return;
48+
const element = toElement(node) ?? null;
4849

49-
anchorRef.current = node;
50-
setAnchorEl(node);
50+
if (!element || element === anchorRef.current) return;
51+
52+
anchorRef.current = element;
53+
setAnchorElement(element || anchorFallbackRef.current);
54+
}, []);
55+
56+
const setAnchorFallbackRef = React.useCallback((node: HTMLElement | null) => {
57+
const element = toElement(node) ?? null;
58+
59+
if (!element || element === anchorFallbackRef.current) return;
60+
61+
anchorFallbackRef.current = element;
62+
63+
if (!anchorRef.current) {
64+
anchorRef.current = element;
65+
setAnchorElement(element);
66+
}
5167
}, []);
5268

5369
const setPositionerRef = React.useCallback((node: HTMLDivElement | null) => {
54-
if (node === positionerRef.current) return;
70+
const element = (toElement(node) ?? null) as HTMLDivElement | null;
71+
72+
if (!element || element === positionerRef.current) return;
5573

56-
positionerRef.current = node;
57-
setPositionerEl(node);
74+
positionerRef.current = element;
75+
setPositionerElement(element);
5876
}, []);
5977

6078
const setPopupRef = React.useCallback((node: HTMLElement | null) => {
61-
if (node === popupRef.current) return;
79+
const element = toElement(node) ?? null;
6280

63-
popupRef.current = node;
64-
focusTrap.containerRef.current = node;
65-
setPopupEl(node);
81+
if (!element || element === popupRef.current) return;
82+
83+
popupRef.current = element;
84+
focusTrap.containerRef.current = element;
85+
setPopupElement(element);
6686
}, []);
6787

6888
const setArrowRef = React.useCallback((node: HTMLDivElement | null) => {
69-
if (node === arrowRef.current) return;
89+
const element = (toElement(node) ?? null) as HTMLDivElement | null;
90+
91+
if (!element || element === arrowRef.current) return;
7092

71-
arrowRef.current = node;
72-
setArrowEl(node);
93+
arrowRef.current = element;
94+
setArrowElement(element);
7395
}, []);
7496

7597
function setOpen(open: boolean, originalEvent?: Event) {
@@ -85,40 +107,42 @@ export const usePopover = withHeadless({
85107
const handleFocusOut = (event: FocusEvent) => {
86108
const relatedTarget = event.relatedTarget as Node | null;
87109

88-
if (!relatedTarget || positionerRef.current?.contains(relatedTarget) || anchorRef.current?.contains(relatedTarget)) return;
110+
if (!relatedTarget || positionerRef.current?.contains(relatedTarget) || anchorRef.current?.contains(relatedTarget) || anchorFallbackRef.current?.contains(relatedTarget)) return;
89111

90112
setOpen(false, event);
91113
};
92114

93115
const onOpenComplete = React.useCallback(() => {
94116
if (props.autoFocus === false) return;
95117

96-
const popupElement = popupRef.current;
118+
const popupNode = popupRef.current;
97119

98-
if (!popupElement) return;
120+
if (!popupNode) return;
99121

100122
const activeElement = document.activeElement as HTMLElement | null;
123+
const anchorNode = anchorRef.current || anchorFallbackRef.current;
101124

102-
if (activeElement && anchorRef.current?.contains(activeElement) && anchorRef.current !== activeElement) {
125+
if (activeElement && anchorNode?.contains(activeElement) && anchorNode !== activeElement) {
103126
return;
104127
}
105128

106-
const firstFocusable = getFirstFocusableElement(popupElement);
129+
const firstFocusable = getFirstFocusableElement(popupNode);
107130

108131
if (firstFocusable) {
109132
focus(firstFocusable as HTMLElement, { preventScroll: true });
110133
} else {
111-
focus(popupElement, { preventScroll: true });
134+
focus(popupNode, { preventScroll: true });
112135
}
113136
}, [props.autoFocus]);
114137

115138
const [bindOutsideClickListener, unbindOutsideClickListener] = useEventListener({
116139
type: 'click',
117140
listener: (event: Event) => {
118-
const positionerElement = positionerRef.current;
119-
const anchorElement = anchorRef.current;
141+
const positionerNode = positionerRef.current;
142+
const anchorNode = anchorRef.current;
143+
const anchorFallbackNode = anchorFallbackRef.current;
120144

121-
if (openState && positionerElement && !positionerElement.contains(event.target as Node) && (!anchorElement || !anchorElement.contains(event.target as Node))) {
145+
if (openState && positionerNode && !positionerNode.contains(event.target as Node) && (!anchorNode || !anchorNode.contains(event.target as Node)) && (!anchorFallbackNode || !anchorFallbackNode.contains(event.target as Node))) {
122146
setOpen(false, event);
123147
}
124148
}
@@ -129,7 +153,7 @@ export const usePopover = withHeadless({
129153
listener: (event: Event) => {
130154
if ((event as KeyboardEvent).key === 'Escape') {
131155
setOpen(false, event);
132-
focus(anchorRef.current as HTMLElement, { preventScroll: true });
156+
focus((anchorRef.current || anchorFallbackRef.current) as HTMLElement, { preventScroll: true });
133157
}
134158
}
135159
});
@@ -155,14 +179,14 @@ export const usePopover = withHeadless({
155179
}, [openState, props.closeOnEscape, bindOutsideClickListener, unbindOutsideClickListener, bindEscapeListener, unbindEscapeListener]);
156180

157181
React.useEffect(() => {
158-
const positionerElement = positionerRef.current;
182+
const positionerNode = positionerRef.current;
159183

160-
if (!openState || !positionerElement || props.trapped) return;
184+
if (!openState || !positionerNode || props.trapped) return;
161185

162-
positionerElement.addEventListener('focusout', handleFocusOut);
186+
positionerNode.addEventListener('focusout', handleFocusOut);
163187

164188
return () => {
165-
positionerElement.removeEventListener('focusout', handleFocusOut);
189+
positionerNode.removeEventListener('focusout', handleFocusOut);
166190
};
167191
}, [openState, props.trapped, handleFocusOut]);
168192

@@ -198,6 +222,7 @@ export const usePopover = withHeadless({
198222
// refs
199223
setArrowRef,
200224
setAnchorRef,
225+
setAnchorFallbackRef,
201226
setPopupRef,
202227
setPositionerRef,
203228
// prop getters

packages/@primereact/headless/src/tooltip/useTooltip.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const useTooltip = withHeadless({
1717
onChange: props.onOpenChange
1818
});
1919

20-
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
21-
const [positionerEl, setPositionerEl] = React.useState<HTMLDivElement | null>(null);
22-
const [arrowEl, setArrowEl] = React.useState<HTMLDivElement | null>(null);
20+
const [anchorElement, setAnchorElement] = React.useState<HTMLElement | null>(null);
21+
const [positionerElement, setPositionerElement] = React.useState<HTMLDivElement | null>(null);
22+
const [arrowElement, setArrowElement] = React.useState<HTMLDivElement | null>(null);
2323
const [instantType, setInstantType] = React.useState<InstantType>(undefined);
2424

2525
const tooltipId = React.useId();
@@ -41,21 +41,21 @@ export const useTooltip = withHeadless({
4141
if (node === anchorRef.current) return;
4242

4343
anchorRef.current = node;
44-
setAnchorEl(node);
44+
setAnchorElement(node);
4545
}, []);
4646

4747
const setPositionerRef = React.useCallback((node: HTMLDivElement | null) => {
4848
if (node === positionerRef.current) return;
4949

5050
positionerRef.current = node;
51-
setPositionerEl(node);
51+
setPositionerElement(node);
5252
}, []);
5353

5454
const setArrowRef = React.useCallback((node: HTMLDivElement | null) => {
5555
if (node === arrowRef.current) return;
5656

5757
arrowRef.current = node;
58-
setArrowEl(node);
58+
setArrowElement(node);
5959
}, []);
6060

6161
const clearTimers = () => {
@@ -283,7 +283,7 @@ export const useTooltip = withHeadless({
283283
trigger.removeEventListener('blur', onBlur);
284284
trigger.removeEventListener('keydown', onKeyDown);
285285
};
286-
}, [anchorEl, disabled]);
286+
}, [anchorElement, disabled]);
287287

288288
React.useEffect(() => {
289289
if (!openState) return;
@@ -321,7 +321,7 @@ export const useTooltip = withHeadless({
321321
positioner.removeEventListener('pointerleave', onPointerLeave);
322322
isOverPositionerRef.current = false;
323323
};
324-
}, [positionerEl, openState, interactive]);
324+
}, [positionerElement, openState, interactive]);
325325

326326
React.useEffect(() => {
327327
if (openState && tooltipManager) {
@@ -335,9 +335,9 @@ export const useTooltip = withHeadless({
335335

336336
const state = {
337337
open: openState,
338-
anchorEl,
339-
positionerEl,
340-
arrowEl,
338+
anchorElement,
339+
positionerElement,
340+
arrowElement,
341341
instantType
342342
};
343343

packages/@primereact/styles/src/chip/Chip.style.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const theme = /*css*/ `
2222
background: transparent;
2323
border: 1px solid light-dark(var(--p-surface-200), var(--p-surface-700));
2424
}
25-
25+
2626
.p-chip-label{
2727
font-size: 0.75rem;
2828
font-weight: 500;
@@ -50,7 +50,7 @@ const theme = /*css*/ `
5050
.p-chip-start {
5151
margin-inline-start: calc(-1 * dt('chip.padding.y') / 2);
5252
}
53-
53+
5454
.p-chip-start:not(:has(img)){
5555
margin-inline-end: calc(-1 * dt('chip.padding.y') / 2);
5656
}
@@ -76,16 +76,19 @@ const theme = /*css*/ `
7676
padding-inline-end: dt('chip.padding.y');
7777
}
7878
79-
.p-chip-remove-icon {
79+
.p-chip-remove {
8080
cursor: pointer;
8181
border-radius: 50%;
8282
transition:
8383
outline-color dt('chip.transition.duration'),
8484
box-shadow dt('chip.transition.duration');
8585
outline-color: transparent;
86+
display: inline-flex;
87+
align-items: center;
88+
justify-content: center;
8689
}
8790
88-
.p-chip-remove-icon:focus-visible {
91+
.p-chip-remove:focus-visible {
8992
box-shadow: dt('chip.remove.icon.focus.ring.shadow');
9093
outline: dt('chip.remove.icon.focus.ring.width') dt('chip.remove.icon.focus.ring.style') dt('chip.remove.icon.focus.ring.color');
9194
outline-offset: dt('chip.remove.icon.focus.ring.offset');
@@ -104,7 +107,7 @@ export const styles = createStyles<ChipRootInstance>({
104107
}
105108
],
106109
label: 'p-chip-label',
107-
remove: 'p-chip-remove-icon',
110+
remove: 'p-chip-remove',
108111
start: 'p-chip-start',
109112
end: 'p-chip-end'
110113
}

0 commit comments

Comments
 (0)