From f76c445b5b9265640098f14338b42261837d4746 Mon Sep 17 00:00:00 2001 From: Jeff Luyau Date: Tue, 22 Apr 2025 13:53:15 -0700 Subject: [PATCH 01/24] feat(s2): coachmarks (#7590) * GW coachmark * fix TS errors * remove unrelated files, update yarn.lock * remove unneeded import * lint fix * more lint fixes * remove old stories * remove unused import * fix yarn lock * API changes to bring in line with other s2 components * Cleanup to get merged * fix lint and tests * remove from exports entirely --------- Co-authored-by: Robert Snow Co-authored-by: Robert Snow --- packages/@react-spectrum/s2/package.json | 2 + packages/@react-spectrum/s2/src/CoachMark.tsx | 530 ++++++++++++++++++ .../s2/stories/CoachMark.stories.tsx | 201 +++++++ .../s2/test/CoachMark.test.tsx | 78 +++ yarn.lock | 2 + 5 files changed, 813 insertions(+) create mode 100644 packages/@react-spectrum/s2/src/CoachMark.tsx create mode 100644 packages/@react-spectrum/s2/stories/CoachMark.stories.tsx create mode 100644 packages/@react-spectrum/s2/test/CoachMark.test.tsx diff --git a/packages/@react-spectrum/s2/package.json b/packages/@react-spectrum/s2/package.json index 25c7e00c451..c631e2beaf8 100644 --- a/packages/@react-spectrum/s2/package.json +++ b/packages/@react-spectrum/s2/package.json @@ -135,9 +135,11 @@ "@react-aria/i18n": "^3.12.8", "@react-aria/interactions": "^3.25.0", "@react-aria/live-announcer": "^3.4.2", + "@react-aria/overlays": "^3.27.0", "@react-aria/utils": "^3.28.2", "@react-spectrum/utils": "^3.12.4", "@react-stately/layout": "^4.2.2", + "@react-stately/menu": "^3.9.3", "@react-stately/utils": "^3.10.6", "@react-types/dialog": "^3.5.17", "@react-types/grid": "^3.3.1", diff --git a/packages/@react-spectrum/s2/src/CoachMark.tsx b/packages/@react-spectrum/s2/src/CoachMark.tsx new file mode 100644 index 00000000000..4edb0a55c84 --- /dev/null +++ b/packages/@react-spectrum/s2/src/CoachMark.tsx @@ -0,0 +1,530 @@ + +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import {ActionMenuContext} from './ActionMenu'; +import { + DialogTriggerProps as AriaDialogTriggerProps, + Popover as AriaPopover, + ContextValue, + DEFAULT_SLOT, + DialogContext, + OverlayTriggerStateContext, + PopoverContext, + PopoverProps, + Provider, + RootMenuTriggerStateContext, + useContextProps +} from 'react-aria-components'; +import {ButtonContext} from './Button'; +import {Card} from './Card'; +import {CheckboxContext} from './Checkbox'; +import {colorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {ColorSchemeContext} from './Provider'; +import {ContentContext, FooterContext, KeyboardContext, TextContext} from './Content'; +import { + createContext, + ForwardedRef, + forwardRef, + ReactNode, + useContext, + useRef +} from 'react'; +import {DividerContext} from './Divider'; +import {forwardRefType} from './types'; +import {ImageContext} from './Image'; +import {ImageCoordinator} from './ImageCoordinator'; +import {keyframes, raw} from '../style/style-macro' with {type: 'macro'}; +import {mergeStyles} from '../style/runtime'; +import {PressResponder} from '@react-aria/interactions'; +import {SliderContext} from './Slider'; +import {space, style} from '../style' with {type: 'macro'}; +import {useId, useObjectRef, useOverlayTrigger} from 'react-aria'; +import {useLayoutEffect} from '@react-aria/utils'; +import {useMenuTriggerState} from '@react-stately/menu'; + +export interface CoachMarkProps extends Omit, StyleProps { + /** The children of the coach mark. */ + children: ReactNode, + + size?: 'S' | 'M' | 'L' | 'XL' +} + +const fadeKeyframes = keyframes(` + from { + opacity: 0; + } + + to { + opacity: 1; + } +`); +const slideUpKeyframes = keyframes(` + from { + transform: translateY(-4px); + } + + to { + transform: translateY(0); + } +`); +const slideDownKeyframes = keyframes(` + from { + transform: translateY(4px); + } + + to { + transform: translateY(0); + } +`); +const slideRightKeyframes = keyframes(` + from { + transform: translateX(4px); + } + + to { + transform: translateX(0); + } +`); +const slideLeftKeyframes = keyframes(` + from { + transform: translateX(-4px); + } + + to { + transform: translateX(0); + } +`); + +let popover = style({ + ...colorScheme(), + '--s2-container-bg': { + type: 'backgroundColor', + value: 'layer-2' + }, + backgroundColor: '--s2-container-bg', + borderRadius: 'lg', + filter: { + isArrowShown: 'elevated' + }, + // Use box-shadow instead of filter when an arrow is not shown. + // This fixes the shadow stacking problem with submenus. + boxShadow: { + default: 'elevated', + isArrowShown: 'none' + }, + borderStyle: 'solid', + borderWidth: 1, + borderColor: { + default: 'gray-200', + forcedColors: 'ButtonBorder' + }, + width: { + size: { + // Copied from designs, not sure if correct. + S: 336, + M: 416, + L: 576 + } + }, + // Don't be larger than full screen minus 2 * containerPadding + maxWidth: '[calc(100vw - 24px)]', + boxSizing: 'border-box', + translateY: { + placement: { + bottom: { + isArrowShown: 8 // TODO: not defined yet should this change with font size? need boolean support for 'hideArrow' prop + }, + top: { + isArrowShown: -8 + } + } + }, + translateX: { + placement: { + left: { + isArrowShown: -8 + }, + right: { + isArrowShown: 8 + } + } + }, + animation: { + placement: { + top: { + isEntering: `${slideDownKeyframes}, ${fadeKeyframes}`, + isExiting: `${slideDownKeyframes}, ${fadeKeyframes}` + }, + bottom: { + isEntering: `${slideUpKeyframes}, ${fadeKeyframes}`, + isExiting: `${slideUpKeyframes}, ${fadeKeyframes}` + }, + left: { + isEntering: `${slideRightKeyframes}, ${fadeKeyframes}`, + isExiting: `${slideRightKeyframes}, ${fadeKeyframes}` + }, + right: { + isEntering: `${slideLeftKeyframes}, ${fadeKeyframes}`, + isExiting: `${slideLeftKeyframes}, ${fadeKeyframes}` + } + }, + isSubmenu: { + isEntering: fadeKeyframes, + isExiting: fadeKeyframes + } + }, + animationDuration: { + isEntering: 200, + isExiting: 200 + }, + animationDirection: { + isEntering: 'normal', + isExiting: 'reverse' + }, + animationTimingFunction: { + isExiting: 'in' + }, + transition: '[opacity, transform]', + willChange: '[opacity, transform]', + isolation: 'isolate', + pointerEvents: { + isExiting: 'none' + } +}, getAllowedOverrides()); + +const image = style({ + width: 'full', + aspectRatio: '[3/2]', + objectFit: 'cover', + userSelect: 'none', + pointerEvents: 'none' +}); + +let title = style({ + font: 'title', + fontSize: { + size: { + XS: 'title-xs', + S: 'title-xs', + M: 'title-sm', + L: 'title', + XL: 'title-lg' + } + }, + lineClamp: 3, + gridArea: 'title' +}); + +let description = style({ + font: 'body', + fontSize: { + size: { + XS: 'body-2xs', + S: 'body-2xs', + M: 'body-xs', + L: 'body-sm', + XL: 'body' + } + }, + lineClamp: 3, + gridArea: 'description' +}); + +let keyboard = style({ + gridArea: 'keyboard', + font: 'ui', + fontWeight: 'light', + color: 'gray-600', + background: 'gray-25', + unicodeBidi: 'plaintext' +}); + +let steps = style({ + font: 'detail', + fontSize: 'detail-sm', + alignSelf: 'center' +}); + +let content = style({ + display: 'grid', + // By default, all elements are displayed in a stack. + // If an action menu is present, place it next to the title. + gridTemplateColumns: { + default: ['1fr'], + ':has([data-slot=menu])': ['minmax(0, 1fr)', 'auto'] + }, + gridTemplateAreas: { + default: [ + 'title keyboard', + 'description keyboard' + ], + ':has([data-slot=menu])': [ + 'title menu', + 'keyboard keyboard', + 'description description' + ] + }, + columnGap: 4, + flexGrow: 1, + alignItems: 'baseline', + alignContent: 'space-between', + rowGap: { + size: { + XS: 4, + S: 4, + M: space(6), + L: space(6), + XL: 8 + } + }, + paddingTop: { + default: '--card-spacing', + ':first-child': 0 + }, + paddingBottom: { + default: '[calc(var(--card-spacing) * 1.5 / 2)]', + ':last-child': 0 + } +}); + +let actionMenu = style({ + gridArea: 'menu', + // Don't cause the row to expand, preserve gap between title and description text. + // Would use -100% here but it doesn't work in Firefox. + marginY: '[calc(-1 * self(height))]' +}); + +let footer = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'end', + justifyContent: 'space-between', + gap: 8, + paddingTop: '[calc(var(--card-spacing) * 1.5 / 2)]' +}); + +const actionButtonSize = { + XS: 'XS', + S: 'XS', + M: 'S', + L: 'M', + XL: 'L' +} as const; + +export const CoachMarkContext = createContext>({}); + +export const CoachMark = forwardRef((props: CoachMarkProps, ref: ForwardedRef) => { + let colorScheme = useContext(ColorSchemeContext); + [props, ref] = useContextProps(props, ref, CoachMarkContext); + let {UNSAFE_style} = props; + let {size = 'M'} = props; + let popoverRef = useObjectRef(ref); + + let children = ( + + + {props.children} + + + ); + + return ( + mergeStyles(popover({...renderProps, colorScheme}))}> + + {/* }// Reset OverlayTriggerStateContext so the buttons inside the dialog don't retain their hover state. */} + + {children} + + + + ); +}); + + +export interface CoachMarkTriggerProps extends AriaDialogTriggerProps { +} + +/** + * DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's + * open state with the trigger's press state. Additionally, it allows you to customize the type and + * positioning of the Dialog. + */ +export function CoachMarkTrigger(props: CoachMarkTriggerProps): ReactNode { + let triggerRef = useRef(null); + // Use useMenuTriggerState instead of useOverlayTriggerState in case a menu is embedded in the dialog. + // This is needed to handle submenus. + let state = useMenuTriggerState(props); + + let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, triggerRef); + + // Label dialog by the trigger as a fallback if there is no title slot. + // This is done in RAC instead of hooks because otherwise we cannot distinguish + // between context and props. Normally aria-labelledby overrides the title + // but when sent by context we want the title to win. + triggerProps.id = useId(); + overlayProps['aria-labelledby'] = triggerProps.id; + + + return ( + + + + {props.children} + + + + ); +} + + +// TODO better way to calculate 4px transform? (not 4%?) +const pulseAnimation = keyframes(` + 0% { + box-shadow: 0 0 0 4px rgba(20, 115, 230, 0.40); + transform: scale(calc(100%)); + } + 50% { + box-shadow: 0 0 0 10px rgba(20, 115, 230, 0.20); + transform: scale(104%); + } + 100% { + box-shadow: 0 0 0 4px rgba(20, 115, 230, 0.40); + transform: scale(calc(100%)); + } +`); + + +const indicator = style({ + animationDuration: 1000, + animationIterationCount: 'infinite', + animationFillMode: 'forwards', + animationTimingFunction: 'in-out', + position: 'relative', + '--activeElement': { + type: 'outlineColor', + value: { + default: 'focus-ring', + forcedColors: 'Highlight' + } + }, + '--borderOffset': { + type: 'top', + value: { + default: '[-2px]', + ':has([data-trigger=checkbox])': '[-6px]', + ':has([data-trigger=slider])': '[-8px]', + offset: { + M: '[-6px]', + L: '[-8px]' + } + } + }, + '--ringRadius': { + type: 'top', // is there a generic for pixel values? + value: { + default: '[10px]', + ':has([data-trigger=button])': '[18px]', + ':has([data-trigger=checkbox])': '[6px]' + } + } +}); + +const pulse = raw(`&:before { content: ""; display: inline-block; position: absolute; top: var(--borderOffset); bottom: var(--borderOffset); left: var(--borderOffset); right: var(--borderOffset); border-radius: var(--ringRadius); outline-style: solid; outline-color: var(--activeElement); outline-width: 4px; animation-duration: 2s; animation-iteration-count: infinite; animation-timing-function: ease-in-out; animation-fill-mode: forwards; animation-name: ${pulseAnimation}}`); + +interface CoachMarkIndicatorProps { + children: ReactNode, + isActive?: boolean +} +export const CoachMarkIndicator = /*#__PURE__*/ (forwardRef as forwardRefType)(function CoachMarkIndicator(props: CoachMarkIndicatorProps, ref: ForwardedRef) { + const {children, isActive} = props; + let objRef = useObjectRef(ref); + + // This is very silly... better ways? can't use display: contents because it breaks positioning + // this will break if there is a resize or different styles + useLayoutEffect(() => { + if (objRef.current) { + let styles = getComputedStyle(objRef.current.children[0]); + let childDisplay = styles.getPropertyValue('display'); + let childMaxWidth = styles.getPropertyValue('max-width'); + let childMaxHeight = styles.getPropertyValue('max-height'); + let childWidth = styles.getPropertyValue('width'); + let childHeight = styles.getPropertyValue('height'); + let childMinWidth = styles.getPropertyValue('min-width'); + let childMinHeight = styles.getPropertyValue('min-height'); + objRef.current.style.display = childDisplay; + objRef.current.style.maxWidth = childMaxWidth; + objRef.current.style.maxHeight = childMaxHeight; + objRef.current.style.width = childWidth; + objRef.current.style.height = childHeight; + objRef.current.style.minWidth = childMinWidth; + objRef.current.style.minHeight = childMinHeight; + } + }, [children]); + + return ( +
+ + {children} + +
+ ); +}); diff --git a/packages/@react-spectrum/s2/stories/CoachMark.stories.tsx b/packages/@react-spectrum/s2/stories/CoachMark.stories.tsx new file mode 100644 index 00000000000..3c213163383 --- /dev/null +++ b/packages/@react-spectrum/s2/stories/CoachMark.stories.tsx @@ -0,0 +1,201 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { + ActionButton, + ActionMenu, + Button, + CardPreview, + Checkbox, + Content, + Footer, + Image, + Keyboard, + MenuItem, + Slider, + Text +} from '../src'; +import {CoachMark, CoachMarkTrigger} from '../src/CoachMark'; +import Filter from '../s2wf-icons/S2_Icon_Filter_20_N.svg'; +import type {Meta, StoryObj} from '@storybook/react'; +import {style} from '../style' with {type: 'macro'}; +import {useState} from 'react'; + +const meta: Meta = { + component: CoachMark, + parameters: { + layout: 'centered' + }, + argTypes: { + placement: { + control: 'radio', + options: ['top', 'left', 'left top', 'right', 'right top', 'bottom'] + } + }, + title: 'CoachMark' +}; + +export default meta; +type Story = StoryObj; + +export const CoachMarkExample: Story = { + render: (args) => ( +
+ + + Sync with CC + + + + + + Hello + + Skip tour + Restart tour + + Command + B + This is the description + +
+ 1 of 10 + + +
+
+
+ +
+ ), + parameters: { + docs: { + disable: true + } + } +}; + +function ControlledCoachMark(args) { + let [isOpen, setIsOpen] = useState(false); + + return ( +
+ + + Sync with CC + + + + + + Hello + + Skip tour + Restart tour + + Command + B + This is the description + +
+ 1 of 10 + + +
+
+
+ +
+ ); +} + +export const CoachMarkRestartable: Story = { + render: (args) => ( + + ), + parameters: { + docs: { + disable: true + } + } +}; + +export const CoachMarkSlider: Story = { + render: (args) => ( +
+ + + + + + + + + Hello + + Skip tour + Restart tour + + Command + B + This is the description + +
+ 1 of 10 + + +
+
+
+ +
+ ), + parameters: { + docs: { + disable: true + } + } +}; + +export const CoachMarkButton: Story = { + render: (args) => ( +
+ + + + + + + + + + + Hello + + Skip tour + Restart tour + + Command + B + This is the description + +
+ 1 of 10 + + +
+
+
+ +
+ ), + parameters: { + docs: { + disable: true + } + } +}; diff --git a/packages/@react-spectrum/s2/test/CoachMark.test.tsx b/packages/@react-spectrum/s2/test/CoachMark.test.tsx new file mode 100644 index 00000000000..6814ac9d717 --- /dev/null +++ b/packages/@react-spectrum/s2/test/CoachMark.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; +import { + ActionMenu, + Button, + CardPreview, + Checkbox, + Content, + Footer, + Image, + Keyboard, + MenuItem, + Text +} from '../src'; +import {CoachMark, CoachMarkTrigger} from '../src/CoachMark'; +import React from 'react'; +import userEvent, {UserEvent} from '@testing-library/user-event'; + +const mockAnimations = () => { + Element.prototype.animate = jest.fn().mockImplementation(() => ({finished: Promise.resolve()})); +}; + +describe('CoachMark', () => { + let user: UserEvent | null = null; + beforeAll(() => { + jest.useFakeTimers(); + mockAnimations(); + }); + beforeEach(() => { + user = userEvent.setup({delay: null, pointerMap}); + }); + afterAll(() => { + act(() => {jest.runAllTimers();}); + }); + + it('renders a coachmark', async () => { + let onPress = jest.fn(); + let {getAllByRole} = render( + + Sync with CC + + + + + + Hello + + Skip tour + Restart tour + + Command + B + This is the description + +
+ 1 of 10 + + +
+
+
+ ); + act(() => {jest.runAllTimers();}); + expect(getAllByRole('button').length).toBe(4); // 2 Dismiss + 2 actions + await user?.click(getAllByRole('button')[2]); + expect(onPress).toHaveBeenCalled(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 1908468e5ff..6dae49af9c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7943,10 +7943,12 @@ __metadata: "@react-aria/i18n": "npm:^3.12.8" "@react-aria/interactions": "npm:^3.25.0" "@react-aria/live-announcer": "npm:^3.4.2" + "@react-aria/overlays": "npm:^3.27.0" "@react-aria/test-utils": "npm:1.0.0-alpha.3" "@react-aria/utils": "npm:^3.28.2" "@react-spectrum/utils": "npm:^3.12.4" "@react-stately/layout": "npm:^4.2.2" + "@react-stately/menu": "npm:^3.9.3" "@react-stately/utils": "npm:^3.10.6" "@react-types/dialog": "npm:^3.5.17" "@react-types/grid": "npm:^3.3.1" From 2e9061df69eb17f17b0fb7d64d318c2835aed145 Mon Sep 17 00:00:00 2001 From: Kyle Taborski Date: Wed, 23 Apr 2025 15:20:07 -0700 Subject: [PATCH 02/24] fix: spelling mistake in style macros list-style-type (#8124) --- packages/@react-spectrum/s2/style/spectrum-theme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 38ff6ce7b1a..4193e2c85a4 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -789,7 +789,7 @@ export const style = createTheme({ ':lang(ja, ko, zh, zh-Hant, zh-Hans)': getToken('code-cjk-line-height') } }, - listStyleType: ['none', 'dist', 'decimal'] as const, + listStyleType: ['none', 'disc', 'decimal'] as const, listStylePosition: ['inside', 'outside'] as const, textTransform: ['uppercase', 'lowercase', 'capitalize', 'none'] as const, textAlign: ['start', 'center', 'end', 'justify'] as const, From 9fe5629838c758b86d9fae2e99266518f9ebe0e0 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Sat, 26 Apr 2025 08:24:09 +1000 Subject: [PATCH 03/24] chore: add portal provider to react-aria monopackage (#8135) --- packages/react-aria/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-aria/src/index.ts b/packages/react-aria/src/index.ts index 09f001db48b..bc63fd9515a 100644 --- a/packages/react-aria/src/index.ts +++ b/packages/react-aria/src/index.ts @@ -31,7 +31,7 @@ export {useListBox, useListBoxSection, useOption} from '@react-aria/listbox'; export {useMenu, useMenuItem, useMenuSection, useMenuTrigger, useSubmenuTrigger} from '@react-aria/menu'; export {useMeter} from '@react-aria/meter'; export {useNumberField} from '@react-aria/numberfield'; -export {DismissButton, ModalProvider, Overlay, OverlayContainer, OverlayProvider, useModal, useModalOverlay, useModalProvider, useOverlay, useOverlayPosition, useOverlayTrigger, usePopover, usePreventScroll} from '@react-aria/overlays'; +export {DismissButton, ModalProvider, Overlay, OverlayContainer, OverlayProvider, useModal, useModalOverlay, useModalProvider, useOverlay, useOverlayPosition, useOverlayTrigger, usePopover, usePreventScroll, UNSAFE_PortalProvider} from '@react-aria/overlays'; export {useProgressBar} from '@react-aria/progress'; export {useRadio, useRadioGroup} from '@react-aria/radio'; export {useSearchField} from '@react-aria/searchfield'; From 63d4359d6c7073033dbd000e4bddba1ab342ddb2 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Sat, 26 Apr 2025 08:33:42 +1000 Subject: [PATCH 04/24] chore: TS update 5.8 (#8061) --- .../@types-node-npm-20.14.13-41f92d384c.patch | 13 +++++++ lib/viewTransitions.d.ts | 5 --- package.json | 7 ++-- .../dnd/src/useDroppableCollection.ts | 29 +++++++++------ .../grid/src/useGridSelectionAnnouncement.ts | 19 ++++++---- .../calendar/stories/Calendar.stories.tsx | 2 +- .../datepicker/stories/DateField.stories.tsx | 4 +-- .../datepicker/stories/DatePicker.stories.tsx | 2 +- .../stories/DateRangePicker.stories.tsx | 2 +- .../form/stories/Form.stories.tsx | 2 +- .../@react-spectrum/s2/src/Breadcrumbs.tsx | 12 ++++--- .../@react-spectrum/s2/src/CloseButton.tsx | 2 +- packages/@react-spectrum/s2/src/ComboBox.tsx | 3 +- packages/@react-spectrum/s2/src/Menu.tsx | 7 ++-- .../@react-spectrum/s2/src/NumberField.tsx | 20 +++-------- .../s2/src/SegmentedControl.tsx | 23 ++++++------ packages/@react-spectrum/s2/src/Slider.tsx | 7 ++-- packages/@react-spectrum/s2/src/TableView.tsx | 9 +++-- packages/@react-spectrum/s2/src/Tabs.tsx | 14 +++++--- .../@react-spectrum/s2/src/TabsPicker.tsx | 4 +-- packages/@react-spectrum/s2/src/TagGroup.tsx | 3 +- .../s2/style/spectrum-theme.ts | 3 +- .../steplist/stories/StepList.stories.tsx | 4 +-- packages/@react-spectrum/tabs/src/Tabs.tsx | 11 ++++-- .../tabs/stories/Tabs.stories.tsx | 9 +++-- .../disclosure/src/useDisclosureGroupState.ts | 11 +++--- .../tabs/src/useTabListState.ts | 5 +++ .../@react-types/shared/src/selection.d.ts | 2 +- packages/@react-types/tabs/src/index.d.ts | 12 ++++--- packages/dev/codemods/package.json | 4 +-- .../dev/docs/pages/react-aria/home/A11y.tsx | 2 +- .../react-aria-components/docs/Checkbox.mdx | 2 +- .../react-aria-components/docs/GridList.mdx | 2 +- packages/react-aria-components/docs/Menu.mdx | 2 +- packages/react-aria-components/docs/Table.mdx | 2 +- .../react-aria-components/docs/TagGroup.mdx | 2 +- packages/react-aria-components/docs/Tree.mdx | 2 +- scripts/extractExamples.mjs | 1 - yarn.lock | 36 ++++++++++++++----- 39 files changed, 184 insertions(+), 117 deletions(-) create mode 100644 .yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch diff --git a/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch b/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch new file mode 100644 index 00000000000..7afc48f1b36 --- /dev/null +++ b/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch @@ -0,0 +1,13 @@ +diff --git a/buffer.d.ts b/buffer.d.ts +index 5d6c97d6b5d47fd189f795498aefd6b8d7713b7d..b9a22c4634fa6308006ae17d3527ff3c518a789d 100644 +--- a/buffer.d.ts ++++ b/buffer.d.ts +@@ -629,7 +629,7 @@ declare module "buffer" { + */ + poolSize: number; + } +- interface Buffer extends Uint8Array { ++ interface Buffer extends Uint8Array { + /** + * Writes `string` to `buf` at `offset` according to the character encoding in`encoding`. The `length` parameter is the number of bytes to write. If `buf` did + * not contain enough space to fit the entire string, only part of `string` will be diff --git a/lib/viewTransitions.d.ts b/lib/viewTransitions.d.ts index d0ff07d05f0..b23422f436f 100644 --- a/lib/viewTransitions.d.ts +++ b/lib/viewTransitions.d.ts @@ -13,8 +13,3 @@ interface Document { startViewTransition(update: (() => void) | {update: () => void, types: string[]}): ViewTransition; } - -interface ViewTransition { - ready: Promise; - finished: Promise; -} diff --git a/package.json b/package.json index 0017e10be7f..9a4d516a881 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "tailwindcss": "^4.0.0", "tailwindcss-animate": "^1.0.7", "tempy": "^0.5.0", - "typescript": "^5.5.0", + "typescript": "^5.8.2", "typescript-eslint": "^8.9.0", "verdaccio": "^6.0.0", "walk-object": "^4.0.0", @@ -234,7 +234,10 @@ "recast": "0.23.6", "ast-types": "0.16.1", "svgo": "^3", - "@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch" + "@testing-library/user-event": "patch:@testing-library/user-event@npm%3A14.6.1#~/.yarn/patches/@testing-library-user-event-npm-14.6.1-5da7e1d4e2.patch", + "@types/node@npm:*": "patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch", + "@types/node@npm:^18.0.0": "patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch", + "@types/node@npm:>= 8": "patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch" }, "@parcel/transformer-css": { "cssModules": { diff --git a/packages/@react-aria/dnd/src/useDroppableCollection.ts b/packages/@react-aria/dnd/src/useDroppableCollection.ts index 234793a78e6..53d409e9e5b 100644 --- a/packages/@react-aria/dnd/src/useDroppableCollection.ts +++ b/packages/@react-aria/dnd/src/useDroppableCollection.ts @@ -260,18 +260,25 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: // inserted item. If selection is disabled, then also show the focus ring so there // is some indication that items were added. if (state.selectionManager.focusedKey === prevFocusedKey) { - let first = newKeys.keys().next().value; - let item = state.collection.getItem(first); - - // If this is a cell, focus the parent row. - if (item?.type === 'cell') { - first = item.parentKey; - } + let first: Key | null | undefined = newKeys.keys().next().value; + if (first != null) { + let item = state.collection.getItem(first); + + // If this is a cell, focus the parent row. + // eslint-disable-next-line max-depth + if (item?.type === 'cell') { + first = item.parentKey; + } - state.selectionManager.setFocusedKey(first); + // eslint-disable-next-line max-depth + if (first != null) { + state.selectionManager.setFocusedKey(first); + } - if (state.selectionManager.selectionMode === 'none') { - setInteractionModality('keyboard'); + // eslint-disable-next-line max-depth + if (state.selectionManager.selectionMode === 'none') { + setInteractionModality('keyboard'); + } } } } else if ( @@ -337,7 +344,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: }, 50); }, [localState, defaultOnDrop, ref, updateFocusAfterDrop]); - + useEffect(() => { return () => { if (droppingState.current) { diff --git a/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts b/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts index 122aa695a6b..c3d84564c32 100644 --- a/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts +++ b/packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts @@ -61,20 +61,25 @@ export function useGridSelectionAnnouncement(props: GridSelectionAnnouncement let messages: string[] = []; if ((state.selectionManager.selectedKeys.size === 1 && isReplace)) { - if (state.collection.getItem(state.selectionManager.selectedKeys.keys().next().value)) { - let currentSelectionText = getRowText(state.selectionManager.selectedKeys.keys().next().value); + let firstKey = state.selectionManager.selectedKeys.keys().next().value; + if (firstKey != null && state.collection.getItem(firstKey)) { + let currentSelectionText = getRowText(firstKey); if (currentSelectionText) { messages.push(stringFormatter.format('selectedItem', {item: currentSelectionText})); } } } else if (addedKeys.size === 1 && removedKeys.size === 0) { - let addedText = getRowText(addedKeys.keys().next().value); - if (addedText) { - messages.push(stringFormatter.format('selectedItem', {item: addedText})); + let firstKey = addedKeys.keys().next().value; + if (firstKey != null) { + let addedText = getRowText(firstKey); + if (addedText) { + messages.push(stringFormatter.format('selectedItem', {item: addedText})); + } } } else if (removedKeys.size === 1 && addedKeys.size === 0) { - if (state.collection.getItem(removedKeys.keys().next().value)) { - let removedText = getRowText(removedKeys.keys().next().value); + let firstKey = removedKeys.keys().next().value; + if (firstKey != null && state.collection.getItem(firstKey)) { + let removedText = getRowText(firstKey); if (removedText) { messages.push(stringFormatter.format('deselectedItem', {item: removedText})); } diff --git a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx index 5da987a336d..d12df3d13e3 100644 --- a/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx +++ b/packages/@react-spectrum/calendar/stories/Calendar.stories.tsx @@ -209,7 +209,7 @@ const calendars = [ function Example(props) { let [locale, setLocale] = React.useState(''); - let [calendar, setCalendar] = React.useState(calendars[0].key); + let [calendar, setCalendar] = React.useState(calendars[0].key); let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale)!; diff --git a/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx index 682e5484224..3a495dd5ba6 100644 --- a/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DateField.stories.tsx @@ -212,7 +212,7 @@ export const IsDateUnavailable: DateFieldStory = { ...Default, args: { isDateUnavailable: (date) => { - return date.compare(new CalendarDate(1980, 1, 1)) >= 0 + return date.compare(new CalendarDate(1980, 1, 1)) >= 0 && date.compare(new CalendarDate(1980, 1, 8)) <= 0; }, errorMessage: 'Date unavailable.', @@ -310,7 +310,7 @@ const calendars = [ function Example(props) { let [locale, setLocale] = React.useState(''); - let [calendar, setCalendar] = React.useState(calendars[0].key); + let [calendar, setCalendar] = React.useState(calendars[0].key); let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); diff --git a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx index e51901897ee..f53d6ae8a6f 100644 --- a/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DatePicker.stories.tsx @@ -338,7 +338,7 @@ const calendars = [ function Example(props) { let [locale, setLocale] = React.useState(''); - let [calendar, setCalendar] = React.useState(calendars[0].key); + let [calendar, setCalendar] = React.useState(calendars[0].key); let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); diff --git a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx index 5ac199a8194..b71709b0f80 100644 --- a/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx +++ b/packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx @@ -238,7 +238,7 @@ const calendars = [ function Example(props) { let [locale, setLocale] = React.useState(''); - let [calendar, setCalendar] = React.useState(calendars[0].key); + let [calendar, setCalendar] = React.useState(calendars[0].key); let {locale: defaultLocale} = useLocale(); let pref = preferences.find(p => p.locale === locale); diff --git a/packages/@react-spectrum/form/stories/Form.stories.tsx b/packages/@react-spectrum/form/stories/Form.stories.tsx index 131f5592b4f..e154e5efad7 100644 --- a/packages/@react-spectrum/form/stories/Form.stories.tsx +++ b/packages/@react-spectrum/form/stories/Form.stories.tsx @@ -482,7 +482,7 @@ function FormWithControls(props: any = {}) { let [firstName, setFirstName] = useState('hello'); let [isHunter, setIsHunter] = useState(true); let [favoritePet, setFavoritePet] = useState('cats'); - let [favoriteColor, setFavoriteColor] = useState('green' as Key); + let [favoriteColor, setFavoriteColor] = useState('green'); let [howIFeel, setHowIFeel] = useState('I feel good, o I feel so good!'); let [birthday, setBirthday] = useState(new CalendarDate(1732, 2, 22)); let [money, setMoney] = useState(50); diff --git a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx index be0f1cdacf3..a8f696b0bf9 100644 --- a/packages/@react-spectrum/s2/src/Breadcrumbs.tsx +++ b/packages/@react-spectrum/s2/src/Breadcrumbs.tsx @@ -20,6 +20,7 @@ import { DefaultCollectionRenderer, HeadingContext, Link, + LinkRenderProps, Provider, Breadcrumbs as RACBreadcrumbs } from 'react-aria-components'; @@ -97,7 +98,7 @@ const wrapper = style({ const InternalBreadcrumbsContext = createContext>>({}); -/** Breadcrumbs show hierarchy and navigational context for a user’s location within an application. */ +/** Breadcrumbs show hierarchy and navigational context for a user's location within an application. */ export const Breadcrumbs = /*#__PURE__*/ (forwardRef as forwardRefType)(function Breadcrumbs(props: BreadcrumbsProps, ref: DOMRef) { [props, ref] = useSpectrumContextProps(props, ref, BreadcrumbsContext); let domRef = useDOMRef(ref); @@ -200,7 +201,7 @@ let HiddenBreadcrumbs = function (props: {listRef: RefObject({ display: 'flex', alignItems: 'center', justifyContent: 'start', @@ -245,7 +246,7 @@ const chevronStyles = style({ } }); -const linkStyles = style({ +const linkStyles = style({ ...focusRing(), borderRadius: 'sm', font: 'control', @@ -255,7 +256,8 @@ const linkStyles = style({ isCurrent: 'neutral', forcedColors: { default: 'LinkText', - isDisabled: 'GrayText' + isDisabled: 'GrayText', + isCurrent: 'GrayText' } }, transition: 'default', @@ -337,7 +339,7 @@ export const Breadcrumb = /*#__PURE__*/ (forwardRef as forwardRefType)(function ping={ping} referrerPolicy={referrerPolicy} isDisabled={isDisabled || isCurrent} - className={({isFocused, isFocusVisible, isHovered, isDisabled, isPressed}) => linkStyles({isFocused, isFocusVisible, isHovered, isDisabled, size, isPressed})}> + className={({isFocused, isFocusVisible, isHovered, isDisabled, isPressed}) => linkStyles({isFocused, isFocusVisible, isHovered, isDisabled, size, isPressed, isCurrent})}> {children} ({ ...focusRing(), ...staticColor(), display: 'flex', diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 1a755084b22..99439d91e38 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -16,6 +16,7 @@ import { ListBoxSection as AriaListBoxSection, PopoverProps as AriaPopoverProps, Button, + ButtonRenderProps, ContextValue, InputContext, ListBox, @@ -95,7 +96,7 @@ export interface ComboBoxProps extends export const ComboBoxContext = createContext>, TextFieldRef>>(null); -const inputButton = style({ +const inputButton = style({ display: 'flex', outlineStyle: 'none', textAlign: 'center', diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 8872ce1406c..9d8bc4b9f4d 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -23,6 +23,7 @@ import { SubmenuTriggerProps as AriaSubmenuTriggerProps, ContextValue, DEFAULT_SLOT, + MenuItemRenderProps, Provider, Separator, SeparatorProps @@ -146,7 +147,7 @@ export let sectionHeading = style({ margin: 0 }); -export let menuitem = style({ +export let menuitem = style & {isFocused: boolean, size: 'S' | 'M' | 'L' | 'XL', isLink?: boolean, hasSubmenu?: boolean, isOpen?: boolean}>({ ...focusRing(), boxSizing: 'border-box', borderRadius: 'control', @@ -293,7 +294,7 @@ let value = style({ marginStart: 8 }); -let keyboard = style({ +let keyboard = style<{size: 'S' | 'M' | 'L' | 'XL', isDisabled: boolean}>({ gridArea: 'keyboard', marginStart: 8, font: 'ui', @@ -305,7 +306,7 @@ let keyboard = style({ isDisabled: 'GrayText' } }, - background: 'gray-25', + backgroundColor: 'gray-25', unicodeBidi: 'plaintext' }); diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index a18f70ac951..970a477b92b 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -16,6 +16,7 @@ import { NumberField as AriaNumberField, NumberFieldProps as AriaNumberFieldProps, ButtonContext, + ButtonRenderProps, ContextValue, InputContext, Text, @@ -56,7 +57,7 @@ export interface NumberFieldProps extends export const NumberFieldContext = createContext, TextFieldRef>>(null); -const inputButton = style({ +const inputButton = style({ display: 'flex', outlineStyle: 'none', textAlign: 'center', @@ -69,9 +70,6 @@ const inputButton = style({ L: '[5px]', XL: '[6px]' } - }, - type: { - decrementStep: 'none' } }, borderBottomRadius: { @@ -82,9 +80,6 @@ const inputButton = style({ L: '[5px]', XL: '[6px]' } - }, - type: { - incrementStep: 'none' } }, alignItems: 'center', @@ -132,13 +127,6 @@ const inputButton = style({ const iconStyles = style({ flexShrink: 0, - rotate: { - default: 0, - type: { - incrementStep: 270, - decrementStep: 90 - } - }, '--iconPrimary': { type: 'fill', value: 'currentColor' @@ -261,7 +249,7 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro type: 'decrement', size })}> - + - + } diff --git a/packages/@react-spectrum/s2/src/SegmentedControl.tsx b/packages/@react-spectrum/s2/src/SegmentedControl.tsx index f1828b7b92e..d412b098404 100644 --- a/packages/@react-spectrum/s2/src/SegmentedControl.tsx +++ b/packages/@react-spectrum/s2/src/SegmentedControl.tsx @@ -12,7 +12,7 @@ import {AriaLabelingProps, DOMRef, DOMRefValue, FocusableRef, Key} from '@react-types/shared'; import {centerBaseline} from './CenterBaseline'; -import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, SlotProps, ToggleButton, ToggleButtonGroup, ToggleGroupStateContext} from 'react-aria-components'; +import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, SlotProps, ToggleButton, ToggleButtonGroup, ToggleButtonRenderProps, ToggleGroupStateContext} from 'react-aria-components'; import {createContext, forwardRef, ReactNode, RefObject, useCallback, useContext, useRef} from 'react'; import {focusRing, space, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -62,7 +62,7 @@ const segmentedControl = style({ width: 'fit' }, getAllowedOverrides()); -const controlItem = style({ +const controlItem = style({ ...focusRing(), position: 'relative', display: 'flex', @@ -107,7 +107,7 @@ const controlItem = style({ } }, getAllowedOverrides()); -const slider = style({ +const slider = style<{isDisabled: boolean}>({ backgroundColor: { default: 'gray-25', forcedColors: { @@ -170,14 +170,17 @@ export const SegmentedControl = /*#__PURE__*/ forwardRef(function SegmentedContr if (currentSelectedRef.current) { prevRef.current = currentSelectedRef?.current.getBoundingClientRect(); } - + if (onSelectionChange) { - onSelectionChange(values.values().next().value); + let firstKey = values.values().next().value; + if (firstKey != null) { + onSelectionChange(firstKey); + } } }; return ( - + ]}> {props.children} ); @@ -255,15 +258,15 @@ export const SegmentedControlItem = /*#__PURE__*/ forwardRef(function SegmentedC }, [isSelected, reduceMotion]); return ( - (props.UNSAFE_className || '') + controlItem({...renderProps, isJustified}, props.styles)} > {({isSelected, isPressed, isDisabled}) => ( <> {isSelected &&
} - ({ ...focusRing(), display: 'inline-block', boxSizing: 'border-box', @@ -271,7 +272,7 @@ const trackStyling = { } } as const; -export let upperTrack = style({ +export let upperTrack = style<{isDisabled?: boolean, trackStyle: 'thin' | 'thick'}>({ ...trackStyling, position: 'absolute', backgroundColor: { @@ -292,7 +293,7 @@ export let upperTrack = style({ } }); -export let filledTrack = style({ +export let filledTrack = style<{isDisabled?: boolean, isEmphasized?: boolean, trackStyle: 'thin' | 'thick'}>({ ...trackStyling, position: 'absolute', backgroundColor: { diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index eeb558e975b..6d26eb34bc4 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -645,14 +645,13 @@ const resizerHandleContainer = style({ } }); -const resizerHandle = style({ +const resizerHandle = style<{isFocusVisible: boolean, isResizing: boolean}>({ backgroundColor: { default: 'gray-300', isFocusVisible: lightDark('informative-900', 'informative-700'), // --spectrum-informative-background-color-default, can't use `informative` because that will use the focusVisible version isResizing: lightDark('informative-900', 'informative-700'), forcedColors: { default: 'Background', - isHovered: 'ButtonBorder', isFocusVisible: 'Highlight', isResizing: 'Highlight' } @@ -802,7 +801,7 @@ function ColumnWithMenu(props: ColumnWithMenuProps) { resizerHandleContainer({resizableDirection, isResizing, isInResizeMode})}> {({isFocusVisible, isResizing}) => ( <> - + {(isFocusVisible || isInResizeMode) && isResizing &&
} )} @@ -813,9 +812,9 @@ function ColumnWithMenu(props: ColumnWithMenuProps) { ); } -function ResizerIndicator({isFocusVisible, isResizing, isInResizeMode}) { +function ResizerIndicator({isFocusVisible, isResizing}) { return ( -
+
); } diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 414b7316429..81b2a38b3a1 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -22,7 +22,8 @@ import { Tab as RACTab, TabList as RACTabList, Tabs as RACTabs, - TabListStateContext + TabListStateContext, + TabRenderProps } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared'; @@ -233,7 +234,7 @@ interface TabLineProps { density?: 'compact' | 'regular' } -const selectedIndicator = style({ +const selectedIndicator = style<{isDisabled: boolean, orientation?: Orientation}>({ position: 'absolute', backgroundColor: { default: 'neutral', @@ -320,7 +321,7 @@ function TabLine(props: TabLineProps) { ); } -const tab = style({ +const tab = style({ ...focusRing(), display: 'flex', color: { @@ -520,7 +521,12 @@ let HiddenTabs = function (props: { let TabsMenu = (props: {valueId: string, items: Array>, onSelectionChange: TabsProps['onSelectionChange']} & Omit) => { let {id, items, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, valueId} = props; - let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, labelBehavior} = useContext(InternalTabsContext); + let {density, onSelectionChange: _onSelectionChange, selectedKey, isDisabled, disabledKeys, labelBehavior} = useContext(InternalTabsContext); + let onSelectionChange = useCallback((key: Key | null) => { + if (key != null) { + _onSelectionChange?.(key); + } + }, [_onSelectionChange]); let state = useContext(TabListStateContext); let allKeysDisabled = useMemo(() => { return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set()); diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index f417bba8859..09987c91455 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -281,7 +281,7 @@ let _Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(Picker); export {_Picker as Picker}; -const selectedIndicator = style({ +const selectedIndicator = style<{isDisabled?: boolean}>({ backgroundColor: { default: 'neutral', isDisabled: 'disabled', @@ -297,7 +297,7 @@ const selectedIndicator = style({ transitionDuration: 130, transitionTimingFunction: 'in-out' }); -function TabLine(props) { +function TabLine(props: {isDisabled?: boolean}) { return
; } diff --git a/packages/@react-spectrum/s2/src/TagGroup.tsx b/packages/@react-spectrum/s2/src/TagGroup.tsx index 93fe47d3e94..b2aa19e19be 100644 --- a/packages/@react-spectrum/s2/src/TagGroup.tsx +++ b/packages/@react-spectrum/s2/src/TagGroup.tsx @@ -23,6 +23,7 @@ import { TextContext as RACTextContext, TagList, TagListProps, + TagRenderProps, useLocale, useSlottedContext } from 'react-aria-components'; @@ -437,7 +438,7 @@ function ActionGroup(props) { ); } -const tagStyles = style({ +const tagStyles = style({ ...focusRing(), display: 'inline-flex', boxSizing: 'border-box', diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 4193e2c85a4..0ef9c2b9d2f 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -967,7 +967,8 @@ export const style = createTheme({ // eslint-disable-next-line @typescript-eslint/no-unused-vars disableTapHighlight: createArbitraryProperty((_value: true) => ({ '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' - })) + })), + unicodeBidi: ['normal', 'embed', 'bidi-override', 'isolate', 'isolate-override', 'plaintext'] as const }, shorthands: { padding: ['paddingTop', 'paddingBottom', 'paddingStart', 'paddingEnd'] as const, diff --git a/packages/@react-spectrum/steplist/stories/StepList.stories.tsx b/packages/@react-spectrum/steplist/stories/StepList.stories.tsx index a3c2a55ae19..e18c57d3e08 100644 --- a/packages/@react-spectrum/steplist/stories/StepList.stories.tsx +++ b/packages/@react-spectrum/steplist/stories/StepList.stories.tsx @@ -219,8 +219,8 @@ export const ControlledStory: StepListStory = { }; function Controlled(args) { - const [lastCompletedStep, setLastCompletedStep] = useState(args.lastCompletedStep); - const [selectedKey, setSelectedKey] = useState(args.selectedKey); + const [lastCompletedStep, setLastCompletedStep] = useState(args.lastCompletedStep); + const [selectedKey, setSelectedKey] = useState(args.selectedKey); return ( diff --git a/packages/@react-spectrum/tabs/src/Tabs.tsx b/packages/@react-spectrum/tabs/src/Tabs.tsx index 9de494d5e1b..e6342c14bb7 100644 --- a/packages/@react-spectrum/tabs/src/Tabs.tsx +++ b/packages/@react-spectrum/tabs/src/Tabs.tsx @@ -377,12 +377,13 @@ function TabPanel(props: TabPanelProps) { ); } -interface TabPickerProps extends Omit, 'children'> { +interface TabPickerProps extends Omit, 'children' | 'onSelectionChange'> { density?: 'compact' | 'regular', isEmphasized?: boolean, state: TabListState, className?: string, - visible: boolean + visible: boolean, + onSelectionChange?: (key: Key) => void } function TabPicker(props: TabPickerProps) { @@ -450,7 +451,11 @@ function TabPicker(props: TabPickerProps) { isDisabled={!visible || isDisabled} selectedKey={state.selectedKey} disabledKeys={state.disabledKeys} - onSelectionChange={state.setSelectedKey} + onSelectionChange={key => { + if (key != null) { + state.setSelectedKey(key); + } + }} UNSAFE_className={classNames(styles, 'spectrum-Tabs-picker')}> {item => {item.rendered}} diff --git a/packages/@react-spectrum/tabs/stories/Tabs.stories.tsx b/packages/@react-spectrum/tabs/stories/Tabs.stories.tsx index 66b2c374313..ebc359adeea 100644 --- a/packages/@react-spectrum/tabs/stories/Tabs.stories.tsx +++ b/packages/@react-spectrum/tabs/stories/Tabs.stories.tsx @@ -20,7 +20,7 @@ import Dashboard from '@spectrum-icons/workflow/Dashboard'; import {Item, TabList, TabPanels, Tabs} from '..'; import {Key} from '@react-types/shared'; import {Picker} from '@react-spectrum/picker'; -import React, {ReactNode, useState} from 'react'; +import React, {ReactNode, useCallback, useState} from 'react'; import {RouterProvider} from '@react-aria/utils'; import {SpectrumTabsProps} from '@react-types/tabs'; import {TextField} from '@react-spectrum/textfield'; @@ -907,7 +907,12 @@ let DynamicTabsWithDecoration = (props = {}) => { }; let ControlledSelection = () => { - let [selectedKey, setSelectedKey] = useState('Tab 1'); + let [selectedKey, _setSelectedKey] = useState('Tab 1'); + let setSelectedKey = useCallback((key: Key | null) => { + if (key != null) { + _setSelectedKey(key); + } + }, [_setSelectedKey]); return (
diff --git a/packages/@react-stately/disclosure/src/useDisclosureGroupState.ts b/packages/@react-stately/disclosure/src/useDisclosureGroupState.ts index 81d04ae955b..ef880baa6e9 100644 --- a/packages/@react-stately/disclosure/src/useDisclosureGroupState.ts +++ b/packages/@react-stately/disclosure/src/useDisclosureGroupState.ts @@ -33,7 +33,7 @@ export interface DisclosureGroupState { /** Whether all items are disabled. */ readonly isDisabled: boolean, - + /** A set of keys for items that are expanded. */ readonly expandedKeys: Set, @@ -55,11 +55,14 @@ export function useDisclosureGroupState(props: DisclosureGroupProps): Disclosure useMemo(() => props.defaultExpandedKeys ? new Set(props.defaultExpandedKeys) : new Set(), [props.defaultExpandedKeys]), props.onExpandedChange ); - + useEffect(() => { // Ensure only one item is expanded if allowsMultipleExpanded is false. if (!allowsMultipleExpanded && expandedKeys.size > 1) { - setExpandedKeys(new Set([expandedKeys.values().next().value])); + let firstKey = expandedKeys.values().next().value; + if (firstKey != null) { + setExpandedKeys(new Set([firstKey])); + } } }); @@ -80,7 +83,7 @@ export function useDisclosureGroupState(props: DisclosureGroupProps): Disclosure } else { keys = new Set(expandedKeys.has(key) ? [] : [key]); } - + setExpandedKeys(keys); } }; diff --git a/packages/@react-stately/tabs/src/useTabListState.ts b/packages/@react-stately/tabs/src/useTabListState.ts index c9bc6c3c23d..116e39b059f 100644 --- a/packages/@react-stately/tabs/src/useTabListState.ts +++ b/packages/@react-stately/tabs/src/useTabListState.ts @@ -29,6 +29,11 @@ export interface TabListState extends SingleSelectListState { export function useTabListState(props: TabListStateOptions): TabListState { let state = useSingleSelectListState({ ...props, + onSelectionChange: props.onSelectionChange ? (key => { + if (key != null) { + props.onSelectionChange?.(key); + } + }) : undefined, suppressTextValueWarning: true, defaultSelectedKey: props.defaultSelectedKey ?? findDefaultSelectedKey(props.collection, props.disabledKeys ? new Set(props.disabledKeys) : new Set()) ?? undefined }); diff --git a/packages/@react-types/shared/src/selection.d.ts b/packages/@react-types/shared/src/selection.d.ts index e6b89f6ba39..e59aaf3e435 100644 --- a/packages/@react-types/shared/src/selection.d.ts +++ b/packages/@react-types/shared/src/selection.d.ts @@ -20,7 +20,7 @@ export interface SingleSelection { /** The initial selected key in the collection (uncontrolled). */ defaultSelectedKey?: Key, /** Handler that is called when the selection changes. */ - onSelectionChange?: (key: Key) => void + onSelectionChange?: (key: Key | null) => void } export type SelectionMode = 'none' | 'single' | 'multiple'; diff --git a/packages/@react-types/tabs/src/index.d.ts b/packages/@react-types/tabs/src/index.d.ts index ebd96c99934..e760d2596a1 100644 --- a/packages/@react-types/tabs/src/index.d.ts +++ b/packages/@react-types/tabs/src/index.d.ts @@ -30,12 +30,14 @@ export interface AriaTabProps extends AriaLabelingProps { shouldSelectOnPressUp?: boolean } -export interface TabListProps extends CollectionBase, Omit { +export interface TabListProps extends CollectionBase, Omit { /** * Whether the TabList is disabled. * Shows that a selection exists, but is not available in that circumstance. */ - isDisabled?: boolean + isDisabled?: boolean, + /** Handler that is called when the selection changes. */ + onSelectionChange?: (key: Key) => void } interface AriaTabListBase extends AriaLabelingProps { @@ -55,7 +57,7 @@ export interface AriaTabListProps extends TabListProps, AriaTabListBase, D export interface AriaTabPanelProps extends DOMProps, AriaLabelingProps {} -export interface SpectrumTabsProps extends AriaTabListBase, SingleSelection, DOMProps, StyleProps { +export interface SpectrumTabsProps extends AriaTabListBase, Omit, DOMProps, StyleProps { /** The children of the `` element. Should include `` and `` elements. */ children: ReactNode, /** The item objects for each tab, for dynamic collections. */ @@ -69,7 +71,9 @@ export interface SpectrumTabsProps extends AriaTabListBase, SingleSelection, /** Whether the tabs are displayed in an emphasized style. */ isEmphasized?: boolean, /** The amount of space between the tabs. */ - density?: 'compact' | 'regular' + density?: 'compact' | 'regular', + /** Handler that is called when the selection changes. */ + onSelectionChange?: (key: Key) => void } export interface SpectrumTabListProps extends DOMProps, StyleProps { diff --git a/packages/dev/codemods/package.json b/packages/dev/codemods/package.json index 2364ed4b400..b861ff234e6 100644 --- a/packages/dev/codemods/package.json +++ b/packages/dev/codemods/package.json @@ -26,7 +26,7 @@ "@babel/types": "^7.24.5", "@react-spectrum/s2": "^0.8.0", "@react-types/shared": "^3.29.0", - "@types/node": "^20", + "@types/node": "patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch", "boxen": "^5.1.2", "build": "^0.1.4", "chalk": "^4.0.0", @@ -38,7 +38,7 @@ }, "devDependencies": { "@types/jscodeshift": "^0.11.11", - "typescript": "^5.5.0" + "typescript": "^5.8.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0-rc.1", diff --git a/packages/dev/docs/pages/react-aria/home/A11y.tsx b/packages/dev/docs/pages/react-aria/home/A11y.tsx index 73a14913dd5..d9e9820dfbe 100644 --- a/packages/dev/docs/pages/react-aria/home/A11y.tsx +++ b/packages/dev/docs/pages/react-aria/home/A11y.tsx @@ -66,7 +66,7 @@ export function A11y() { let [fingerPos, setFingerPos] = useState(null); let [isOpen, setOpen] = useState(false); let [caption, setCaption] = useState(''); - let [selectedKey, setSelectedKey] = useState('read'); + let [selectedKey, setSelectedKey] = useState('read'); useIntersectionObserver(ref, useCallback(() => { let button: HTMLButtonElement | null = null; let listbox: HTMLElement | null = null; diff --git a/packages/react-aria-components/docs/Checkbox.mdx b/packages/react-aria-components/docs/Checkbox.mdx index 6fe342d7d97..0df45705d92 100644 --- a/packages/react-aria-components/docs/Checkbox.mdx +++ b/packages/react-aria-components/docs/Checkbox.mdx @@ -167,7 +167,7 @@ This example wraps `Checkbox` and all of its children together into a single com ```tsx example export=true import type {CheckboxProps} from 'react-aria-components'; -export function MyCheckbox({children, ...props}: CheckboxProps) { +export function MyCheckbox({children, ...props}: Omit & {children?: React.ReactNode}) { return ( {({isIndeterminate}) => <> diff --git a/packages/react-aria-components/docs/GridList.mdx b/packages/react-aria-components/docs/GridList.mdx index 7d5483a0cb5..b00bc9796c8 100644 --- a/packages/react-aria-components/docs/GridList.mdx +++ b/packages/react-aria-components/docs/GridList.mdx @@ -318,7 +318,7 @@ export function MyGridList({children, ...props}: GridListProps ); } -export function MyItem({children, ...props}: GridListItemProps) { +export function MyItem({children, ...props}: Omit & {children?: React.ReactNode}) { let textValue = typeof children === 'string' ? children : undefined; return ( diff --git a/packages/react-aria-components/docs/Menu.mdx b/packages/react-aria-components/docs/Menu.mdx index 9b808aa7d54..dac0cca9e55 100644 --- a/packages/react-aria-components/docs/Menu.mdx +++ b/packages/react-aria-components/docs/Menu.mdx @@ -227,7 +227,7 @@ function MyMenuButton({label, children, ...props}: MyMenuButto ); } -export function MyItem(props: MenuItemProps) { +export function MyItem(props: Omit & {children?: React.ReactNode}) { let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); return ( & {children?: React.ReactNode}) { return ( {({allowsSorting, sortDirection}) => <> diff --git a/packages/react-aria-components/docs/TagGroup.mdx b/packages/react-aria-components/docs/TagGroup.mdx index 0868156df51..79d8e5765cb 100644 --- a/packages/react-aria-components/docs/TagGroup.mdx +++ b/packages/react-aria-components/docs/TagGroup.mdx @@ -228,7 +228,7 @@ function MyTagGroup({label, description, errorMessage, items, ); } -function MyTag({children, ...props}: TagProps) { +function MyTag({children, ...props}: Omit & {children?: React.ReactNode}) { let textValue = typeof children === 'string' ? children : undefined; return ( diff --git a/packages/react-aria-components/docs/Tree.mdx b/packages/react-aria-components/docs/Tree.mdx index 55275f73ab6..c93d508afc0 100644 --- a/packages/react-aria-components/docs/Tree.mdx +++ b/packages/react-aria-components/docs/Tree.mdx @@ -350,7 +350,7 @@ If you will use a Tree in multiple places in your app, you can wrap all of the p import type {TreeItemContentProps, TreeItemContentRenderProps} from 'react-aria-components'; import {Button} from 'react-aria-components'; -function MyTreeItemContent(props: TreeItemContentProps) { +function MyTreeItemContent(props: Omit & {children?: React.ReactNode}) { return ( {({hasChildItems, selectionBehavior, selectionMode}: TreeItemContentRenderProps) => ( diff --git a/scripts/extractExamples.mjs b/scripts/extractExamples.mjs index 1757dc4d6cb..137b3d6f174 100644 --- a/scripts/extractExamples.mjs +++ b/scripts/extractExamples.mjs @@ -108,7 +108,6 @@ import ReactDOM from 'react-dom/client'; fs.copyFileSync('lib/svg.d.ts', `${distDir}/svg.d.ts`); fs.copyFileSync('lib/css.d.ts', `${distDir}/css.d.ts`); -fs.copyFileSync('lib/viewTransitions.d.ts', `${distDir}/viewTransitions.d.ts`); fs.writeFileSync(`${distDir}/tsconfig.json`, `{ "compilerOptions": { "target": "es2018", diff --git a/yarn.lock b/yarn.lock index 6dae49af9c3..37f6db6fb06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7194,7 +7194,7 @@ __metadata: "@react-spectrum/s2": "npm:^0.8.0" "@react-types/shared": "npm:^3.29.0" "@types/jscodeshift": "npm:^0.11.11" - "@types/node": "npm:^20" + "@types/node": "patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch" boxen: "npm:^5.1.2" build: "npm:^0.1.4" chalk: "npm:^4.0.0" @@ -7202,7 +7202,7 @@ __metadata: jscodeshift: "npm:^0.15.2" recast: "npm:^0.23.9" ts-node: "npm:^10.9.2" - typescript: "npm:^5.5.0" + typescript: "npm:^5.8.2" uuid: "npm:^9.0.1" peerDependencies: react: ^18.0.0 || ^19.0.0-rc.1 @@ -11441,7 +11441,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:^20": +"@types/node@npm:20.14.13": version: 20.14.13 resolution: "@types/node@npm:20.14.13" dependencies: @@ -11450,12 +11450,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.0.0": - version: 18.19.31 - resolution: "@types/node@npm:18.19.31" +"@types/node@patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch": + version: 20.14.13 + resolution: "@types/node@patch:@types/node@npm%3A20.14.13#~/.yarn/patches/@types-node-npm-20.14.13-41f92d384c.patch::version=20.14.13&hash=24fda2" dependencies: undici-types: "npm:~5.26.4" - checksum: 10c0/bfebae8389220c0188492c82eaf328f4ba15e6e9b4abee33d6bf36d3b13f188c2f53eb695d427feb882fff09834f467405e2ed9be6aeb6ad4705509822d2ea08 + checksum: 10c0/1bbbadf2732c27a8f1bc7b20be41bf7faa50436461c1af6e67398c2003a8bde9893e04f762d55b231ef818d4567ec6bcb6bcdf28748f73a715a11dd7df88e9f5 languageName: node linkType: hard @@ -29656,7 +29656,7 @@ __metadata: tailwindcss: "npm:^4.0.0" tailwindcss-animate: "npm:^1.0.7" tempy: "npm:^0.5.0" - typescript: "npm:^5.5.0" + typescript: "npm:^5.8.2" typescript-eslint: "npm:^8.9.0" verdaccio: "npm:^6.0.0" walk-object: "npm:^4.0.0" @@ -33602,6 +33602,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.8.2": + version: 5.8.2 + resolution: "typescript@npm:5.8.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A^5.5.0#optional!builtin": version: 5.5.2 resolution: "typescript@patch:typescript@npm%3A5.5.2#optional!builtin::version=5.5.2&hash=b45daf" @@ -33612,6 +33622,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": + version: 5.8.2 + resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=b45daf" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10c0/8a6cd29dfb59bd5a978407b93ae0edb530ee9376a5b95a42ad057a6f80ffb0c410489ccd6fe48d1d0dfad6e8adf5d62d3874bbd251f488ae30e11a1ce6dabd28 + languageName: node + linkType: hard + "ua-parser-js@npm:0.7.17": version: 0.7.17 resolution: "ua-parser-js@npm:0.7.17" From 16ab6bfba14227f343858e112e5ad8928161b18e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Fri, 25 Apr 2025 18:10:56 -0500 Subject: [PATCH 05/24] support dropping on items --- .../dnd/src/useDroppableCollection.ts | 2 +- .../react-aria-components/example/index.css | 5 +++ packages/react-aria-components/src/Tree.tsx | 34 +++++++++++++------ .../stories/Tree.stories.tsx | 13 +++---- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/@react-aria/dnd/src/useDroppableCollection.ts b/packages/@react-aria/dnd/src/useDroppableCollection.ts index 234793a78e6..fa17d25b8ac 100644 --- a/packages/@react-aria/dnd/src/useDroppableCollection.ts +++ b/packages/@react-aria/dnd/src/useDroppableCollection.ts @@ -139,7 +139,7 @@ export function useDroppableCollection(props: DroppableCollectionOptions, state: await onItemDrop({items: filteredItems, dropOperation, isInternal, target}); } - if (target.dropPosition !== 'on') { + if (target.dropPosition !== 'on' || localState.state.collection.getItem(target.key)?.hasChildNodes) { if (!isInternal && onInsert) { await onInsert({items: filteredItems, dropOperation, target}); } diff --git a/packages/react-aria-components/example/index.css b/packages/react-aria-components/example/index.css index 3a56707c261..43a01fb013b 100644 --- a/packages/react-aria-components/example/index.css +++ b/packages/react-aria-components/example/index.css @@ -52,6 +52,11 @@ html { background: lightseagreen; color: white; } + + &[data-drop-target] { + background: purple; + color: white; + } } .menu { diff --git a/packages/react-aria-components/src/Tree.tsx b/packages/react-aria-components/src/Tree.tsx index 290a8bb317a..aac53d9fa66 100644 --- a/packages/react-aria-components/src/Tree.tsx +++ b/packages/react-aria-components/src/Tree.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {AriaTreeItemOptions, AriaTreeProps, DraggableItemResult, DropIndicatorProps, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridListSelectionCheckbox, useHover, useLocale, useTree, useTreeItem} from 'react-aria'; +import {AriaTreeItemOptions, AriaTreeProps, DraggableItemResult, DropIndicatorAria, DropIndicatorProps, DroppableCollectionResult, DroppableItemResult, FocusScope, ListKeyboardDelegate, mergeProps, useCollator, useFocusRing, useGridListSelectionCheckbox, useHover, useLocale, useTree, useTreeItem, useVisuallyHidden} from 'react-aria'; import {ButtonContext} from './Button'; import {CheckboxContext} from './RSPContexts'; import {Collection, CollectionBuilder, CollectionNode, createBranchComponent, createLeafComponent, useCachedChildren} from '@react-aria/collections'; @@ -260,6 +260,10 @@ function TreeInner({props, collection, treeRef: ref}: TreeInne { keyboardDelegate, dropTargetDelegate, + shouldAcceptItemDrop: (target) => { + let item = state.collection.getItem(target.key); + return item?.hasChildNodes ?? false; + }, onDropActivate: (e) => { // Expand collapsed item when dragging over if (e.target.type === 'item') { @@ -514,13 +518,17 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', (null); + let {visuallyHiddenProps} = useVisuallyHidden(); if (dropState && dragAndDropHooks) { - droppableItem = dragAndDropHooks.useDroppableItem!({target: {type: 'item', key: item.key, dropPosition: 'on'}}, dropState, ref); + dropIndicator = dragAndDropHooks.useDropIndicator!({ + target: {type: 'item', key: item.key, dropPosition: 'on'} + }, dropState, dropIndicatorRef); } + let isDragging = dragState && dragState.isDragging(item.key); - let isDropTarget = droppableItem?.isDropTarget; let selectionMode = state.selectionManager.selectionMode; let selectionBehavior = state.selectionManager.selectionBehavior; @@ -538,8 +546,8 @@ export const TreeItem = /*#__PURE__*/ createBranchComponent('item', (null); useEffect(() => { if (dragState && !dragButtonRef.current && process.env.NODE_ENV !== 'production') { - console.warn('Draggable items in a Table must contain a