From 4de15b973d69509ea30d02568be4f5abab1089b0 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 7 Aug 2025 07:01:45 +1000 Subject: [PATCH 01/10] chore: upgrade chromatic cli (#8660) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 58889e73574..eeb18d801c5 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "babel-plugin-transform-glob-import": "^1.0.1", "babelify": "^10.0.0", "chalk": "^4.1.2", - "chromatic": "^11.25.2", + "chromatic": "^13.1.3", "clsx": "^2.0.0", "color-space": "^1.16.0", "concurrently": "^6.0.2", diff --git a/yarn.lock b/yarn.lock index 009a250feb7..6524be4bcf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13167,9 +13167,9 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^11.25.2": - version: 11.25.2 - resolution: "chromatic@npm:11.25.2" +"chromatic@npm:^13.1.3": + version: 13.1.3 + resolution: "chromatic@npm:13.1.3" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 @@ -13182,7 +13182,7 @@ __metadata: chroma: dist/bin.js chromatic: dist/bin.js chromatic-cli: dist/bin.js - checksum: 10c0/2cb4bb40a062005292a4cd606321f6c9bdaa31e255e66bae12c780bca9b72e883c017ebe48c5a9228db88a010f5977571ef7dfdcdd4195ad0e7b955f9966d7df + checksum: 10c0/5fa2d381e06d1b089ecb790247844cfb510b063c4d8f8c0d2a3d0620ff94864003158e34338246bb1d07504d554e73dc8d5b639dc3e176ce3c88816fdc853285 languageName: node linkType: hard @@ -26230,7 +26230,7 @@ __metadata: babel-plugin-transform-glob-import: "npm:^1.0.1" babelify: "npm:^10.0.0" chalk: "npm:^4.1.2" - chromatic: "npm:^11.25.2" + chromatic: "npm:^13.1.3" clsx: "npm:^2.0.0" color-space: "npm:^1.16.0" concurrently: "npm:^6.0.2" From 2d445560435ee24b3a0c6f7a8869c27f2beb4fc6 Mon Sep 17 00:00:00 2001 From: Emmanuel Krebs Date: Thu, 7 Aug 2025 00:57:51 +0200 Subject: [PATCH 02/10] feat: add maxSpace option to @react-stately/layout GridLayout (#8654) * feat: add new maxSpace option * feat: add new private margin property * feat: take maxSpace into account when computing horizontal spacing & compute margin * example use of GridList maxSpace * chore: rename maxSpace into maxHorizontalSpace * feat: add controls to the VirtualizedGridListGrid story --- .../@react-stately/layout/src/GridLayout.ts | 20 +++++-- .../stories/GridList.stories.tsx | 52 ++++++++++++++++++- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/packages/@react-stately/layout/src/GridLayout.ts b/packages/@react-stately/layout/src/GridLayout.ts index 04911998f0f..14e3ae7588d 100644 --- a/packages/@react-stately/layout/src/GridLayout.ts +++ b/packages/@react-stately/layout/src/GridLayout.ts @@ -36,6 +36,11 @@ export interface GridLayoutOptions { * @default 18 x 18 */ minSpace?: Size, + /** + * The maximum allowed horizontal space between items. + * @default Infinity + */ + maxHorizontalSpace?: number, /** * The maximum number of columns. * @default Infinity @@ -53,6 +58,7 @@ const DEFAULT_OPTIONS = { maxItemSize: new Size(Infinity, Infinity), preserveAspectRatio: false, minSpace: new Size(18, 18), + maxSpace: Infinity, maxColumns: Infinity, dropIndicatorThickness: 2 }; @@ -69,6 +75,7 @@ export class GridLayout exte protected numColumns: number = 0; private contentSize: Size = new Size(); private layoutInfos: Map = new Map(); + private margin: number = 0; shouldInvalidateLayoutOptions(newOptions: O, oldOptions: O): boolean { return newOptions.maxColumns !== oldOptions.maxColumns @@ -76,7 +83,8 @@ export class GridLayout exte || newOptions.preserveAspectRatio !== oldOptions.preserveAspectRatio || (!(newOptions.minItemSize || DEFAULT_OPTIONS.minItemSize).equals(oldOptions.minItemSize || DEFAULT_OPTIONS.minItemSize)) || (!(newOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize).equals(oldOptions.maxItemSize || DEFAULT_OPTIONS.maxItemSize)) - || (!(newOptions.minSpace || DEFAULT_OPTIONS.minSpace).equals(oldOptions.minSpace || DEFAULT_OPTIONS.minSpace)); + || (!(newOptions.minSpace || DEFAULT_OPTIONS.minSpace).equals(oldOptions.minSpace || DEFAULT_OPTIONS.minSpace)) + || newOptions.maxHorizontalSpace !== oldOptions.maxHorizontalSpace; } update(invalidationContext: InvalidationContext): void { @@ -85,6 +93,7 @@ export class GridLayout exte maxItemSize = DEFAULT_OPTIONS.maxItemSize, preserveAspectRatio = DEFAULT_OPTIONS.preserveAspectRatio, minSpace = DEFAULT_OPTIONS.minSpace, + maxHorizontalSpace = DEFAULT_OPTIONS.maxSpace, maxColumns = DEFAULT_OPTIONS.maxColumns, dropIndicatorThickness = DEFAULT_OPTIONS.dropIndicatorThickness } = invalidationContext.layoutOptions || {}; @@ -116,9 +125,10 @@ export class GridLayout exte let itemHeight = minItemSize.height + Math.floor((maxItemHeight - minItemSize.height) * t); itemHeight = Math.max(minItemSize.height, Math.min(maxItemHeight, itemHeight)); - // Compute the horizontal spacing and content height - let horizontalSpacing = Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1)); + // Compute the horizontal spacing, content height and horizontal margin + let horizontalSpacing = Math.min(maxHorizontalSpace, Math.floor((visibleWidth - numColumns * itemWidth) / (numColumns + 1))); this.gap = new Size(horizontalSpacing, minSpace.height); + this.margin = Math.floor((visibleWidth - numColumns * itemWidth - horizontalSpacing * (numColumns + 1)) / 2); // If there is a skeleton loader within the last 2 items in the collection, increment the collection size // so that an additional row is added for the skeletons. @@ -133,7 +143,7 @@ export class GridLayout exte } lastKey = collection.getKeyBefore(lastKey); } - + let rows = Math.ceil(collectionSize / numColumns); let iterator = collection[Symbol.iterator](); let y = rows > 0 ? minSpace.height : 0; @@ -165,7 +175,7 @@ export class GridLayout exte if (skeleton) { content = oldLayoutInfo && oldLayoutInfo.content.key === key ? oldLayoutInfo.content : {...skeleton, key}; } - let x = horizontalSpacing + col * (itemWidth + horizontalSpacing); + let x = horizontalSpacing + col * (itemWidth + horizontalSpacing) + this.margin; let height = itemHeight; let estimatedSize = !preserveAspectRatio; if (oldLayoutInfo && estimatedSize) { diff --git a/packages/react-aria-components/stories/GridList.stories.tsx b/packages/react-aria-components/stories/GridList.stories.tsx index a58143c296e..569b2e497d1 100644 --- a/packages/react-aria-components/stories/GridList.stories.tsx +++ b/packages/react-aria-components/stories/GridList.stories.tsx @@ -200,7 +200,20 @@ export const VirtualizedGridList: StoryObj = { } }; -export let VirtualizedGridListGrid: GridListStory = () => { +interface VirtualizedGridListGridProps { + maxItemSizeWidth?: number, + maxColumns?: number, + minHorizontalSpace?: number, + maxHorizontalSpace?: number +} + +export let VirtualizedGridListGrid: StoryFn = (args) => { + const { + maxItemSizeWidth = 65, + maxColumns = Infinity, + minHorizontalSpace = 0, + maxHorizontalSpace = Infinity + } = args; let items: {id: number, name: string}[] = []; for (let i = 0; i < 10000; i++) { items.push({id: i, name: `Item ${i}`}); @@ -210,7 +223,11 @@ export let VirtualizedGridListGrid: GridListStory = () => { {item => {item.name}} @@ -219,6 +236,37 @@ export let VirtualizedGridListGrid: GridListStory = () => { ); }; +VirtualizedGridListGrid.story = { + args: { + maxItemSizeWidth: 65, + maxColumns: undefined, + minHorizontalSpace: 0, + maxHorizontalSpace: undefined + }, + argTypes: { + maxItemSizeWidth: { + control: 'number', + description: 'Maximum width of each item in the grid list.', + defaultValue: 65 + }, + maxColumns: { + control: 'number', + description: 'Maximum number of columns in the grid list.', + defaultValue: undefined + }, + minHorizontalSpace: { + control: 'number', + description: 'Minimum horizontal space between grid items.', + defaultValue: 0 + }, + maxHorizontalSpace: { + control: 'number', + description: 'Maximum horizontal space between grid items.', + defaultValue: undefined + } + } +}; + let renderEmptyState = ({isLoading}) => { return (
From 0173413869504c470a0752dd49c150dc11704706 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 6 Aug 2025 15:58:00 -0700 Subject: [PATCH 03/10] Revert "fix: ignore typing leading zeros in DateField segments (#8447)" (#8684) This reverts commit 7e14ffa419f59827dffb4f3ef142726e1686c36d. --- .../datepicker/src/useDateSegment.ts | 8 ++-- .../datepicker/test/DateField.test.js | 8 +--- .../datepicker/test/DatePicker.test.js | 16 ++------ .../test/DateField.test.js | 38 ------------------- 4 files changed, 10 insertions(+), 60 deletions(-) diff --git a/packages/@react-aria/datepicker/src/useDateSegment.ts b/packages/@react-aria/datepicker/src/useDateSegment.ts index e846b93ecca..2aad84bc0ea 100644 --- a/packages/@react-aria/datepicker/src/useDateSegment.ts +++ b/packages/@react-aria/datepicker/src/useDateSegment.ts @@ -256,8 +256,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: if (shouldSetValue) { focusManager.focusNext(); } - } else if (shouldSetValue) { - // Don't accept leading zeros except for fields that accept 0 as a entire value (aka 00 for minutes/seconds/etc) + } else { enteredKeys.current = newValue; } break; @@ -326,6 +325,7 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: if (ref.current) { ref.current.textContent = compositionRef.current; } + // Android sometimes fires key presses of letters as composition events. Need to handle am/pm keys here too. // Can also happen e.g. with Pinyin keyboard on iOS. if (data != null && (startsWith(am, data) || startsWith(pm, data))) { @@ -387,9 +387,9 @@ export function useDateSegment(segment: DateSegment, state: DateFieldState, ref: let segmentStyle: CSSProperties = {caretColor: 'transparent'}; if (direction === 'rtl') { - // While the bidirectional algorithm seems to work properly on inline elements with actual values, it returns different results for placeholder strings. + // While the bidirectional algorithm seems to work properly on inline elements with actual values, it returns different results for placeholder strings. // To ensure placeholder render in correct format, we apply the CSS equivalent of LRE (left-to-right embedding). See https://www.unicode.org/reports/tr9/#Explicit_Directional_Embeddings. - // However, we apply this to both placeholders and date segments with an actual value because the date segments will shift around when deleting otherwise. + // However, we apply this to both placeholders and date segments with an actual value because the date segments will shift around when deleting otherwise. segmentStyle.unicodeBidi = 'embed'; let format = options[segment.type]; if (format === 'numeric' || format === '2-digit') { diff --git a/packages/@react-spectrum/datepicker/test/DateField.test.js b/packages/@react-spectrum/datepicker/test/DateField.test.js index 3424a46db5e..23f67cd79c1 100644 --- a/packages/@react-spectrum/datepicker/test/DateField.test.js +++ b/packages/@react-spectrum/datepicker/test/DateField.test.js @@ -235,11 +235,7 @@ describe('DateField', function () { errorMessage="Date unavailable." /> ); await user.tab(); - await user.keyboard('1'); - await user.keyboard('[ArrowRight]'); - await user.keyboard('1'); - await user.keyboard('[ArrowRight]'); - await user.keyboard('1980'); + await user.keyboard('01011980'); expect(tree.getByText('Date unavailable.')).toBeInTheDocument(); }); @@ -249,7 +245,7 @@ describe('DateField', function () { ); - + let segments = Array.from(getByRole('group').querySelectorAll('[data-testid]')); let segmentTypes = segments.map(s => s.getAttribute('data-testid')); expect(segmentTypes).toEqual(['year', 'month', 'day']); diff --git a/packages/@react-spectrum/datepicker/test/DatePicker.test.js b/packages/@react-spectrum/datepicker/test/DatePicker.test.js index e7221078b27..3b931ba8152 100644 --- a/packages/@react-spectrum/datepicker/test/DatePicker.test.js +++ b/packages/@react-spectrum/datepicker/test/DatePicker.test.js @@ -1426,8 +1426,7 @@ describe('DatePicker', function () { it('should support typing into the month segment', function () { testInput('month,', new CalendarDate(2019, 2, 3), '1', new CalendarDate(2019, 1, 3), false); - testInput('month,', new CalendarDate(2019, 2, 3), '01', new CalendarDate(2019, 1, 3), false); - testInput('month,', new CalendarDate(2019, 2, 3), '012', new CalendarDate(2019, 12, 3), true); + testInput('month,', new CalendarDate(2019, 2, 3), '01', new CalendarDate(2019, 1, 3), true); testInput('month,', new CalendarDate(2019, 2, 3), '12', new CalendarDate(2019, 12, 3), true); testInput('month,', new CalendarDate(2019, 2, 3), '4', new CalendarDate(2019, 4, 3), true); testIgnored('month,', new CalendarDate(2019, 2, 3), '0'); @@ -1436,8 +1435,7 @@ describe('DatePicker', function () { it('should support typing into the day segment', function () { testInput('day,', new CalendarDate(2019, 2, 3), '1', new CalendarDate(2019, 2, 1), false); - testInput('day,', new CalendarDate(2019, 2, 3), '01', new CalendarDate(2019, 2, 1), false); - testInput('day,', new CalendarDate(2019, 2, 3), '012', new CalendarDate(2019, 2, 12), true); + testInput('day,', new CalendarDate(2019, 2, 3), '01', new CalendarDate(2019, 2, 1), true); testInput('day,', new CalendarDate(2019, 2, 3), '12', new CalendarDate(2019, 2, 12), true); testInput('day,', new CalendarDate(2019, 2, 3), '4', new CalendarDate(2019, 2, 4), true); testIgnored('day,', new CalendarDate(2019, 2, 3), '0'); @@ -1446,19 +1444,14 @@ describe('DatePicker', function () { it('should support typing into the year segment', function () { testInput('year,', new CalendarDate(2019, 2, 3), '1993', new CalendarDate(1993, 2, 3), false); - testInput('year,', new CalendarDate(2019, 2, 3), '0199', new CalendarDate(199, 2, 3), false); - testInput('year,', new CalendarDate(2019, 2, 3), '01993', new CalendarDate(1993, 2, 3), false); - testInput('year,', new CalendarDateTime(2019, 2, 3, 8), '0199', new CalendarDateTime(199, 2, 3, 8), false); testInput('year,', new CalendarDateTime(2019, 2, 3, 8), '1993', new CalendarDateTime(1993, 2, 3, 8), true); - testInput('year,', new CalendarDateTime(2019, 2, 3, 8), '01993', new CalendarDateTime(1993, 2, 3, 8), true); testIgnored('year,', new CalendarDate(2019, 2, 3), '0'); }); it('should support typing into the hour segment in 12 hour time', function () { // AM testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '1', new CalendarDateTime(2019, 2, 3, 1), false); - testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '01', new CalendarDateTime(2019, 2, 3, 1), false); - testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '011', new CalendarDateTime(2019, 2, 3, 11), true); + testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '01', new CalendarDateTime(2019, 2, 3, 1), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '11', new CalendarDateTime(2019, 2, 3, 11), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '12', new CalendarDateTime(2019, 2, 3, 0), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 8), '4', new CalendarDateTime(2019, 2, 3, 4), true); @@ -1466,8 +1459,7 @@ describe('DatePicker', function () { // PM testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '1', new CalendarDateTime(2019, 2, 3, 13), false); - testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '01', new CalendarDateTime(2019, 2, 3, 13), false); - testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '011', new CalendarDateTime(2019, 2, 3, 23), true); + testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '01', new CalendarDateTime(2019, 2, 3, 13), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '11', new CalendarDateTime(2019, 2, 3, 23), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '12', new CalendarDateTime(2019, 2, 3, 12), true); testInput('hour,', new CalendarDateTime(2019, 2, 3, 20), '4', new CalendarDateTime(2019, 2, 3, 16), true); diff --git a/packages/react-aria-components/test/DateField.test.js b/packages/react-aria-components/test/DateField.test.js index 662be07a3e6..c54966e07d8 100644 --- a/packages/react-aria-components/test/DateField.test.js +++ b/packages/react-aria-components/test/DateField.test.js @@ -374,44 +374,6 @@ describe('DateField', () => { expect(segmentTypes).toEqual(['year', 'literal', 'month', 'day']); }); - it('should not store leading zeros when typing into the segments', async () => { - let {getAllByRole} = render( - - - - {segment => } - - - ); - - let segements = getAllByRole('spinbutton'); - let monthSegment = segements[0]; - await user.click(monthSegment); - expect(monthSegment).toHaveFocus(); - await user.keyboard('11'); - expect(monthSegment).toHaveTextContent('11'); - await user.click(monthSegment); - await user.keyboard('012'); - expect(monthSegment).toHaveTextContent('12'); - - let daysSegment = segements[1]; - await user.click(daysSegment); - expect(daysSegment).toHaveFocus(); - await user.keyboard('11'); - expect(daysSegment).toHaveTextContent('11'); - await user.click(daysSegment); - await user.keyboard('012'); - expect(daysSegment).toHaveTextContent('12'); - - let yearsSegment = segements[2]; - await user.click(yearsSegment); - expect(yearsSegment).toHaveFocus(); - await user.keyboard('1111'); - expect(yearsSegment).toHaveTextContent('1111'); - await user.keyboard('002222'); - expect(yearsSegment).toHaveTextContent('2222'); - }); - it('should support autofill', async() => { let {getByRole} = render( From 49faf4a744d990283d93c87b59e70016d361ebf5 Mon Sep 17 00:00:00 2001 From: rbuzatto <35271371+rbuzatto@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:59:51 -0300 Subject: [PATCH 04/10] feat: enhance listdata selection methods (#8656) * add addSelectedKeys and removeSelectedKeys methods to useListData * create tests for new methods to useListData * keep same pattern for reassigning selection variable * rename methods * keep original selection value * remove validation for keys * adding some additional test cases --------- Co-authored-by: rbuzatto Co-authored-by: Daniel Lu --- .../@react-stately/data/src/useListData.ts | 43 ++++++++++ .../data/test/useListData.test.js | 79 +++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/packages/@react-stately/data/src/useListData.ts b/packages/@react-stately/data/src/useListData.ts index 1e8b6a840e5..a705b413d19 100644 --- a/packages/@react-stately/data/src/useListData.ts +++ b/packages/@react-stately/data/src/useListData.ts @@ -36,6 +36,12 @@ export interface ListData { /** Sets the selected keys. */ setSelectedKeys(keys: Selection): void, + /** Adds the given keys to the current selected keys. */ + addKeysToSelection(keys: Selection): void, + + /** Removes the given keys from the current selected keys. */ + removeKeysFromSelection(keys: Selection): void, + /** The current filter text. */ filterText: string, @@ -175,6 +181,43 @@ export function createListActions(opts: CreateListOptions, dispatch: selectedKeys })); }, + addKeysToSelection(selectedKeys: Selection) { + dispatch(state => { + if (state.selectedKeys === 'all') { + return state; + } + if (selectedKeys === 'all') { + return { + ...state, + selectedKeys: 'all' + }; + } + + return { + ...state, + selectedKeys: new Set([...state.selectedKeys, ...selectedKeys]) + }; + }); + }, + removeKeysFromSelection(selectedKeys: Selection) { + dispatch(state => { + if (selectedKeys === 'all') { + return { + ...state, + selectedKeys: new Set() + }; + } + + let selection: Selection = state.selectedKeys === 'all' ? new Set(state.items.map(getKey!)) : new Set(state.selectedKeys); + for (let key of selectedKeys) { + selection.delete(key); + } + return { + ...state, + selectedKeys: selection + }; + }); + }, setFilterText(filterText: string) { dispatch(state => ({ ...state, diff --git a/packages/@react-stately/data/test/useListData.test.js b/packages/@react-stately/data/test/useListData.test.js index 5d4f4144113..48086c69352 100644 --- a/packages/@react-stately/data/test/useListData.test.js +++ b/packages/@react-stately/data/test/useListData.test.js @@ -44,6 +44,85 @@ describe('useListData', function () { expect(result.current.selectedKeys).toEqual(new Set(['Sam', 'Julia'])); }); + describe('addKeysToSelection', function () { + it('should add selected keys', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: ['Sam']})); + let initialResult = result.current; + + act(() => { + result.current.addKeysToSelection(['Julia']); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual(new Set(['Sam', 'Julia'])); + }); + + it('should support adding "all" to selected keys', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: ['Sam']})); + let initialResult = result.current; + + act(() => { + result.current.addKeysToSelection('all'); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual('all'); + }); + + it('should still return "all" if selected keys was already "all"', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: 'all'})); + + act(() => { + result.current.addKeysToSelection(['Same']); + }); + expect(result.current.selectedKeys).toEqual('all'); + }); + }); + + describe('removeKeysFromSelection', function () { + it('should remove all keys', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: ['Sam', 'Julia']})); + let initialResult = result.current; + + act(() => { + result.current.removeKeysFromSelection('all'); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual(new Set()); + }); + + it('should remove the selected keys', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: ['Sam', 'Julia']})); + let initialResult = result.current; + + act(() => { + result.current.removeKeysFromSelection(['Sam']); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual(new Set(['Julia'])); + }); + + it('should remove the selected keys from an "all" set', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: 'all'})); + let initialResult = result.current; + + act(() => { + result.current.removeKeysFromSelection(['Sam', 'David']); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual(new Set(['Julia'])); + }); + + it('should support removing "all"', function () { + let {result} = renderHook(() => useListData({initialItems: initial, getKey, initialSelectedKeys: ['Sam', 'Julia']})); + let initialResult = result.current; + + act(() => { + result.current.removeKeysFromSelection('all'); + }); + expect(result.current.selectedKeys).not.toBe(initialResult.selectedKeys); + expect(result.current.selectedKeys).toEqual(new Set([])); + }); + }); + it('should get an item by key', function () { let {result} = renderHook(() => useListData({initialItems: initial, getKey})); expect(result.current.getItem('Sam')).toBe(initial[1]); From 15c52bc2134c94ad4d86949ae3631a1abe106dc9 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 7 Aug 2025 09:10:55 +1000 Subject: [PATCH 05/10] chore: update typescript-eslint so we stop getting a warning when we run eslint (#8633) --- package.json | 2 +- yarn.lock | 212 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 165 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index eeb18d801c5..b64ca456c7f 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "tailwindcss-animate": "^1.0.7", "tempy": "^0.5.0", "typescript": "^5.8.2", - "typescript-eslint": "^8.9.0", + "typescript-eslint": "^8.38.0", "verdaccio": "^6.0.0", "walk-object": "^4.0.0", "wsrun": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 6524be4bcf7..f812e28692e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2190,6 +2190,17 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.7.0": + version: 4.7.0 + resolution: "@eslint-community/eslint-utils@npm:4.7.0" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0": version: 4.11.1 resolution: "@eslint-community/regexpp@npm:4.11.1" @@ -10707,44 +10718,63 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.9.0": - version: 8.9.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.9.0" +"@typescript-eslint/eslint-plugin@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.38.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.9.0" - "@typescript-eslint/type-utils": "npm:8.9.0" - "@typescript-eslint/utils": "npm:8.9.0" - "@typescript-eslint/visitor-keys": "npm:8.9.0" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/type-utils": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" graphemer: "npm:^1.4.0" - ignore: "npm:^5.3.1" + ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^1.3.0" + ts-api-utils: "npm:^2.1.0" peerDependencies: - "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + "@typescript-eslint/parser": ^8.38.0 eslint: ^8.57.0 || ^9.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/07f273dc270268980bbf65ea5e0c69d05377e42dbdb2dd3f4a1293a3536c049ddfb548eb9ec6e60394c2361c4a15b62b8246951f83e16a9d16799578a74dc691 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/199b82e9f0136baecf515df7c31bfed926a7c6d4e6298f64ee1a77c8bdd7a8cb92a2ea55a5a345c9f2948a02f7be6d72530efbe803afa1892b593fbd529d0c27 languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.9.0": - version: 8.9.0 - resolution: "@typescript-eslint/parser@npm:8.9.0" +"@typescript-eslint/parser@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/parser@npm:8.38.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.9.0" - "@typescript-eslint/types": "npm:8.9.0" - "@typescript-eslint/typescript-estree": "npm:8.9.0" - "@typescript-eslint/visitor-keys": "npm:8.9.0" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/aca7c838de85fb700ecf5682dc6f8f90a0fbfe09a3044a176c0dc3ffd9c5e7105beb0919a30824f46b02223a74119b4f5a9834a0663328987f066cb359b5dbed + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/5580c2a328f0c15f85e4a0961a07584013cc0aca85fe868486187f7c92e9e3f6602c6e3dab917b092b94cd492ed40827c6f5fea42730bef88eb17592c947adf4 + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/project-service@npm:8.38.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.38.0" + "@typescript-eslint/types": "npm:^8.38.0" + debug: "npm:^4.3.4" + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/87d2f55521e289bbcdc666b1f4587ee2d43039cee927310b05abaa534b528dfb1b5565c1545bb4996d7fbdf9d5a3b0aa0e6c93a8f1289e3fcfd60d246364a884 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/scope-manager@npm:8.38.0" + dependencies: + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" + checksum: 10c0/ceaf489ea1f005afb187932a7ee363dfe1e0f7cc3db921283991e20e4c756411a5e25afbec72edd2095d6a4384f73591f4c750cf65b5eaa650c90f64ef9fe809 languageName: node linkType: hard @@ -10758,18 +10788,35 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.9.0": - version: 8.9.0 - resolution: "@typescript-eslint/type-utils@npm:8.9.0" +"@typescript-eslint/tsconfig-utils@npm:8.38.0, @typescript-eslint/tsconfig-utils@npm:^8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.38.0" + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/1a90da16bf1f7cfbd0303640a8ead64a0080f2b1d5969994bdac3b80abfa1177f0c6fbf61250bae082e72cf5014308f2f5cc98edd6510202f13420a7ffd07a84 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/type-utils@npm:8.38.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.9.0" - "@typescript-eslint/utils": "npm:8.9.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" debug: "npm:^4.3.4" - ts-api-utils: "npm:^1.3.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/aff06afda9ac7d12f750e76c8f91ed8b56eefd3f3f4fbaa93a64411ec9e0bd2c2972f3407e439320d98062b16f508dce7604b8bb2b803fded9d3148e5ee721b1 + ts-api-utils: "npm:^2.1.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/27795c4bd0be395dda3424e57d746639c579b7522af1c17731b915298a6378fd78869e8e141526064b6047db2c86ba06444469ace19c98cda5779d06f4abd37c + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.38.0, @typescript-eslint/types@npm:^8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/types@npm:8.38.0" + checksum: 10c0/f0ac0060c98c0f3d1871f107177b6ae25a0f1846ca8bd8cfc7e1f1dd0ddce293cd8ac4a5764d6a767de3503d5d01defcd68c758cb7ba6de52f82b209a918d0d2 languageName: node linkType: hard @@ -10780,6 +10827,26 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.38.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.38.0" + "@typescript-eslint/tsconfig-utils": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/visitor-keys": "npm:8.38.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.1.0" + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/00a00f6549877f4ae5c2847fa5ac52bf42cbd59a87533856c359e2746e448ed150b27a6137c92fd50c06e6a4b39e386d6b738fac97d80d05596e81ce55933230 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/typescript-estree@npm:8.9.0" @@ -10799,7 +10866,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.9.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.8.0": +"@typescript-eslint/utils@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/utils@npm:8.38.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.7.0" + "@typescript-eslint/scope-manager": "npm:8.38.0" + "@typescript-eslint/types": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/e97a45bf44f315f9ed8c2988429e18c88e3369c9ee3227ee86446d2d49f7325abebbbc9ce801e178f676baa986d3e1fd4b5391f1640c6eb8944c123423ae43bb + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.8.0": version: 8.9.0 resolution: "@typescript-eslint/utils@npm:8.9.0" dependencies: @@ -10813,6 +10895,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.38.0": + version: 8.38.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.38.0" + dependencies: + "@typescript-eslint/types": "npm:8.38.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/071a756e383f41a6c9e51d78c8c64bd41cd5af68b0faef5fbaec4fa5dbd65ec9e4cd610c2e2cdbe9e2facc362995f202850622b78e821609a277b5b601a1d4ec + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.9.0": version: 8.9.0 resolution: "@typescript-eslint/visitor-keys@npm:8.9.0" @@ -15904,6 +15996,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 + languageName: node + linkType: hard + "eslint@npm:^9.12.0": version: 9.12.0 resolution: "eslint@npm:9.12.0" @@ -18589,13 +18688,20 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.3.1": +"ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337 languageName: node linkType: hard +"ignore@npm:^7.0.0": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d + languageName: node + linkType: hard + "import-fresh@npm:^2.0.0": version: 2.0.0 resolution: "import-fresh@npm:2.0.0" @@ -26298,7 +26404,7 @@ __metadata: tailwindcss-animate: "npm:^1.0.7" tempy: "npm:^0.5.0" typescript: "npm:^5.8.2" - typescript-eslint: "npm:^8.9.0" + typescript-eslint: "npm:^8.38.0" verdaccio: "npm:^6.0.0" walk-object: "npm:^4.0.0" wsrun: "npm:^5.0.0" @@ -29584,6 +29690,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.1.0": + version: 2.1.0 + resolution: "ts-api-utils@npm:2.1.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/9806a38adea2db0f6aa217ccc6bc9c391ddba338a9fe3080676d0d50ed806d305bb90e8cef0276e793d28c8a929f400abb184ddd7ff83a416959c0f4d2ce754f + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -29838,17 +29953,18 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.9.0": - version: 8.9.0 - resolution: "typescript-eslint@npm:8.9.0" +"typescript-eslint@npm:^8.38.0": + version: 8.38.0 + resolution: "typescript-eslint@npm:8.38.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.9.0" - "@typescript-eslint/parser": "npm:8.9.0" - "@typescript-eslint/utils": "npm:8.9.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10c0/96bef4f5d1da9561078fa234642cfa2d024979917b8282b82f63956789bc566bdd5806ff2b414697f3dfdee314e9c9fec05911a7502550d763a496e2ef3af2fd + "@typescript-eslint/eslint-plugin": "npm:8.38.0" + "@typescript-eslint/parser": "npm:8.38.0" + "@typescript-eslint/typescript-estree": "npm:8.38.0" + "@typescript-eslint/utils": "npm:8.38.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/486b9862ee08f7827d808a2264ce03b58087b11c4c646c0da3533c192a67ae3fcb4e68d7a1e69d0f35a1edc274371a903a50ecfe74012d5eaa896cb9d5a81e0b languageName: node linkType: hard From 1f3562775dc9a2e800719bac48ee041b2dd50830 Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 6 Aug 2025 19:23:03 -0400 Subject: [PATCH 06/10] fix: allow user to reset timezone (#8054) * wip * bump actions * use system time zone --- packages/@internationalized/date/src/index.ts | 2 ++ packages/@internationalized/date/src/queries.ts | 11 ++++++++++- .../@internationalized/date/tests/queries.test.js | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/@internationalized/date/src/index.ts b/packages/@internationalized/date/src/index.ts index d55e305e8f9..22e703f66f5 100644 --- a/packages/@internationalized/date/src/index.ts +++ b/packages/@internationalized/date/src/index.ts @@ -63,6 +63,8 @@ export { today, getHoursInDay, getLocalTimeZone, + setLocalTimeZone, + resetLocalTimeZone, startOfMonth, startOfWeek, startOfYear, diff --git a/packages/@internationalized/date/src/queries.ts b/packages/@internationalized/date/src/queries.ts index 1d32b74d7a9..fc95a2f7634 100644 --- a/packages/@internationalized/date/src/queries.ts +++ b/packages/@internationalized/date/src/queries.ts @@ -133,7 +133,6 @@ let localTimeZone: string | null = null; /** Returns the time zone identifier for the current user. */ export function getLocalTimeZone(): string { - // TODO: invalidate this somehow? if (localTimeZone == null) { localTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone; } @@ -141,6 +140,16 @@ export function getLocalTimeZone(): string { return localTimeZone!; } +/** Sets the time zone identifier for the current user. */ +export function setLocalTimeZone(timeZone: string): void { + localTimeZone = timeZone; +} + +/** Resets the time zone identifier for the current user. */ +export function resetLocalTimeZone(): void { + localTimeZone = null; +} + /** Returns the first date of the month for the given date. */ export function startOfMonth(date: ZonedDateTime): ZonedDateTime; export function startOfMonth(date: CalendarDateTime): CalendarDateTime; diff --git a/packages/@internationalized/date/tests/queries.test.js b/packages/@internationalized/date/tests/queries.test.js index 87650e38af4..5d6c75c0290 100644 --- a/packages/@internationalized/date/tests/queries.test.js +++ b/packages/@internationalized/date/tests/queries.test.js @@ -17,6 +17,7 @@ import { endOfYear, EthiopicCalendar, getDayOfWeek, + getLocalTimeZone, getMinimumDayInMonth, getMinimumMonthInYear, getWeeksInMonth, @@ -31,6 +32,8 @@ import { maxDate, minDate, PersianCalendar, + resetLocalTimeZone, + setLocalTimeZone, startOfMonth, startOfWeek, startOfYear, @@ -343,4 +346,15 @@ describe('queries', function () { expect(b.compare(a)).toBeGreaterThan(0); }); }); + + describe('getLocalTimeZone', function () { + it('gets local time zone', function () { + const systemTimeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone; + expect(getLocalTimeZone()).toBe(systemTimeZone); + setLocalTimeZone('America/Denver'); + expect(getLocalTimeZone()).toBe('America/Denver'); + resetLocalTimeZone(); + expect(getLocalTimeZone()).toBe(systemTimeZone); + }); + }); }); From f652637cf6a8e461e11984e956d64f9b11f3def0 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 6 Aug 2025 18:29:27 -0500 Subject: [PATCH 07/10] avoid showing placeholder warning every render (#8682) --- .../autocomplete/src/SearchAutocomplete.tsx | 10 +++++++--- packages/@react-spectrum/color/src/ColorField.tsx | 13 +++++++++---- packages/@react-spectrum/combobox/src/ComboBox.tsx | 10 +++++++--- .../@react-spectrum/searchfield/src/SearchField.tsx | 12 ++++++++---- packages/@react-spectrum/textfield/src/TextArea.tsx | 12 ++++++++---- .../@react-spectrum/textfield/src/TextField.tsx | 12 ++++++++---- 6 files changed, 47 insertions(+), 22 deletions(-) diff --git a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx index ce4e74a00fb..19b4dad828c 100644 --- a/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx +++ b/packages/@react-spectrum/autocomplete/src/SearchAutocomplete.tsx @@ -50,9 +50,13 @@ function SearchAutocomplete(props: SpectrumSearchAutocompleteP props = useProviderProps(props); props = useFormProps(props); - if (props.placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead.'); - } + let hasWarned = useRef(false); + useEffect(() => { + if (props.placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead.'); + hasWarned.current = true; + } + }, [props.placeholder]); let isMobile = useIsMobileDevice(); if (isMobile) { diff --git a/packages/@react-spectrum/color/src/ColorField.tsx b/packages/@react-spectrum/color/src/ColorField.tsx index db92d864122..9cbdfba2618 100644 --- a/packages/@react-spectrum/color/src/ColorField.tsx +++ b/packages/@react-spectrum/color/src/ColorField.tsx @@ -13,7 +13,7 @@ import {classNames} from '@react-spectrum/utils'; import {ColorChannel, SpectrumColorFieldProps} from '@react-types/color'; import {ColorFieldContext, useContextProps} from 'react-aria-components'; -import React, {Ref, useRef} from 'react'; +import React, {Ref, useEffect, useRef} from 'react'; import styles from './colorfield.css'; import {TextFieldBase} from '@react-spectrum/textfield'; import {TextFieldRef} from '@react-types/textfield'; @@ -30,9 +30,14 @@ export const ColorField = React.forwardRef(function ColorField(props: SpectrumCo props = useProviderProps(props); props = useFormProps(props); [props] = useContextProps(props, null, ColorFieldContext); - if (props.placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ColorField.html#help-text'); - } + + let hasWarned = useRef(false); + useEffect(() => { + if (props.placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ColorField.html#help-text'); + hasWarned.current = true; + } + }, [props.placeholder]); if (props.channel) { return ; diff --git a/packages/@react-spectrum/combobox/src/ComboBox.tsx b/packages/@react-spectrum/combobox/src/ComboBox.tsx index 16f5255ce83..b39df0ec9b5 100644 --- a/packages/@react-spectrum/combobox/src/ComboBox.tsx +++ b/packages/@react-spectrum/combobox/src/ComboBox.tsx @@ -60,9 +60,13 @@ export const ComboBox = React.forwardRef(function ComboBox(pro props = useProviderProps(props); props = useFormProps(props); - if (props.placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ComboBox.html#help-text'); - } + let hasWarned = useRef(false); + useEffect(() => { + if (props.placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/ComboBox.html#help-text'); + hasWarned.current = true; + } + }, [props.placeholder]); let isMobile = useIsMobileDevice(); if (isMobile) { diff --git a/packages/@react-spectrum/searchfield/src/SearchField.tsx b/packages/@react-spectrum/searchfield/src/SearchField.tsx index 561cddd26c7..9946bf7b00b 100644 --- a/packages/@react-spectrum/searchfield/src/SearchField.tsx +++ b/packages/@react-spectrum/searchfield/src/SearchField.tsx @@ -13,7 +13,7 @@ import {classNames, useSlotProps} from '@react-spectrum/utils'; import {ClearButton} from '@react-spectrum/button'; import Magnifier from '@spectrum-icons/ui/Magnifier'; -import React, {forwardRef, ReactElement, Ref, useRef} from 'react'; +import React, {forwardRef, ReactElement, Ref, useEffect, useRef} from 'react'; import {SpectrumSearchFieldProps} from '@react-types/searchfield'; import styles from '@adobe/spectrum-css-temp/components/search/vars.css'; import {TextFieldBase} from '@react-spectrum/textfield'; @@ -42,9 +42,13 @@ export const SearchField = forwardRef(function SearchField(props: SpectrumSearch ...otherProps } = props; - if (placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/SearchField.html#help-text'); - } + let hasWarned = useRef(false); + useEffect(() => { + if (placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/SearchField.html#help-text'); + hasWarned.current = true; + } + }, [placeholder]); let state = useSearchFieldState(props); let inputRef = useRef(null); diff --git a/packages/@react-spectrum/textfield/src/TextArea.tsx b/packages/@react-spectrum/textfield/src/TextArea.tsx index 6e08d5ce71d..2424823560b 100644 --- a/packages/@react-spectrum/textfield/src/TextArea.tsx +++ b/packages/@react-spectrum/textfield/src/TextArea.tsx @@ -11,7 +11,7 @@ */ import {chain, useLayoutEffect} from '@react-aria/utils'; -import React, {Ref, useCallback, useRef} from 'react'; +import React, {Ref, useCallback, useEffect, useRef} from 'react'; import {SpectrumTextAreaProps, SpectrumTextFieldBaseProps, TextFieldRef} from '@react-types/textfield'; import {TextFieldBase} from './TextFieldBase'; import {useControlledState} from '@react-stately/utils'; @@ -69,9 +69,13 @@ export const TextArea = React.forwardRef(function TextArea(props: SpectrumTextAr } }, [onHeightChange, inputValue, inputRef]); - if (props.placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextArea.html#help-text'); - } + let hasWarned = useRef(false); + useEffect(() => { + if (props.placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextArea.html#help-text'); + hasWarned.current = true; + } + }, [props.placeholder]); let result = useTextField({ ...props, diff --git a/packages/@react-spectrum/textfield/src/TextField.tsx b/packages/@react-spectrum/textfield/src/TextField.tsx index ed11a4295f7..4381c066d60 100644 --- a/packages/@react-spectrum/textfield/src/TextField.tsx +++ b/packages/@react-spectrum/textfield/src/TextField.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import React, {forwardRef, Ref, useRef} from 'react'; +import React, {forwardRef, Ref, useEffect, useRef} from 'react'; import {SpectrumTextFieldProps, TextFieldRef} from '@react-types/textfield'; import {TextFieldBase} from './TextFieldBase'; import {useFormProps} from '@react-spectrum/form'; @@ -29,9 +29,13 @@ export const TextField = forwardRef(function TextField(props: SpectrumTextFieldP let inputRef = useRef(null); let result = useTextField(props, inputRef); - if (props.placeholder && process.env.NODE_ENV !== 'production') { - console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextField.html#help-text'); - } + let hasWarned = useRef(false); + useEffect(() => { + if (props.placeholder && !hasWarned.current && process.env.NODE_ENV !== 'production') { + console.warn('Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextField.html#help-text'); + hasWarned.current = true; + } + }, [props.placeholder]); return ( Date: Wed, 6 Aug 2025 18:31:52 -0500 Subject: [PATCH 08/10] fix: call clearData in useDrag onDragStart (#8683) * useDrag: clearData is onDragStart before writeToDataTransfer * optional chaining to avoid test failures without mock --- packages/@react-aria/dnd/src/useDrag.ts | 2 ++ packages/@react-aria/dnd/test/mocks.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/packages/@react-aria/dnd/src/useDrag.ts b/packages/@react-aria/dnd/src/useDrag.ts index 19da16c2411..6cfbb9be7b1 100644 --- a/packages/@react-aria/dnd/src/useDrag.ts +++ b/packages/@react-aria/dnd/src/useDrag.ts @@ -116,6 +116,8 @@ export function useDrag(options: DragOptions): DragResult { } let items = options.getItems(); + // Clear existing data (e.g. selected text on the page would be included in some browsers) + e.dataTransfer.clearData?.(); writeToDataTransfer(e.dataTransfer, items); let allowed = DROP_OPERATION.all; diff --git a/packages/@react-aria/dnd/test/mocks.js b/packages/@react-aria/dnd/test/mocks.js index 45824cb5242..5e543ab0278 100644 --- a/packages/@react-aria/dnd/test/mocks.js +++ b/packages/@react-aria/dnd/test/mocks.js @@ -125,6 +125,14 @@ export class DataTransfer { getData(type) { return this.items._items.find(item => item.kind === 'string' && item.type === type)?._data; } + + clearData(type) { + if (type) { + this.items._items = this.items._items.filter(item => item.type !== type); + } else { + this.items._items = []; + } + } } export class DragEvent extends MouseEvent { From f4f8a445aea48460062af80ec35075a4b80f3b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cezar=20S=C3=AErbu?= Date: Thu, 7 Aug 2025 02:49:04 +0300 Subject: [PATCH 09/10] docs: add a new S2 example app using typescript (#8012) * Add typescript build support for s2 * rename package.json * remove pm details * update webpack config * fix typescript * add empty yarn lock --------- Co-authored-by: Cezar Sirbu Co-authored-by: Robert Snow Co-authored-by: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> --- .../.gitignore | 9 + .../s2-webpack-5-typescript-example/README.md | 55 +++ .../package.json | 34 ++ .../src/App.tsx | 263 ++++++++++++ .../src/Lazy.tsx | 385 ++++++++++++++++++ .../src/components/CardViewExample.tsx | 208 ++++++++++ .../src/components/CollectionCardsExample.tsx | 92 +++++ .../src/components/Section.tsx | 30 ++ .../src/index.html | 12 + .../src/index.tsx | 18 + .../tsconfig.json | 20 + .../webpack.config.js | 120 ++++++ .../s2-webpack-5-typescript-example/yarn.lock | 0 13 files changed, 1246 insertions(+) create mode 100644 examples/s2-webpack-5-typescript-example/.gitignore create mode 100644 examples/s2-webpack-5-typescript-example/README.md create mode 100644 examples/s2-webpack-5-typescript-example/package.json create mode 100644 examples/s2-webpack-5-typescript-example/src/App.tsx create mode 100644 examples/s2-webpack-5-typescript-example/src/Lazy.tsx create mode 100644 examples/s2-webpack-5-typescript-example/src/components/CardViewExample.tsx create mode 100644 examples/s2-webpack-5-typescript-example/src/components/CollectionCardsExample.tsx create mode 100644 examples/s2-webpack-5-typescript-example/src/components/Section.tsx create mode 100644 examples/s2-webpack-5-typescript-example/src/index.html create mode 100644 examples/s2-webpack-5-typescript-example/src/index.tsx create mode 100644 examples/s2-webpack-5-typescript-example/tsconfig.json create mode 100644 examples/s2-webpack-5-typescript-example/webpack.config.js create mode 100644 examples/s2-webpack-5-typescript-example/yarn.lock diff --git a/examples/s2-webpack-5-typescript-example/.gitignore b/examples/s2-webpack-5-typescript-example/.gitignore new file mode 100644 index 00000000000..911108854aa --- /dev/null +++ b/examples/s2-webpack-5-typescript-example/.gitignore @@ -0,0 +1,9 @@ +node_modules +build +.DS_Store +npm-debug.log +yarn-error.log +.yarnclean +.vscode +.idea +dist \ No newline at end of file diff --git a/examples/s2-webpack-5-typescript-example/README.md b/examples/s2-webpack-5-typescript-example/README.md new file mode 100644 index 00000000000..13525dc3e29 --- /dev/null +++ b/examples/s2-webpack-5-typescript-example/README.md @@ -0,0 +1,55 @@ +# Webpack 5 example + +This is a [Webpack](https://webpack.js.org/) project with a minimal React configuration. + +## Getting Started + +First, run the development server: + +```bash +yarn install +yarn dev +``` + +Open [http://localhost:8080](http://localhost:8080) with your browser to see the result. + +style-macro and React Spectrum - Spectrum 2 have been added to `src/App.js` to show an example of a Spectrum 2 styled component. This file does client side rendering. The page auto-updates as you edit the file. + +## Macros config + +Edit the webpack.config.js to add an import for the plugin and add a webpack config that adds the webpack version of the macros plugin. An empty config file would be updated to look like the following. + +``` +const macros = require("unplugin-parcel-macros"); + +module.exports = { + // ... + plugins: [ + // ... + macros.webpack(), + // ... + ], +}; +``` + +To use the spectrum-theme via macros, pass your styles object to the style() macro and set the result as a new function. This new function or style() should be used within a `className` prop to style your html elements. Use the `styles` prop on React Spectrum components. + +```jsx +
+ Hello Spectrum 2! +
+``` + +```jsx + +``` + +## Application setup + +Please include the page level CSS in the root of your application to configure and support the light and dark themes. + +``` +import "@react-spectrum/s2/page.css"; +``` diff --git a/examples/s2-webpack-5-typescript-example/package.json b/examples/s2-webpack-5-typescript-example/package.json new file mode 100644 index 00000000000..b89f287aad5 --- /dev/null +++ b/examples/s2-webpack-5-typescript-example/package.json @@ -0,0 +1,34 @@ +{ + "name": "webpack-5-typescript-example", + "version": "1.0.0", + "description": "", + "main": "./src/index.tsx", + "packageManager": "yarn@4.2.2", + "scripts": { + "dev": "webpack serve", + "build": "webpack --mode production" + }, + "dependencies": { + "@react-spectrum/s2": "latest", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@swc/core": "^1.11.13", + "@types/react": "^19.0.12", + "@types/react-dom": "^19.0.4", + "css-loader": "^6.10.0", + "css-minimizer-webpack-plugin": "^7.0.0", + "html-webpack-plugin": "^5.6.0", + "lightningcss": "^1.27.0", + "mini-css-extract-plugin": "^2.9.1", + "style-loader": "^3.3.4", + "swc-loader": "^0.2.6", + "swc-minify-webpack-plugin": "^2.1.3", + "typescript": "^5.8.2", + "unplugin-parcel-macros": "0.0.3", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + } +} diff --git a/examples/s2-webpack-5-typescript-example/src/App.tsx b/examples/s2-webpack-5-typescript-example/src/App.tsx new file mode 100644 index 00000000000..dd380117008 --- /dev/null +++ b/examples/s2-webpack-5-typescript-example/src/App.tsx @@ -0,0 +1,263 @@ +/* + * 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 "@react-spectrum/s2/page.css"; +import { + ActionButton, + ActionButtonGroup, + ActionMenu, + Button, + ButtonGroup, + Cell, + Column, + Divider, + Heading, + LinkButton, + Menu, + MenuItem, + MenuTrigger, + Picker, + PickerItem, + Row, + SubmenuTrigger, + TableBody, + TableHeader, + TableView, + Text, + ToggleButton, + ToggleButtonGroup, + TreeView, + TreeViewItem, + TreeViewItemContent +} from "@react-spectrum/s2"; +import {CardViewExample} from "./components/CardViewExample"; +import {CollectionCardsExample} from "./components/CollectionCardsExample"; +import Edit from "@react-spectrum/s2/icons/Edit"; +import FileTxt from "@react-spectrum/s2/icons/FileText"; +import Folder from "@react-spectrum/s2/icons/Folder"; +import {LoadingState} from '@react-types/shared' +import React, {useState} from "react"; +import Section from "./components/Section"; +import {style} from "@react-spectrum/s2/style" with { type: "macro" }; + +const Lazy = React.lazy(() => import('./Lazy')); + +function App() { + let [isLazyLoaded, setLazyLoaded] = useState(false); + let [cardViewState, setCardViewState] = useState<{layout?: 'grid' | 'waterfall' , loadingState: LoadingState}>({ + layout: 'grid', + loadingState: 'idle', + }); + let cardViewLoadingOptions = [ + {id: 'idle', label: 'Idle'}, + {id: 'loading', label: 'Loading'}, + {id: 'sorting', label: 'Sorting'}, + {id: 'loadingMore', label: 'Loading More'}, + {id: 'error', label: 'Error'}, + ]; + let cardViewLayoutOptions = [ + {id: 'grid', label: 'Grid'}, + {id: 'waterfall', label: 'Waterfall'} + ]; + return ( +
+ + Spectrum 2 + Webpack + Typescript + +
+ +
+
+
+ + + + + + Action Button + + Toggle Button + + Link Button + + + Cut + Copy + Paste + + + Bold + Italic + Underline + + +
+ +
+ + Action Menu Item 1 + Action Menu Item 2 + Action Menu Item 3 + + setCardViewState({...cardViewState, loadingState} as any)}> + {item => {item.label}} + + setCardViewState({...cardViewState, layout} as any)}> + {item => {item.label}} + + + + + + Menu + alert(key.toString())}> + Cut + Copy + Paste + Replace + + Share + alert(key.toString())}> + Copy Link + + Email + alert(key.toString())}> + Email as Attachment + Email as Link + + + SMS + + + Delete + + + + Menu Trigger + + + Link to /foo + + Cut + Copy + Paste + + + + + Name + Type + Date Modified + A + B + + + + Games + File folder + 6/7/2020 + Dummy content + Long long long long long long long cell + + + Program Files + File folder + 4/7/2021 + Dummy content + Long long long long long long long cell + + + bootmgr + System file + 11/20/2010 + Dummy content + Long long long long long long long cell + + + + + + + Photos + + + + + + Projects + + + + + Projects-1 + + + + + Projects-1A + + + + + + + Projects-2 + + + + + + Projects-3 + + + + + +
+ + {!isLazyLoaded && setLazyLoaded(true)}>Load more} + {isLazyLoaded && Loading}> + + } +
+
+ ); +} + +export default App; diff --git a/examples/s2-webpack-5-typescript-example/src/Lazy.tsx b/examples/s2-webpack-5-typescript-example/src/Lazy.tsx new file mode 100644 index 00000000000..2a3d4a41dce --- /dev/null +++ b/examples/s2-webpack-5-typescript-example/src/Lazy.tsx @@ -0,0 +1,385 @@ +import "@react-spectrum/s2/page.css"; +import { + Accordion, + ActionButton, + AlertDialog, + Avatar, + AvatarGroup, + Badge, + Breadcrumb, + Breadcrumbs, + Button, + ButtonGroup, + Checkbox, + CheckboxGroup, + CloseButton, + ColorArea, + ColorField, + ColorSlider, + ColorSwatch, + ColorSwatchPicker, + ColorWheel, + ComboBox, + ComboBoxItem, + Content, + ContextualHelp, + CustomDialog, + Dialog, + DialogContainer, + DialogTrigger, + Disclosure, + DisclosureHeader, + DisclosurePanel, + DisclosureTitle, + DropZone, + Footer, + Form, + Header, + Heading, + IllustratedMessage, + Image, + InlineAlert, + Keyboard, + Link, + Meter, + NumberField, + Picker, + PickerItem, + Popover, + ProgressBar, + ProgressCircle, + Radio, + RadioGroup, + RangeSlider, + SearchField, + SegmentedControl, + SegmentedControlItem, + Slider, + StatusLight, + Switch, + Tab, + TabList, + TabPanel, + Tabs, + Tag, + TagGroup, + Text, + TextArea, + TextField, + Tooltip, + TooltipTrigger, +} from "@react-spectrum/s2"; +import Checkmark from '@react-spectrum/s2/illustrations/gradient/generic1/Checkmark'; +import Cloud from "@react-spectrum/s2/illustrations/linear/Cloud"; +import DropToUpload from "@react-spectrum/s2/illustrations/linear/DropToUpload"; +import Edit from "@react-spectrum/s2/icons/Edit"; +import Section from "./components/Section"; +import {style} from "@react-spectrum/s2/style" with { type: "macro" }; +import {useState} from "react"; + +export default function Lazy() { + let [isDialogOpen, setIsDialogOpen] = useState(false); + return ( + <> +
+ + + + + + + + + + + + +
+ +
+ + + + Drag and drop your file + Or, select a file from your computer + + +
+ +
+
+ + Soccer + Baseball + Basketball + + + + Dogs + Cats + + + Low power mode +