Skip to content

Commit 7a42b9c

Browse files
authored
Merge branch 'main' into tailwind4
2 parents 68f804b + fd7075c commit 7a42b9c

File tree

5 files changed

+119
-4
lines changed

5 files changed

+119
-4
lines changed

packages/@react-aria/gridlist/src/useGridList.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
DisabledBehavior,
1717
DOMAttributes,
1818
DOMProps,
19+
FocusStrategy,
1920
Key,
2021
KeyboardDelegate,
2122
LayoutDelegate,
@@ -30,6 +31,8 @@ import {useHasTabbableChild} from '@react-aria/focus';
3031
import {useSelectableList} from '@react-aria/selection';
3132

3233
export interface GridListProps<T> extends CollectionBase<T>, MultipleSelection {
34+
/** Whether to auto focus the gridlist or an option. */
35+
autoFocus?: boolean | FocusStrategy,
3336
/**
3437
* Handler that is called when a user performs an action on an item. The exact user event depends on
3538
* the collection's `selectionBehavior` prop and the interaction modality.
@@ -113,7 +116,8 @@ export function useGridList<T>(props: AriaGridListOptions<T>, state: ListState<T
113116
isVirtualized,
114117
selectOnFocus: state.selectionManager.selectionBehavior === 'replace',
115118
shouldFocusWrap: props.shouldFocusWrap,
116-
linkBehavior
119+
linkBehavior,
120+
autoFocus: props.autoFocus
117121
});
118122

119123
let id = useId(props.id);

packages/@react-stately/tabs/src/useTabListState.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function useTabListState<T extends object>(props: TabListStateOptions<T>)
4343
useEffect(() => {
4444
// Ensure a tab is always selected (in case no selected key was specified or if selected item was deleted from collection)
4545
let selectedKey = currentSelectedKey;
46-
if (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey)) {
46+
if (props.selectedKey == null && (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey))) {
4747
selectedKey = findDefaultSelectedKey(collection, state.disabledKeys);
4848
if (selectedKey != null) {
4949
// directly set selection because replace/toggle selection won't consider disabled keys

packages/react-aria-components/test/GridList.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ describe('GridList', () => {
128128
expect(itemRef.current).toBeInstanceOf(HTMLElement);
129129
});
130130

131+
it('should support autoFocus', () => {
132+
let {getByRole} = renderGridList({autoFocus: true});
133+
let gridList = getByRole('grid');
134+
135+
expect(document.activeElement).toBe(gridList);
136+
});
137+
131138
it('should support hover', async () => {
132139
let onHoverStart = jest.fn();
133140
let onHoverChange = jest.fn();

packages/react-aria-components/test/ListBox.test.js

+6
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,12 @@ describe('ListBox', () => {
337337
expect(getAllByRole('option').map(o => o.textContent)).toEqual(['Hi']);
338338
});
339339

340+
it('should support autoFocus', () => {
341+
let {getByRole} = renderListbox({autoFocus: true});
342+
let listbox = getByRole('listbox');
343+
expect(document.activeElement).toBe(listbox);
344+
});
345+
340346
it('should support hover', async () => {
341347
let hoverStartSpy = jest.fn();
342348
let hoverChangeSpy = jest.fn();

packages/react-aria-components/test/Tabs.test.js

+100-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {Button, Collection, Tab, TabList, TabPanel, Tabs} from '../';
1314
import {fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal';
14-
import React from 'react';
15-
import {Tab, TabList, TabPanel, Tabs} from '../';
15+
import React, {useState} from 'react';
1616
import {TabsExample} from '../stories/Tabs.stories';
1717
import {User} from '@react-aria/test-utils';
1818
import userEvent from '@testing-library/user-event';
@@ -497,4 +497,102 @@ describe('Tabs', () => {
497497
expect(innerTabs[0]).toHaveTextContent('One');
498498
expect(innerTabs[1]).toHaveTextContent('Two');
499499
});
500+
501+
it('can add tabs and keep the current selected key', async () => {
502+
let onSelectionChange = jest.fn();
503+
function Example(props) {
504+
let [tabs, setTabs] = useState([
505+
{id: 1, title: 'Tab 1', content: 'Tab body 1'},
506+
{id: 2, title: 'Tab 2', content: 'Tab body 2'},
507+
{id: 3, title: 'Tab 3', content: 'Tab body 3'}
508+
]);
509+
510+
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
511+
512+
let addTab = () => {
513+
const tabId = tabs.length + 1;
514+
515+
setTabs((prevTabs) => [
516+
...prevTabs,
517+
{
518+
id: tabId,
519+
title: `Tab ${tabId}`,
520+
content: `Tab body ${tabId}`
521+
}
522+
]);
523+
524+
// Use functional update to ensure you're working with the most recent state
525+
setSelectedTabId(tabId);
526+
};
527+
528+
let removeTab = () => {
529+
if (tabs.length > 1) {
530+
setTabs((prevTabs) => {
531+
const updatedTabs = prevTabs.slice(0, -1);
532+
// Update selectedTabId to the last remaining tab's ID if the current selected tab is removed
533+
const newSelectedTabId = updatedTabs[updatedTabs.length - 1].id;
534+
setSelectedTabId(newSelectedTabId);
535+
return updatedTabs;
536+
});
537+
}
538+
};
539+
540+
const onSelectionChange = (value) => {
541+
setSelectedTabId(value);
542+
props.onSelectionChange(value);
543+
};
544+
545+
return (
546+
<Tabs selectedKey={selectedTabId} onSelectionChange={onSelectionChange}>
547+
<div style={{display: 'flex'}}>
548+
<TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}>
549+
{(item) => (
550+
<Tab>
551+
{({isSelected}) => (
552+
<p
553+
style={{
554+
color: isSelected ? 'red' : 'black'
555+
}}>
556+
{item.title}
557+
</p>
558+
)}
559+
</Tab>
560+
)}
561+
</TabList>
562+
<div className="button-group">
563+
<Button onPress={addTab}>Add tab</Button>
564+
<Button onPress={removeTab}>Remove tab</Button>
565+
</div>
566+
</div>
567+
<Collection items={tabs}>
568+
{(item) => (
569+
<TabPanel
570+
style={{
571+
borderTop: '2px solid black'
572+
}}>
573+
{item.content}
574+
</TabPanel>
575+
)}
576+
</Collection>
577+
</Tabs>
578+
);
579+
}
580+
let {getAllByRole} = render(<Example onSelectionChange={onSelectionChange} />);
581+
let tabs = getAllByRole('tab');
582+
await user.tab();
583+
await user.keyboard('{ArrowRight}');
584+
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
585+
await user.tab();
586+
onSelectionChange.mockClear();
587+
await user.keyboard('{Enter}');
588+
expect(onSelectionChange).not.toHaveBeenCalled();
589+
tabs = getAllByRole('tab');
590+
expect(tabs[3]).toHaveAttribute('aria-selected', 'true');
591+
592+
await user.tab();
593+
await user.keyboard('{Enter}');
594+
expect(onSelectionChange).not.toHaveBeenCalled();
595+
tabs = getAllByRole('tab');
596+
expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
597+
});
500598
});

0 commit comments

Comments
 (0)