diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index 40b130b4a7a..c4a1d63ea8b 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -1,6 +1,6 @@ { "name": "@react-spectrum/s2", - "version": "1.3.0", + "version": "1.3.1", "description": "Spectrum 2 UI components in React", "license": "Apache-2.0", "repository": { diff --git a/packages/@react-spectrum/s2/src/Calendar.tsx b/packages/@react-spectrum/s2/src/Calendar.tsx index 68e61293b3a..6b5360c818f 100644 --- a/packages/@react-spectrum/s2/src/Calendar.tsx +++ b/packages/@react-spectrum/s2/src/Calendar.tsx @@ -126,7 +126,8 @@ const titleStyles = style({ font: 'title-lg', textAlign: 'center', flexGrow: 1, - flexShrink: 0 + flexShrink: 0, + marginY: 0 }); const headerCellStyles = style({ diff --git a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js index 71923011af6..70b9d26ef04 100644 --- a/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js +++ b/packages/@react-spectrum/s2/style/__tests__/style-macro.test.js @@ -40,26 +40,26 @@ describe('style-macro', () => { "@layer _.a, _.b, _.c; @layer _.b { - .Jbs13:first-child { + .Jbs131:first-child { margin-top: 0.25rem; } } @layer _.c.p { @media (min-width: 64rem) { - .Jbpv13:first-child { + .Jbpv131:first-child { margin-top: 0.5rem; } } } -.-macro-static-YFh7Dd { - --macro-data-YFh7Dd: {"style":{"marginTop":{":first-child":{"default":4,"lg":8}}},"loc":"undefined:undefined:undefined"}; +.-macro-static-MypNfe { + --macro-data-MypNfe: {"style":{"marginTop":{":first-child":{"default":4,"lg":8}}},"loc":"undefined:undefined:undefined"}; } " `); - expect(js).toMatchInlineSnapshot('" Jbs13 Jbpv13 -macro-static-YFh7Dd"'); + expect(js).toMatchInlineSnapshot('" Jbs131 Jbpv131 -macro-static-MypNfe"'); }); it('should support self references', () => { @@ -73,59 +73,59 @@ describe('style-macro', () => { "@layer _.a; @layer _.a { - ._kc13 { + ._kc131 { border-top-width: 2px; } - .hc13 { + .hc131 { border-bottom-width: 2px; } - .mCPFGYc13 { + .mCPFGYc131 { border-inline-start-width: var(--m); } - .lc13 { + .lc131 { border-inline-end-width: 2px; } - .SMBFGYc13 { + .SMBFGYc131 { padding-inline-start: var(--S); } - .Rv13 { + .Rv131 { padding-inline-end: calc(var(--F, var(--M)) * 3 / 8); } - .ZjUQgKd13 { + .ZjUQgKd131 { width: calc(200px - var(--m) - var(--S)); } - .-m_-mc13 { + .-m_-mc131 { --m: 2px; } - .-S_-Sv13 { + .-S_-Sv131 { --S: calc(var(--F, var(--M)) * 3 / 8); } } -.-macro-static-1Cddbe { - --macro-data-1Cddbe: {"style":{"borderWidth":2,"paddingX":"edge-to-text","width":"calc(200px - self(borderStartWidth) - self(paddingStart))"},"loc":"undefined:undefined:undefined"}; +.-macro-static-uhFF5b { + --macro-data-uhFF5b: {"style":{"borderWidth":2,"paddingX":"edge-to-text","width":"calc(200px - self(borderStartWidth) - self(paddingStart))"},"loc":"undefined:undefined:undefined"}; } " `); - expect(js).toMatchInlineSnapshot('" _kc13 hc13 mCPFGYc13 lc13 SMBFGYc13 Rv13 ZjUQgKd13 -m_-mc13 -S_-Sv13 -macro-static-1Cddbe"'); + expect(js).toMatchInlineSnapshot('" _kc131 hc131 mCPFGYc131 lc131 SMBFGYc131 Rv131 ZjUQgKd131 -m_-mc131 -S_-Sv131 -macro-static-uhFF5b"'); }); it('should support allowed overrides', () => { @@ -142,9 +142,9 @@ describe('style-macro', () => { color: 'green-400' }); - expect(js()).toMatchInlineSnapshot('" gw13 pg13 -macro-dynamic-1sq4ojw"'); - expect(overrides).toMatchInlineSnapshot('" g8tmWqb13 pHJ3AUd13 -macro-static-y8GgU"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb13 pg13 -macro-dynamic-ip5paw"'); + expect(js()).toMatchInlineSnapshot('" gw131 pg131 -macro-dynamic-mu4yby"'); + expect(overrides).toMatchInlineSnapshot('" g8tmWqb131 pHJ3AUd131 -macro-static-CvspF"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" g8tmWqb131 pg131 -macro-dynamic-1b9jdne"'); }); it('should support allowed overrides for properties that expand into multiple', () => { @@ -159,9 +159,9 @@ describe('style-macro', () => { translateX: 40 }); - expect(js()).toMatchInlineSnapshot('" -_7PloMd-B13 __Ya13 -macro-dynamic-x9xvvh"'); - expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D13 __Ya13 -macro-static-RwkXic"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D13 __Ya13 -macro-dynamic-1xvle73"'); + expect(js()).toMatchInlineSnapshot('" -_7PloMd-B131 __Ya131 -macro-dynamic-4f51mn"'); + expect(overrides).toMatchInlineSnapshot('" -_7PloMd-D131 __Ya131 -macro-static-nMoh6b"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" -_7PloMd-D131 __Ya131 -macro-dynamic-f33ipt"'); }); it('should support allowed overrides for shorthands', () => { @@ -176,9 +176,9 @@ describe('style-macro', () => { padding: 40 }); - expect(js()).toMatchInlineSnapshot('" Tk13 Qk13 Sk13 Rk13 -macro-dynamic-1y1msxh"'); - expect(overrides).toMatchInlineSnapshot('" Tm13 Qm13 Sm13 Rm13 -macro-static-9aCkg"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" Tm13 Qm13 Sm13 Rm13 -macro-dynamic-12xmef1"'); + expect(js()).toMatchInlineSnapshot('" Tk131 Qk131 Sk131 Rk131 -macro-dynamic-efgw2h"'); + expect(overrides).toMatchInlineSnapshot('" Tm131 Qm131 Sm131 Rm131 -macro-static-9GxvHb"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" Tm131 Qm131 Sm131 Rm131 -macro-dynamic-15z7wbl"'); }); it('should support allowed overrides for fontSize', () => { @@ -193,9 +193,9 @@ describe('style-macro', () => { fontSize: 'ui-xs' }); - expect(js()).toMatchInlineSnapshot('" -_6BNtrc-woabcc13 vx13 -macro-dynamic-n1gji4"'); - expect(overrides).toMatchInlineSnapshot('" -_6BNtrc-a13 vx13 -macro-static-Qivc3b"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" -_6BNtrc-a13 vx13 -macro-dynamic-amscby"'); + expect(js()).toMatchInlineSnapshot('" -_6BNtrc-woabcc131 vx131 -macro-dynamic-zj2kbi"'); + expect(overrides).toMatchInlineSnapshot('" -_6BNtrc-a131 vx131 -macro-static-EPvKFd"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" -_6BNtrc-a131 vx131 -macro-dynamic-jkg234"'); }); it("should support allowed overrides for values that aren't defined", () => { @@ -210,9 +210,9 @@ describe('style-macro', () => { minWidth: 32 }); - expect(js()).toMatchInlineSnapshot('" gE13 -macro-dynamic-n9ew9r"'); - expect(overrides).toMatchInlineSnapshot('" Nk13 -macro-static-kp86Ie"'); - expect(js({}, overrides)).toMatchInlineSnapshot('" Nk13 gE13 -macro-dynamic-1r3wdy4"'); + expect(js()).toMatchInlineSnapshot('" gE131 -macro-dynamic-1osl3dc"'); + expect(overrides).toMatchInlineSnapshot('" Nk131 -macro-static-J7ItY"'); + expect(js({}, overrides)).toMatchInlineSnapshot('" Nk131 gE131 -macro-dynamic-1soyf0e"'); }); it('should support runtime conditions', () => { @@ -233,32 +233,32 @@ describe('style-macro', () => { "@layer _.a; @layer _.a { - .gH13 { + .gH131 { background-color: light-dark(rgb(233, 233, 233), rgb(44, 44, 44)); } - .gF13 { + .gF131 { background-color: light-dark(rgb(225, 225, 225), rgb(50, 50, 50)); } - .gE13 { + .gE131 { background-color: light-dark(rgb(218, 218, 218), rgb(57, 57, 57)); } - .pt13 { + .pt131 { color: light-dark(rgb(41, 41, 41), rgb(219, 219, 219)); } - .po13 { + .po131 { color: light-dark(rgb(19, 19, 19), rgb(242, 242, 242)); } - .pm13 { + .pm131 { color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255)); } } @@ -266,9 +266,9 @@ describe('style-macro', () => { " `); - expect(js({})).toMatchInlineSnapshot('" gH13 pt13 -macro-dynamic-1mxpwju"'); - expect(js({isHovered: true})).toMatchInlineSnapshot('" gF13 po13 -macro-dynamic-xzamxv"'); - expect(js({isPressed: true})).toMatchInlineSnapshot('" gE13 pm13 -macro-dynamic-1t1eo"'); + expect(js({})).toMatchInlineSnapshot('" gH131 pt131 -macro-dynamic-15nvpf0"'); + expect(js({isHovered: true})).toMatchInlineSnapshot('" gF131 po131 -macro-dynamic-1g7f3s5"'); + expect(js({isPressed: true})).toMatchInlineSnapshot('" gE131 pm131 -macro-dynamic-1nv6ioy"'); }); it('should support nested runtime conditions', () => { @@ -287,32 +287,32 @@ describe('style-macro', () => { "@layer _.a; @layer _.a { - .gH13 { + .gH131 { background-color: light-dark(rgb(233, 233, 233), rgb(44, 44, 44)); } - .gF13 { + .gF131 { background-color: light-dark(rgb(225, 225, 225), rgb(50, 50, 50)); } - .g_h13 { + .g_h131 { background-color: light-dark(rgb(75, 117, 255), rgb(64, 105, 253)); } - .g313 { + .g3131 { background-color: light-dark(rgb(59, 99, 251), rgb(86, 129, 255)); } } " `); - expect(js({})).toMatchInlineSnapshot('" gH13 -macro-dynamic-10lz7uq"'); - expect(js({isHovered: true})).toMatchInlineSnapshot('" gF13 -macro-dynamic-1fdz0sg"'); - expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h13 -macro-dynamic-1xv38n5"'); - expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g313 -macro-dynamic-e750r1"'); + expect(js({})).toMatchInlineSnapshot('" gH131 -macro-dynamic-43fbrn"'); + expect(js({isHovered: true})).toMatchInlineSnapshot('" gF131 -macro-dynamic-1tkkitt"'); + expect(js({isSelected: true})).toMatchInlineSnapshot('" g_h131 -macro-dynamic-zzf86q"'); + expect(js({isSelected: true, isHovered: true})).toMatchInlineSnapshot('" g3131 -macro-dynamic-19tzf3y"'); }); it('should support variant runtime conditions', () => { @@ -326,9 +326,9 @@ describe('style-macro', () => { } }); - expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY13 -macro-dynamic-hjoyur"'); - expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe13 -macro-dynamic-h5ftf1"'); - expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw13 -macro-dynamic-8z42q9"'); + expect(js({variant: 'accent'})).toMatchInlineSnapshot('" gY131 -macro-dynamic-ea1kkk"'); + expect(js({variant: 'primary'})).toMatchInlineSnapshot('" gjQquMe131 -macro-dynamic-17nl5q"'); + expect(js({variant: 'secondary'})).toMatchInlineSnapshot('" gw131 -macro-dynamic-fjccci"'); }); it('supports runtime conditions nested inside css conditions', () => { @@ -346,14 +346,14 @@ describe('style-macro', () => { @layer _.b.l { @media (forced-colors: active) { - .plb13 { + .plb131 { color: ButtonText; } } @media (forced-colors: active) { - .ple13 { + .ple131 { color: HighlightText; } } @@ -362,8 +362,8 @@ describe('style-macro', () => { " `); - expect(js({})).toMatchInlineSnapshot('" plb13 -macro-dynamic-whoju9"'); - expect(js({isSelected: true})).toMatchInlineSnapshot('" ple13 -macro-dynamic-19u8vf8"'); + expect(js({})).toMatchInlineSnapshot('" plb131 -macro-dynamic-a7vfaa"'); + expect(js({isSelected: true})).toMatchInlineSnapshot('" ple131 -macro-dynamic-ojtpnp"'); }); it('inherits parent default when nested branch has no default key', () => { @@ -379,8 +379,8 @@ describe('style-macro', () => { }); // forcedColors.default should apply when variant=highlight but !isSelected expect(css).toContain('ButtonText'); - expect(js({variant: 'highlight'})).toMatchInlineSnapshot('" plb13 -macro-dynamic-whoju9"'); - expect(js({variant: 'highlight', isSelected: true})).toMatchInlineSnapshot('" ple13 -macro-dynamic-19u8vf8"'); + expect(js({variant: 'highlight'})).toMatchInlineSnapshot('" plb131 -macro-dynamic-a7vfaa"'); + expect(js({variant: 'highlight', isSelected: true})).toMatchInlineSnapshot('" ple131 -macro-dynamic-ojtpnp"'); }); it('should expand shorthand properties to longhands', () => { @@ -388,33 +388,33 @@ describe('style-macro', () => { padding: 24 }); - expect(js).toMatchInlineSnapshot('" Th13 Qh13 Sh13 Rh13 -macro-static-jobh3c"'); + expect(js).toMatchInlineSnapshot('" Th131 Qh131 Sh131 Rh131 -macro-static-D80Fbe"'); expect(css).toMatchInlineSnapshot(` "@layer _.a; @layer _.a { - .Th13 { + .Th131 { padding-top: 24px; } - .Qh13 { + .Qh131 { padding-bottom: 24px; } - .Sh13 { + .Sh131 { padding-inline-start: 24px; } - .Rh13 { + .Rh131 { padding-inline-end: 24px; } } -.-macro-static-jobh3c { - --macro-data-jobh3c: {"style":{"padding":24},"loc":"undefined:undefined:undefined"}; +.-macro-static-D80Fbe { + --macro-data-D80Fbe: {"style":{"padding":24},"loc":"undefined:undefined:undefined"}; } " @@ -430,13 +430,13 @@ describe('style-macro', () => { "@layer _.a; @layer _.a { - .gpQzfVb13 { + .gpQzfVb131 { background-color: rgb(from light-dark(rgb(39, 77, 234), rgb(105, 149, 254)) r g b / 50%); } } -.-macro-static-FwWE9d { - --macro-data-FwWE9d: {"style":{"backgroundColor":"blue-1000/50"},"loc":"undefined:undefined:undefined"}; +.-macro-static-aOn8Q { + --macro-data-aOn8Q: {"style":{"backgroundColor":"blue-1000/50"},"loc":"undefined:undefined:undefined"}; } " @@ -455,13 +455,13 @@ describe('style-macro', () => { "@layer _.a; @layer _.a { - .-FUeYm-gE13 { + .-FUeYm-gE131 { --foo: light-dark(rgb(218, 218, 218), rgb(57, 57, 57)); } } -.-macro-static-7WNpCd { - --macro-data-7WNpCd: {"style":{"--foo":{"type":"backgroundColor","value":"gray-300"}},"loc":"undefined:undefined:undefined"}; +.-macro-static-kFNJUb { + --macro-data-kFNJUb: {"style":{"--foo":{"type":"backgroundColor","value":"gray-300"}},"loc":"undefined:undefined:undefined"}; } " diff --git a/packages/dev/codemods/package.json b/packages/dev/codemods/package.json index 60e997c84bd..f24cc75f27b 100644 --- a/packages/dev/codemods/package.json +++ b/packages/dev/codemods/package.json @@ -28,7 +28,7 @@ "@babel/parser": "^7.24.5", "@babel/traverse": "^7.24.5", "@babel/types": "^7.24.5", - "@react-spectrum/s2": "1.3.0", + "@react-spectrum/s2": "1.3.1", "@react-types/shared": "^3.34.0", "@types/node": "^24", "boxen": "^5.1.2", diff --git a/packages/react-aria-components/src/ComboBox.tsx b/packages/react-aria-components/src/ComboBox.tsx index f75cd8d5642..e3fc0c00a1e 100644 --- a/packages/react-aria-components/src/ComboBox.tsx +++ b/packages/react-aria-components/src/ComboBox.tsx @@ -33,14 +33,14 @@ import {createHideableComponent} from 'react-aria/private/collections/Hidden'; import {FieldErrorContext} from './FieldError'; import {filterDOMProps} from 'react-aria/filterDOMProps'; import {FormContext} from './Form'; -import {forwardRefType, GlobalDOMAttributes, Key, RefObject} from '@react-types/shared'; +import {GlobalDOMAttributes, Key, RefObject} from '@react-types/shared'; import {GroupContext} from './Group'; import {InputContext} from './Input'; import {LabelContext} from './Label'; import {ListBoxContext, ListStateContext} from './ListBox'; import {OverlayTriggerStateContext} from './Dialog'; import {PopoverContext} from './Popover'; -import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react'; +import React, {createContext, ForwardedRef, HTMLAttributes, ReactElement, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react'; import {TextContext} from './Text'; import {useFilter} from 'react-aria/useFilter'; import {useListFormatter} from 'react-aria/useListFormatter'; @@ -100,7 +100,7 @@ export const ComboBoxStateContext = createContext(props: ComboBoxProps, ref: ForwardedRef) { +export const ComboBox = /*#__PURE__*/ createHideableComponent(function ComboBox(props: ComboBoxProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, ComboBoxContext); let {children, isDisabled = false, isInvalid = false, isRequired = false, isReadOnly = false} = props; let content = useMemo(() => ( diff --git a/packages/react-aria-components/src/Menu.tsx b/packages/react-aria-components/src/Menu.tsx index 8d474a61192..24d63d3511f 100644 --- a/packages/react-aria-components/src/Menu.tsx +++ b/packages/react-aria-components/src/Menu.tsx @@ -75,6 +75,7 @@ import {SharedElementTransition} from './SharedElementTransition'; import {TextContext} from './Text'; import {TreeState, useTreeState} from 'react-stately/useTreeState'; import {useHover} from 'react-aria/useHover'; +import {useIsHidden} from 'react-aria/private/collections/Hidden'; import {useMultipleSelectionState} from 'react-stately/useMultipleSelectionState'; import {useObjectRef} from 'react-aria/useObjectRef'; @@ -87,7 +88,7 @@ export interface MenuTriggerProps extends BaseMenuTriggerProps { children: ReactNode } -export function MenuTrigger(props: MenuTriggerProps): JSX.Element { +export function MenuTrigger(props: MenuTriggerProps): JSX.Element | null { let state = useMenuTriggerState(props); let ref = useRef(null); let {menuTriggerProps, menuProps} = useMenuTrigger({ @@ -96,6 +97,13 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element { }, state, ref); let scrollRef = useRef(null); + // If within a collection (e.g. Tabs), render nothing. + // Not using createHideableComponent for this because that also creates a forwardRef. + let isHidden = useIsHidden(); + if (isHidden) { + return null; + } + return ( (props: SelectProps, ref: ForwardedRef) { +export const Select = /*#__PURE__*/ createHideableComponent(function Select(props: SelectProps, ref: ForwardedRef) { [props, ref] = useContextProps(props, ref, SelectContext); let {children, isDisabled = false, isInvalid = false, isRequired = false} = props; let content = useMemo(() => ( diff --git a/packages/react-aria-components/test/Menu.test.tsx b/packages/react-aria-components/test/Menu.test.tsx index 798ca2d6eed..38b43a8fa44 100644 --- a/packages/react-aria-components/test/Menu.test.tsx +++ b/packages/react-aria-components/test/Menu.test.tsx @@ -137,7 +137,10 @@ describe('Menu', () => { it('should support custom render function', () => { let {getAllByRole, getByRole} = renderMenu( {render: props =>
}, - {render: props =>
} + {render: props => { + expect('href' in props).toBe(false); + return
; + }} ); let menu = getByRole('menu'); expect(menu).toHaveAttribute('data-custom', 'true'); @@ -150,8 +153,11 @@ describe('Menu', () => { it('should support custom render function as a link', () => { let {getAllByRole, getByRole} = renderMenu( {render: props =>
}, - // eslint-disable-next-line jsx-a11y/anchor-has-content - {href: '#foo', render: props => } + {href: '#foo', render: props => { + expect(props.href).toBe('#foo'); + // eslint-disable-next-line jsx-a11y/anchor-has-content + return ; + }} ); let menu = getByRole('menu'); expect(menu).toHaveAttribute('data-custom', 'true'); diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 7982487d543..f78eddf1d9c 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -12,8 +12,15 @@ import {act, fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal'; import {Button} from '../src/Button'; +import {ComboBox} from '../src/ComboBox'; +import {Input} from '../src/Input'; +import {Label} from '../src/Label'; +import {ListBox, ListBoxItem} from '../src/ListBox'; +import {Menu, MenuItem, MenuTrigger} from '../src/Menu'; +import {Popover} from '../src/Popover'; import React, {useState} from 'react'; import {RouterProvider} from 'react-aria/private/utils/openLink'; +import {Select, SelectValue} from '../src/Select'; import {Tab, TabList, TabPanel, TabPanels, Tabs} from '../src/Tabs'; import {TabsExample} from '../stories/Tabs.stories'; import {Tooltip, TooltipTrigger} from '../src/Tooltip'; @@ -847,4 +854,120 @@ describe('Tabs', () => { expect(getAllByRole('tab')).toHaveLength(3); }); } + + it('supports Menu inside Tabs', async () => { + let tree = render( + +
+ + First Tab + Second Tab + + + + + + Item 1 + Item 2 + + + +
+ + First Tab content + + + Second Tab content + +
+ ); + + let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')}); + expect(tester.tabs.length).toBe(2); + + let trigger = tree.getByRole('button'); + let menu = testUtilUser.createTester('Menu', {root: trigger}); + await menu.open(); + expect(menu.options()).toHaveLength(2); + await menu.close(); + }); + + it('supports Select inside Tabs', async () => { + let tree = render( + +
+ + First Tab + Second Tab + + +
+ + First Tab content + + + Second Tab content + +
+ ); + + let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')}); + expect(tester.tabs.length).toBe(2); + + let trigger = tree.getByRole('button'); + let menu = testUtilUser.createTester('Select', {root: trigger}); + await menu.open(); + expect(menu.options()).toHaveLength(3); + await menu.close(); + }); + + it('supports ComboBox inside Tabs', async () => { + let tree = render( + +
+ + First Tab + Second Tab + + + + + + + Cat + Dog + Kangaroo + + + +
+ + First Tab content + + + Second Tab content + +
+ ); + + let tester = testUtilUser.createTester('Tabs', {root: tree.getByRole('tablist')}); + expect(tester.tabs.length).toBe(2); + + let menu = testUtilUser.createTester('ComboBox', {interactionType: 'keyboard', root: tree.container.querySelector('.react-aria-ComboBox')}); + await menu.open(); + expect(menu.options()).toHaveLength(3); + await menu.close(); + }); }); diff --git a/packages/react-aria-components/test/Tree.test.tsx b/packages/react-aria-components/test/Tree.test.tsx index 1da7313e8f9..80c346bbe33 100644 --- a/packages/react-aria-components/test/Tree.test.tsx +++ b/packages/react-aria-components/test/Tree.test.tsx @@ -915,144 +915,160 @@ describe('Tree', () => { }); describe('keyboard interactions', () => { - it('left and right arrows should navigate between interactive elements in the row', async () => { - let {getAllByRole} = render(); - let expandableRow = getAllByRole('row')[0]; - let buttons = within(expandableRow).getAllByRole('button'); - let checkbox = within(expandableRow).getByRole('checkbox'); - - await user.tab(); - expect(expandableRow).toHaveAttribute('aria-expanded', 'true'); - expect(document.activeElement).toBe(expandableRow); - await user.keyboard('{ArrowRight}'); - expect(document.activeElement).toBe(checkbox); - await user.keyboard('{ArrowRight}'); - expect(document.activeElement).toBe(buttons[1]); - await user.keyboard('{ArrowRight}'); - expect(document.activeElement).toBe(buttons[2]); - await user.keyboard('{ArrowRight}'); - expect(document.activeElement).toBe(expandableRow); - - // Test that if focus is on the row that right/left will expand/collapse if it isn't already - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(expandableRow); - expect(expandableRow).toHaveAttribute('aria-expanded', 'false'); - - await user.keyboard('{ArrowRight}'); - expect(document.activeElement).toBe(expandableRow); - expect(expandableRow).toHaveAttribute('aria-expanded', 'true'); - - // Resume testing navigation to interacive elements - await user.keyboard('{ArrowLeft}'); - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(buttons[2]); - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(buttons[1]); - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(checkbox); - }); - - it('should support type ahead', async () => { - let {getAllByRole, queryByText} = render(); - await user.tab(); - let rows = getAllByRole('row'); - expect(document.activeElement).toBe(rows[0]); - await user.keyboard('Reports 1ABC'); - expect(document.activeElement).toBe(rows[16]); - - act(() => {jest.runAllTimers();}); - await user.keyboard('Pro'); - expect(document.activeElement).toBe(rows[0]); - - // Test typeahead doesn't match against hidden rows - await user.click(rows[12]); - expect(queryByText('Reports 1ABC')).toBeFalsy(); - await user.keyboard('Reports 1ABC'); - expect(document.activeElement).toBe(rows[12]); - expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); - }); - - it('should support collapse key to navigate to parent', async () => { - let {getAllByRole} = render(); - await user.tab(); - let rows = getAllByRole('row'); - expect(rows).toHaveLength(20); - expect(document.activeElement).toBe(rows[0]); - expect(document.activeElement).toHaveAttribute('data-expanded', 'true'); - - // Navigate down to Project 2B - await user.keyboard('{ArrowDown}'); - await user.keyboard('{ArrowDown}'); - await user.keyboard('{ArrowRight}'); - await user.keyboard('{ArrowDown}'); - await user.keyboard('{ArrowDown}'); - expect(document.activeElement).toBe(rows[4]); - expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2B'); - - // Collapse key on leaf node should move focus to parent (Projects) - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(rows[2]); - expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2'); - expect(document.activeElement).toHaveAttribute('data-expanded', 'true'); - - // Collapse key on expanded parent should collapse it - await user.keyboard('{ArrowLeft}'); - // Projects should now be collapsed, so fewer rows visible - rows = getAllByRole('row'); - expect(rows.length).toBeLessThan(20); - expect(document.activeElement).toBe(rows[2]); - expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2'); - expect(document.activeElement).not.toHaveAttribute('data-expanded'); + describe.each(['DynamicTree', 'DynamicSectionTree'])('%s', (comp) => { + const TreeExample = comp === 'DynamicSectionTree' ? DynamicSectionTree : DynamicTree; + + function getAllRows(tree: HTMLElement) { + const rowGroups = within(tree).queryAllByRole('rowgroup'); + return rowGroups.length > 0 + ? rowGroups.flatMap((rowGroup) => within(rowGroup).queryAllByRole('row').slice(1)) + : within(tree).queryAllByRole('row'); + } - // Collapse key again on now-collapsed parent should move to its parent - await user.keyboard('{ArrowLeft}'); - expect(document.activeElement).toBe(rows[0]); - expect(document.activeElement).toHaveAttribute('aria-label', 'Projects'); - }); + it('left and right arrows should navigate between interactive elements in the row', async () => { + let {getByRole} = render(); + let tree = getByRole('treegrid'); + let expandableRow = getAllRows(tree)[0]; + let buttons = within(expandableRow).getAllByRole('button'); + let checkbox = within(expandableRow).getByRole('checkbox'); + + await user.tab(); + expect(expandableRow).toHaveAttribute('aria-expanded', 'true'); + expect(document.activeElement).toBe(expandableRow); + await user.keyboard('{ArrowRight}'); + expect(document.activeElement).toBe(checkbox); + await user.keyboard('{ArrowRight}'); + expect(document.activeElement).toBe(buttons[1]); + await user.keyboard('{ArrowRight}'); + expect(document.activeElement).toBe(buttons[2]); + await user.keyboard('{ArrowRight}'); + expect(document.activeElement).toBe(expandableRow); + + // Test that if focus is on the row that right/left will expand/collapse if it isn't already + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(expandableRow); + expect(expandableRow).toHaveAttribute('aria-expanded', 'false'); + + await user.keyboard('{ArrowRight}'); + expect(document.activeElement).toBe(expandableRow); + expect(expandableRow).toHaveAttribute('aria-expanded', 'true'); + + // Resume testing navigation to interacive elements + await user.keyboard('{ArrowLeft}'); + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(buttons[2]); + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(buttons[1]); + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(checkbox); + }); - it('should navigate between visible rows when using Arrow Up/Down', async () => { - let {getAllByRole} = render(); - await user.tab(); - let rows = getAllByRole('row'); - expect(rows).toHaveLength(20); - expect(document.activeElement).toBe(rows[0]); - await user.keyboard('{ArrowDown}'); - expect(document.activeElement).toBe(rows[1]); - expect(rows[1]).toHaveAttribute('aria-label', 'Project 1'); - await user.keyboard('{ArrowUp}'); + it('should support type ahead', async () => { + let {getByRole, queryByText} = render(); + await user.tab(); + let tree = getByRole('treegrid'); + let rows = getAllRows(tree); + expect(document.activeElement).toBe(rows[0]); + await user.keyboard('Reports 1ABC'); + expect(document.activeElement).toBe(rows[16]); + + act(() => {jest.runAllTimers();}); + await user.keyboard('Pro'); + expect(document.activeElement).toBe(rows[0]); + + // Test typeahead doesn't match against hidden rows + await user.click(rows[12]); + expect(queryByText('Reports 1ABC')).toBeFalsy(); + await user.keyboard('Reports 1ABC'); + expect(document.activeElement).toBe(rows[12]); + expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); + }); - // Collapse parent row and try arrow navigation again - await user.keyboard('{ArrowLeft}'); - rows = getAllByRole('row'); - expect(rows).toHaveLength(9); - await user.keyboard('{ArrowDown}'); - expect(document.activeElement).toBe(rows[1]); - expect(rows[1]).toHaveAttribute('aria-label', 'Reports'); - await user.keyboard('{ArrowUp}'); - expect(document.activeElement).toBe(rows[0]); - expect(rows[0]).toHaveAttribute('aria-label', 'Projects'); - }); + it('should support collapse key to navigate to parent', async () => { + let {getByRole} = render(); + await user.tab(); + let tree = getByRole('treegrid'); + let rows = getAllRows(tree); + expect(rows).toHaveLength(20); + expect(document.activeElement).toBe(rows[0]); + expect(document.activeElement).toHaveAttribute('data-expanded', 'true'); + + // Navigate down to Project 2B + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowRight}'); + await user.keyboard('{ArrowDown}'); + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(rows[4]); + expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2B'); + + // Collapse key on leaf node should move focus to parent (Projects) + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(rows[2]); + expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2'); + expect(document.activeElement).toHaveAttribute('data-expanded', 'true'); + + // Collapse key on expanded parent should collapse it + await user.keyboard('{ArrowLeft}'); + // Projects should now be collapsed, so fewer rows visible + rows = getAllRows(tree); + expect(rows.length).toBeLessThan(20); + expect(document.activeElement).toBe(rows[2]); + expect(document.activeElement).toHaveAttribute('aria-label', 'Project 2'); + expect(document.activeElement).not.toHaveAttribute('data-expanded'); + + // Collapse key again on now-collapsed parent should move to its parent + await user.keyboard('{ArrowLeft}'); + expect(document.activeElement).toBe(rows[0]); + expect(document.activeElement).toHaveAttribute('aria-label', 'Projects'); + }); - it('should navigate between visible rows when using Home/End', async () => { - let {getAllByRole} = render(); - await user.tab(); - let rows = getAllByRole('row'); - expect(rows).toHaveLength(20); - expect(document.activeElement).toBe(rows[0]); - await user.keyboard('{End}'); - expect(document.activeElement).toBe(rows[19]); - expect(rows[19]).toHaveAttribute('aria-label', 'Reports 2'); - await user.keyboard('{Home}'); - expect(document.activeElement).toBe(rows[0]); + it('should navigate between visible rows when using Arrow Up/Down', async () => { + let {getByRole} = render(); + await user.tab(); + let tree = getByRole('treegrid'); + let rows = getAllRows(tree); + expect(rows).toHaveLength(20); + expect(document.activeElement).toBe(rows[0]); + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(rows[1]); + expect(rows[1]).toHaveAttribute('aria-label', 'Project 1'); + await user.keyboard('{ArrowUp}'); + + // Collapse parent row and try arrow navigation again + await user.keyboard('{ArrowLeft}'); + rows = getAllRows(tree); + expect(rows).toHaveLength(9); + await user.keyboard('{ArrowDown}'); + expect(document.activeElement).toBe(rows[1]); + expect(rows[1]).toHaveAttribute('aria-label', 'Reports'); + await user.keyboard('{ArrowUp}'); + expect(document.activeElement).toBe(rows[0]); + expect(rows[0]).toHaveAttribute('aria-label', 'Projects'); + }); - // Collapse the 2nd top level row and try End/Home again - await user.click(rows[12]); - rows = getAllByRole('row'); - expect(rows).toHaveLength(13); - await user.keyboard('{Home}'); - await user.keyboard('{End}'); - expect(document.activeElement).toBe(rows[12]); - expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); + it('should navigate between visible rows when using Home/End', async () => { + let {getByRole} = render(); + await user.tab(); + let tree = getByRole('treegrid'); + let rows = getAllRows(tree); + expect(rows).toHaveLength(20); + expect(document.activeElement).toBe(rows[0]); + await user.keyboard('{End}'); + expect(document.activeElement).toBe(rows[19]); + expect(rows[19]).toHaveAttribute('aria-label', 'Reports 2'); + await user.keyboard('{Home}'); + expect(document.activeElement).toBe(rows[0]); + + // Collapse the 2nd top level row and try End/Home again + await user.click(rows[12]); + rows = getAllRows(tree); + expect(rows).toHaveLength(13); + await user.keyboard('{Home}'); + await user.keyboard('{End}'); + expect(document.activeElement).toBe(rows[12]); + expect(rows[12]).toHaveAttribute('aria-label', 'Reports'); + }); }); }); }); diff --git a/packages/react-aria/src/gridlist/useGridListItem.ts b/packages/react-aria/src/gridlist/useGridListItem.ts index c2ab564852c..ba06535d305 100644 --- a/packages/react-aria/src/gridlist/useGridListItem.ts +++ b/packages/react-aria/src/gridlist/useGridListItem.ts @@ -164,7 +164,8 @@ export function useGridListItem(props: AriaGridListItemOptions, state: ListSt return; } else if ( !state.expandedKeys.has(node.key) && - node.parentKey + node.parentKey && + state.collection.getItem(node.parentKey)?.type === 'item' ) { // Item is a leaf or already collapsed, move focus to parent state.selectionManager.setFocusedKey(node.parentKey); diff --git a/packages/react-aria/src/utils/openLink.tsx b/packages/react-aria/src/utils/openLink.tsx index d38980d2396..a22fae74ef2 100644 --- a/packages/react-aria/src/utils/openLink.tsx +++ b/packages/react-aria/src/utils/openLink.tsx @@ -174,14 +174,15 @@ export function getSyntheticLinkProps(props: LinkDOMProps): DOMAttributes