Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/react-aria-components/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
);
});

// Contexts to clear inside the popover.
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, InputContext, GroupContext, TextContext];

interface ComboBoxInnerProps<T extends object> {
props: ComboBoxProps<T>,
collection: Collection<Node<T>>,
Expand Down Expand Up @@ -197,7 +200,8 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
placement: 'bottom start',
isNonModal: true,
trigger: 'ComboBox',
style: {'--trigger-width': menuWidth} as React.CSSProperties
style: {'--trigger-width': menuWidth} as React.CSSProperties,
clearContexts: CLEAR_CONTEXTS
}],
[ListBoxContext, {...listBoxProps, ref: listBoxRef}],
[ListStateContext, state],
Expand Down
9 changes: 7 additions & 2 deletions packages/react-aria-components/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const DateRangePickerContext = createContext<ContextValue<DateRangePicker
export const DatePickerStateContext = createContext<DatePickerState | null>(null);
export const DateRangePickerStateContext = createContext<DateRangePickerState | null>(null);

// Contexts to clear inside the popover.
const CLEAR_CONTEXTS = [GroupContext, ButtonContext, LabelContext, TextContext];

/**
* A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value.
*/
Expand Down Expand Up @@ -148,7 +151,8 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
trigger: 'DatePicker',
triggerRef: groupRef,
placement: 'bottom start',
style: {'--trigger-width': groupWidth} as React.CSSProperties
style: {'--trigger-width': groupWidth} as React.CSSProperties,
clearContexts: CLEAR_CONTEXTS
}],
[DialogContext, dialogProps],
[TextContext, {
Expand Down Expand Up @@ -251,7 +255,8 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
trigger: 'DateRangePicker',
triggerRef: groupRef,
placement: 'bottom start',
style: {'--trigger-width': groupWidth} as React.CSSProperties
style: {'--trigger-width': groupWidth} as React.CSSProperties,
clearContexts: CLEAR_CONTEXTS
}],
[DialogContext, dialogProps],
[DateFieldContext, {
Expand Down
26 changes: 21 additions & 5 deletions packages/react-aria-components/src/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {focusSafely} from '@react-aria/interactions';
import {OverlayArrowContext} from './OverlayArrow';
import {OverlayTriggerProps, OverlayTriggerState, useOverlayTriggerState} from 'react-stately';
import {OverlayTriggerStateContext} from './Dialog';
import React, {createContext, ForwardedRef, forwardRef, useContext, useEffect, useRef, useState} from 'react';
import React, {Context, createContext, ForwardedRef, forwardRef, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {useIsHidden} from '@react-aria/collections';

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

export const PopoverContext = createContext<ContextValue<PopoverProps, HTMLElement>>(null);
interface PopoverContextValue extends PopoverProps {
/** Contexts to clear. */
clearContexts?: Context<any>[]
}

export const PopoverContext = createContext<ContextValue<PopoverContextValue, HTMLElement>>(null);

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

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

let children = useMemo(() => {
let children = renderProps.children;
if (clearContexts) {
for (let Context of clearContexts) {
children = <Context.Provider value={null}>{children}</Context.Provider>;
}
}
return children;
}, [renderProps.children, clearContexts]);

let style = {...popoverProps.style, ...renderProps.style};
let overlay = (
<div
Expand All @@ -209,7 +225,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
data-exiting={isExiting || undefined}>
{!props.isNonModal && <DismissButton onDismiss={state.close} />}
<OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
{renderProps.children}
{children}
</OverlayArrowContext.Provider>
<DismissButton onDismiss={state.close} />
</div>
Expand Down
6 changes: 5 additions & 1 deletion packages/react-aria-components/src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export const Select = /*#__PURE__*/ (forwardRef as forwardRefType)(function Sele
);
});

// Contexts to clear inside the popover.
const CLEAR_CONTEXTS = [LabelContext, ButtonContext, TextContext];

interface SelectInnerProps<T extends object> {
props: SelectProps<T>,
selectRef: ForwardedRef<HTMLDivElement>,
Expand Down Expand Up @@ -186,7 +189,8 @@ function SelectInner<T extends object>({props, selectRef: ref, collection}: Sele
scrollRef,
placement: 'bottom start',
style: {'--trigger-width': buttonWidth} as React.CSSProperties,
'aria-labelledby': menuProps['aria-labelledby']
'aria-labelledby': menuProps['aria-labelledby'],
clearContexts: CLEAR_CONTEXTS
}],
[ListBoxContext, {...menuProps, ref: scrollRef}],
[ListStateContext, state],
Expand Down
38 changes: 38 additions & 0 deletions packages/react-aria-components/test/ComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,42 @@ describe('ComboBox', () => {

expect(comboboxTester.options()).toHaveLength(7);
});

it('should clear contexts inside popover', async () => {
let tree = render(
<ComboBox>
<Label>Preferred fruit or vegetable</Label>
<Input />
<Button />
<Popover data-testid="popover">
<Label>Hello</Label>
<Button>Yo</Button>
<Input />
<Text>hi</Text>
<ListBox>
<ListBoxItem id="cat">Cat</ListBoxItem>
<ListBoxItem id="dog">Dog</ListBoxItem>
<ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
</ListBox>
</Popover>
</ComboBox>
);

let selectTester = testUtilUser.createTester('Select', {root: tree.container});

await selectTester.open();

let popover = await tree.getByTestId('popover');
let label = popover.querySelector('.react-aria-Label');
expect(label).not.toHaveAttribute('for');

let button = popover.querySelector('.react-aria-Button');
expect(button).not.toHaveAttribute('aria-expanded');

let input = popover.querySelector('.react-aria-Input');
expect(input).not.toHaveAttribute('role');

let text = popover.querySelector('.react-aria-Text');
expect(text).not.toHaveAttribute('id');
});
});
49 changes: 49 additions & 0 deletions packages/react-aria-components/test/DatePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,53 @@ describe('DatePicker', () => {
let hiddenInput = getByRole('textbox', {hidden: true});
expect(hiddenInput).toHaveAttribute('disabled');
});

it('should clear contexts inside popover', async () => {
let {getByRole, getByTestId} = render(
<DatePicker data-foo="bar">
<Label>Birth date</Label>
<Group>
<DateInput>
{(segment) => <DateSegment segment={segment} />}
</DateInput>
<Button>▼</Button>
</Group>
<Text slot="description">Description</Text>
<Text slot="errorMessage">Error</Text>
<Popover data-testid="popover">
<Dialog>
<Label>Hi</Label>
<Group>Yo</Group>
<Button>Hi</Button>
<Text>test</Text>
<Calendar>
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<CalendarGrid>
{(date) => <CalendarCell date={date} />}
</CalendarGrid>
</Calendar>
</Dialog>
</Popover>
</DatePicker>
);

await user.click(getByRole('button'));

let popover = await getByTestId('popover');
let label = popover.querySelector('.react-aria-Label');
expect(label).not.toHaveAttribute('id');

let button = popover.querySelector('.react-aria-Button');
expect(button).not.toHaveAttribute('aria-expanded');

let group = popover.querySelector('.react-aria-Group');
expect(group).not.toHaveAttribute('id');

let text = popover.querySelector('.react-aria-Text');
expect(text).not.toHaveAttribute('id');
});
});
53 changes: 53 additions & 0 deletions packages/react-aria-components/test/DateRangePicker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,57 @@ describe('DateRangePicker', () => {
expect(spinbutton).toHaveAttribute('aria-disabled', 'true');
}
});

it('should clear contexts inside popover', async () => {
let {getByRole, getByTestId} = render(
<DateRangePicker data-foo="bar">
<Label>Birth date</Label>
<Group>
<DateInput slot="start">
{(segment) => <DateSegment segment={segment} />}
</DateInput>
<span aria-hidden="true">–</span>
<DateInput slot="end">
{(segment) => <DateSegment segment={segment} />}
</DateInput>
<Button>▼</Button>
</Group>
<Text slot="description">Description</Text>
<Text slot="errorMessage">Error</Text>
<Popover data-testid="popover">
<Dialog>
<Label>Hi</Label>
<Group>Yo</Group>
<Button>Hi</Button>
<Text>test</Text>
<RangeCalendar>
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<CalendarGrid>
{(date) => <CalendarCell date={date} />}
</CalendarGrid>
</RangeCalendar>
</Dialog>
</Popover>
</DateRangePicker>
);

await user.click(getByRole('button'));

let popover = await getByTestId('popover');
let label = popover.querySelector('.react-aria-Label');
expect(label).not.toHaveAttribute('id');

let button = popover.querySelector('.react-aria-Button');
expect(button).not.toHaveAttribute('aria-expanded');

let group = popover.querySelector('.react-aria-Group');
expect(group).not.toHaveAttribute('id');

let text = popover.querySelector('.react-aria-Text');
expect(text).not.toHaveAttribute('id');
});
});
36 changes: 36 additions & 0 deletions packages/react-aria-components/test/Select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,4 +377,40 @@ describe('Select', () => {
let trigger = selectTester.trigger;
expect(document.activeElement).toBe(trigger);
});

it('should clear contexts inside popover', async () => {
let {getByTestId} = render(
<Select data-testid="select" defaultSelectedKey="cat">
<Label>Favorite Animal</Label>
<Button>
<SelectValue />
</Button>
<Popover data-testid="popover">
<Label>Hello</Label>
<Button>Yo</Button>
<Text>hi</Text>
<ListBox>
<ListBoxItem id="cat">Cat</ListBoxItem>
<ListBoxItem id="dog">Dog</ListBoxItem>
<ListBoxItem id="kangaroo">Kangaroo</ListBoxItem>
</ListBox>
</Popover>
</Select>
);

let wrapper = getByTestId('select');
let selectTester = testUtilUser.createTester('Select', {root: wrapper});

await selectTester.open();

let popover = await getByTestId('popover');
let label = popover.querySelector('.react-aria-Label');
expect(label).not.toHaveAttribute('for');

let button = popover.querySelector('.react-aria-Button');
expect(button).not.toHaveAttribute('aria-expanded');

let text = popover.querySelector('.react-aria-Text');
expect(text).not.toHaveAttribute('id');
});
});