diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index 2f846289e41..a3804b1d5a9 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -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 { props: ComboBoxProps, collection: Collection>, @@ -197,7 +200,8 @@ function ComboBoxInner({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], diff --git a/packages/react-aria-components/src/DatePicker.tsx b/packages/react-aria-components/src/DatePicker.tsx index ac7b8b6df70..55e03a122bf 100644 --- a/packages/react-aria-components/src/DatePicker.tsx +++ b/packages/react-aria-components/src/DatePicker.tsx @@ -72,6 +72,9 @@ export const DateRangePickerContext = createContext(null); export const DateRangePickerStateContext = createContext(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. */ @@ -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, { @@ -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, { diff --git a/packages/react-aria-components/src/Popover.tsx b/packages/react-aria-components/src/Popover.tsx index 33946892d9c..0ab82a28e31 100644 --- a/packages/react-aria-components/src/Popover.tsx +++ b/packages/react-aria-components/src/Popover.tsx @@ -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, Omit, OverlayTriggerProps, RenderProps, SlotProps, AriaLabelingProps { @@ -80,7 +80,12 @@ export interface PopoverRenderProps { isExiting: boolean } -export const PopoverContext = createContext>(null); +interface PopoverContextValue extends PopoverProps { + /** Contexts to clear. */ + clearContexts?: Context[] +} + +export const PopoverContext = createContext>(null); // Stores a ref for the portal container for a group of popovers (e.g. submenus). const PopoverGroupContext = createContext | null>(null); @@ -134,10 +139,11 @@ interface PopoverInnerProps extends AriaPopoverProps, RenderProps[] } -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(null); @@ -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 = {children}; + } + } + return children; + }, [renderProps.children, clearContexts]); + let style = {...popoverProps.style, ...renderProps.style}; let overlay = (
{!props.isNonModal && } - {renderProps.children} + {children}
diff --git a/packages/react-aria-components/src/Select.tsx b/packages/react-aria-components/src/Select.tsx index f29cb6ba4e3..d96efb4dc23 100644 --- a/packages/react-aria-components/src/Select.tsx +++ b/packages/react-aria-components/src/Select.tsx @@ -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 { props: SelectProps, selectRef: ForwardedRef, @@ -186,7 +189,8 @@ function SelectInner({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], diff --git a/packages/react-aria-components/test/ComboBox.test.js b/packages/react-aria-components/test/ComboBox.test.js index 7fa0f239a22..670362f6a65 100644 --- a/packages/react-aria-components/test/ComboBox.test.js +++ b/packages/react-aria-components/test/ComboBox.test.js @@ -340,4 +340,42 @@ describe('ComboBox', () => { expect(comboboxTester.options()).toHaveLength(7); }); + + it('should clear contexts inside popover', async () => { + let tree = render( + + + + + + hi + + Cat + Dog + Kangaroo + + + + ); + + 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'); + }); }); diff --git a/packages/react-aria-components/test/DatePicker.test.js b/packages/react-aria-components/test/DatePicker.test.js index ccd2f03bb1d..59a0239bc8f 100644 --- a/packages/react-aria-components/test/DatePicker.test.js +++ b/packages/react-aria-components/test/DatePicker.test.js @@ -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( + + + + + {(segment) => } + + + + Description + Error + + + + Yo + + test + +
+ + + +
+ + {(date) => } + +
+
+
+
+ ); + + 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'); + }); }); diff --git a/packages/react-aria-components/test/DateRangePicker.test.js b/packages/react-aria-components/test/DateRangePicker.test.js index 3a43c928017..aab7089ba2a 100644 --- a/packages/react-aria-components/test/DateRangePicker.test.js +++ b/packages/react-aria-components/test/DateRangePicker.test.js @@ -314,4 +314,57 @@ describe('DateRangePicker', () => { expect(spinbutton).toHaveAttribute('aria-disabled', 'true'); } }); + + it('should clear contexts inside popover', async () => { + let {getByRole, getByTestId} = render( + + + + + {(segment) => } + + + + {(segment) => } + + + + Description + Error + + + + Yo + + test + +
+ + + +
+ + {(date) => } + +
+
+
+
+ ); + + 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'); + }); }); diff --git a/packages/react-aria-components/test/Select.test.js b/packages/react-aria-components/test/Select.test.js index ebdb286ab9d..66b6a213bea 100644 --- a/packages/react-aria-components/test/Select.test.js +++ b/packages/react-aria-components/test/Select.test.js @@ -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( + + ); + + 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'); + }); });