Skip to content

Commit bb79a9c

Browse files
authored
fix: Clear contexts inside popovers (#8321)
1 parent 79505ba commit bb79a9c

File tree

8 files changed

+214
-9
lines changed

8 files changed

+214
-9
lines changed

packages/react-aria-components/src/ComboBox.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
9393
);
9494
});
9595

96+
// Contexts to clear inside the popover.
97+
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, InputContext, GroupContext, TextContext];
98+
9699
interface ComboBoxInnerProps<T extends object> {
97100
props: ComboBoxProps<T>,
98101
collection: Collection<Node<T>>,
@@ -197,7 +200,8 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
197200
placement: 'bottom start',
198201
isNonModal: true,
199202
trigger: 'ComboBox',
200-
style: {'--trigger-width': menuWidth} as React.CSSProperties
203+
style: {'--trigger-width': menuWidth} as React.CSSProperties,
204+
clearContexts: CLEAR_CONTEXTS
201205
}],
202206
[ListBoxContext, {...listBoxProps, ref: listBoxRef}],
203207
[ListStateContext, state],

packages/react-aria-components/src/DatePicker.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export const DateRangePickerContext = createContext<ContextValue<DateRangePicker
7272
export const DatePickerStateContext = createContext<DatePickerState | null>(null);
7373
export const DateRangePickerStateContext = createContext<DateRangePickerState | null>(null);
7474

75+
// Contexts to clear inside the popover.
76+
const CLEAR_CONTEXTS = [GroupContext, ButtonContext, LabelContext, TextContext];
77+
7578
/**
7679
* A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
7780
*/
@@ -148,7 +151,8 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
148151
trigger: 'DatePicker',
149152
triggerRef: groupRef,
150153
placement: 'bottom start',
151-
style: {'--trigger-width': groupWidth} as React.CSSProperties
154+
style: {'--trigger-width': groupWidth} as React.CSSProperties,
155+
clearContexts: CLEAR_CONTEXTS
152156
}],
153157
[DialogContext, dialogProps],
154158
[TextContext, {
@@ -251,7 +255,8 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
251255
trigger: 'DateRangePicker',
252256
triggerRef: groupRef,
253257
placement: 'bottom start',
254-
style: {'--trigger-width': groupWidth} as React.CSSProperties
258+
style: {'--trigger-width': groupWidth} as React.CSSProperties,
259+
clearContexts: CLEAR_CONTEXTS
255260
}],
256261
[DialogContext, dialogProps],
257262
[DateFieldContext, {

packages/react-aria-components/src/Popover.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {focusSafely} from '@react-aria/interactions';
1818
import {OverlayArrowContext} from './OverlayArrow';
1919
import {OverlayTriggerProps, OverlayTriggerState, useOverlayTriggerState} from 'react-stately';
2020
import {OverlayTriggerStateContext} from './Dialog';
21-
import React, {createContext, ForwardedRef, forwardRef, useContext, useEffect, useRef, useState} from 'react';
21+
import React, {Context, createContext, ForwardedRef, forwardRef, useContext, useEffect, useMemo, useRef, useState} from 'react';
2222
import {useIsHidden} from '@react-aria/collections';
2323

2424
export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef' | 'groupRef' | 'offset' | 'arrowSize'>, OverlayTriggerProps, RenderProps<PopoverRenderProps>, SlotProps, AriaLabelingProps {
@@ -80,7 +80,12 @@ export interface PopoverRenderProps {
8080
isExiting: boolean
8181
}
8282

83-
export const PopoverContext = createContext<ContextValue<PopoverProps, HTMLElement>>(null);
83+
interface PopoverContextValue extends PopoverProps {
84+
/** Contexts to clear. */
85+
clearContexts?: Context<any>[]
86+
}
87+
88+
export const PopoverContext = createContext<ContextValue<PopoverContextValue, HTMLElement>>(null);
8489

8590
// Stores a ref for the portal container for a group of popovers (e.g. submenus).
8691
const PopoverGroupContext = createContext<RefObject<Element | null> | null>(null);
@@ -134,10 +139,11 @@ interface PopoverInnerProps extends AriaPopoverProps, RenderProps<PopoverRenderP
134139
isExiting: boolean,
135140
UNSTABLE_portalContainer?: Element,
136141
trigger?: string,
137-
dir?: 'ltr' | 'rtl'
142+
dir?: 'ltr' | 'rtl',
143+
clearContexts?: Context<any>[]
138144
}
139145

140-
function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: PopoverInnerProps) {
146+
function PopoverInner({state, isExiting, UNSTABLE_portalContainer, clearContexts, ...props}: PopoverInnerProps) {
141147
// Calculate the arrow size internally (and remove props.arrowSize from PopoverProps)
142148
// Referenced from: packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx
143149
let arrowRef = useRef<HTMLDivElement>(null);
@@ -190,6 +196,16 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
190196
}
191197
}, [isDialog, ref]);
192198

199+
let children = useMemo(() => {
200+
let children = renderProps.children;
201+
if (clearContexts) {
202+
for (let Context of clearContexts) {
203+
children = <Context.Provider value={null}>{children}</Context.Provider>;
204+
}
205+
}
206+
return children;
207+
}, [renderProps.children, clearContexts]);
208+
193209
let style = {...popoverProps.style, ...renderProps.style};
194210
let overlay = (
195211
<div
@@ -209,7 +225,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
209225
data-exiting={isExiting || undefined}>
210226
{!props.isNonModal && <DismissButton onDismiss={state.close} />}
211227
<OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
212-
{renderProps.children}
228+
{children}
213229
</OverlayArrowContext.Provider>
214230
<DismissButton onDismiss={state.close} />
215231
</div>

packages/react-aria-components/src/Select.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export const Select = /*#__PURE__*/ (forwardRef as forwardRefType)(function Sele
100100
);
101101
});
102102

103+
// Contexts to clear inside the popover.
104+
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, TextContext];
105+
103106
interface SelectInnerProps<T extends object> {
104107
props: SelectProps<T>,
105108
selectRef: ForwardedRef<HTMLDivElement>,
@@ -186,7 +189,8 @@ function SelectInner<T extends object>({props, selectRef: ref, collection}: Sele
186189
scrollRef,
187190
placement: 'bottom start',
188191
style: {'--trigger-width': buttonWidth} as React.CSSProperties,
189-
'aria-labelledby': menuProps['aria-labelledby']
192+
'aria-labelledby': menuProps['aria-labelledby'],
193+
clearContexts: CLEAR_CONTEXTS
190194
}],
191195
[ListBoxContext, {...menuProps, ref: scrollRef}],
192196
[ListStateContext, state],

packages/react-aria-components/test/ComboBox.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,42 @@ describe('ComboBox', () => {
340340

341341
expect(comboboxTester.options()).toHaveLength(7);
342342
});
343+
344+
it('should clear contexts inside popover', async () => {
345+
let tree = render(
346+
<ComboBox>
347+
<Label>Preferred fruit or vegetable</Label>
348+
<Input />
349+
<Button />
350+
<Popover data-testid="popover">
351+
<Label>Hello</Label>
352+
<Button>Yo</Button>
353+
<Input />
354+
<Text>hi</Text>
355+
<ListBox>
356+
<ListBoxItem id="cat">Cat</ListBoxItem>
357+
<ListBoxItem id="dog">Dog</ListBoxItem>
358+
<ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
359+
</ListBox>
360+
</Popover>
361+
</ComboBox>
362+
);
363+
364+
let selectTester = testUtilUser.createTester('Select', {root: tree.container});
365+
366+
await selectTester.open();
367+
368+
let popover = await tree.getByTestId('popover');
369+
let label = popover.querySelector('.react-aria-Label');
370+
expect(label).not.toHaveAttribute('for');
371+
372+
let button = popover.querySelector('.react-aria-Button');
373+
expect(button).not.toHaveAttribute('aria-expanded');
374+
375+
let input = popover.querySelector('.react-aria-Input');
376+
expect(input).not.toHaveAttribute('role');
377+
378+
let text = popover.querySelector('.react-aria-Text');
379+
expect(text).not.toHaveAttribute('id');
380+
});
343381
});

packages/react-aria-components/test/DatePicker.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,53 @@ describe('DatePicker', () => {
274274
let hiddenInput = getByRole('textbox', {hidden: true});
275275
expect(hiddenInput).toHaveAttribute('disabled');
276276
});
277+
278+
it('should clear contexts inside popover', async () => {
279+
let {getByRole, getByTestId} = render(
280+
<DatePicker data-foo="bar">
281+
<Label>Birth date</Label>
282+
<Group>
283+
<DateInput>
284+
{(segment) => <DateSegment segment={segment} />}
285+
</DateInput>
286+
<Button></Button>
287+
</Group>
288+
<Text slot="description">Description</Text>
289+
<Text slot="errorMessage">Error</Text>
290+
<Popover data-testid="popover">
291+
<Dialog>
292+
<Label>Hi</Label>
293+
<Group>Yo</Group>
294+
<Button>Hi</Button>
295+
<Text>test</Text>
296+
<Calendar>
297+
<header>
298+
<Button slot="previous"></Button>
299+
<Heading />
300+
<Button slot="next"></Button>
301+
</header>
302+
<CalendarGrid>
303+
{(date) => <CalendarCell date={date} />}
304+
</CalendarGrid>
305+
</Calendar>
306+
</Dialog>
307+
</Popover>
308+
</DatePicker>
309+
);
310+
311+
await user.click(getByRole('button'));
312+
313+
let popover = await getByTestId('popover');
314+
let label = popover.querySelector('.react-aria-Label');
315+
expect(label).not.toHaveAttribute('id');
316+
317+
let button = popover.querySelector('.react-aria-Button');
318+
expect(button).not.toHaveAttribute('aria-expanded');
319+
320+
let group = popover.querySelector('.react-aria-Group');
321+
expect(group).not.toHaveAttribute('id');
322+
323+
let text = popover.querySelector('.react-aria-Text');
324+
expect(text).not.toHaveAttribute('id');
325+
});
277326
});

packages/react-aria-components/test/DateRangePicker.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,57 @@ describe('DateRangePicker', () => {
314314
expect(spinbutton).toHaveAttribute('aria-disabled', 'true');
315315
}
316316
});
317+
318+
it('should clear contexts inside popover', async () => {
319+
let {getByRole, getByTestId} = render(
320+
<DateRangePicker data-foo="bar">
321+
<Label>Birth date</Label>
322+
<Group>
323+
<DateInput slot="start">
324+
{(segment) => <DateSegment segment={segment} />}
325+
</DateInput>
326+
<span aria-hidden="true"></span>
327+
<DateInput slot="end">
328+
{(segment) => <DateSegment segment={segment} />}
329+
</DateInput>
330+
<Button></Button>
331+
</Group>
332+
<Text slot="description">Description</Text>
333+
<Text slot="errorMessage">Error</Text>
334+
<Popover data-testid="popover">
335+
<Dialog>
336+
<Label>Hi</Label>
337+
<Group>Yo</Group>
338+
<Button>Hi</Button>
339+
<Text>test</Text>
340+
<RangeCalendar>
341+
<header>
342+
<Button slot="previous"></Button>
343+
<Heading />
344+
<Button slot="next"></Button>
345+
</header>
346+
<CalendarGrid>
347+
{(date) => <CalendarCell date={date} />}
348+
</CalendarGrid>
349+
</RangeCalendar>
350+
</Dialog>
351+
</Popover>
352+
</DateRangePicker>
353+
);
354+
355+
await user.click(getByRole('button'));
356+
357+
let popover = await getByTestId('popover');
358+
let label = popover.querySelector('.react-aria-Label');
359+
expect(label).not.toHaveAttribute('id');
360+
361+
let button = popover.querySelector('.react-aria-Button');
362+
expect(button).not.toHaveAttribute('aria-expanded');
363+
364+
let group = popover.querySelector('.react-aria-Group');
365+
expect(group).not.toHaveAttribute('id');
366+
367+
let text = popover.querySelector('.react-aria-Text');
368+
expect(text).not.toHaveAttribute('id');
369+
});
317370
});

packages/react-aria-components/test/Select.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,40 @@ describe('Select', () => {
377377
let trigger = selectTester.trigger;
378378
expect(document.activeElement).toBe(trigger);
379379
});
380+
381+
it('should clear contexts inside popover', async () => {
382+
let {getByTestId} = render(
383+
<Select data-testid="select" defaultSelectedKey="cat">
384+
<Label>Favorite Animal</Label>
385+
<Button>
386+
<SelectValue />
387+
</Button>
388+
<Popover data-testid="popover">
389+
<Label>Hello</Label>
390+
<Button>Yo</Button>
391+
<Text>hi</Text>
392+
<ListBox>
393+
<ListBoxItem id="cat">Cat</ListBoxItem>
394+
<ListBoxItem id="dog">Dog</ListBoxItem>
395+
<ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
396+
</ListBox>
397+
</Popover>
398+
</Select>
399+
);
400+
401+
let wrapper = getByTestId('select');
402+
let selectTester = testUtilUser.createTester('Select', {root: wrapper});
403+
404+
await selectTester.open();
405+
406+
let popover = await getByTestId('popover');
407+
let label = popover.querySelector('.react-aria-Label');
408+
expect(label).not.toHaveAttribute('for');
409+
410+
let button = popover.querySelector('.react-aria-Button');
411+
expect(button).not.toHaveAttribute('aria-expanded');
412+
413+
let text = popover.querySelector('.react-aria-Text');
414+
expect(text).not.toHaveAttribute('id');
415+
});
380416
});

0 commit comments

Comments
 (0)