Skip to content

Commit

Permalink
Merge branch 'main' into tailwind4
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Jan 28, 2025
2 parents 68f804b + fd7075c commit 7a42b9c
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 4 deletions.
6 changes: 5 additions & 1 deletion packages/@react-aria/gridlist/src/useGridList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DisabledBehavior,
DOMAttributes,
DOMProps,
FocusStrategy,
Key,
KeyboardDelegate,
LayoutDelegate,
Expand All @@ -30,6 +31,8 @@ import {useHasTabbableChild} from '@react-aria/focus';
import {useSelectableList} from '@react-aria/selection';

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

let id = useId(props.id);
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-stately/tabs/src/useTabListState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function useTabListState<T extends object>(props: TabListStateOptions<T>)
useEffect(() => {
// Ensure a tab is always selected (in case no selected key was specified or if selected item was deleted from collection)
let selectedKey = currentSelectedKey;
if (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey)) {
if (props.selectedKey == null && (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey))) {
selectedKey = findDefaultSelectedKey(collection, state.disabledKeys);
if (selectedKey != null) {
// directly set selection because replace/toggle selection won't consider disabled keys
Expand Down
7 changes: 7 additions & 0 deletions packages/react-aria-components/test/GridList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ describe('GridList', () => {
expect(itemRef.current).toBeInstanceOf(HTMLElement);
});

it('should support autoFocus', () => {
let {getByRole} = renderGridList({autoFocus: true});
let gridList = getByRole('grid');

expect(document.activeElement).toBe(gridList);
});

it('should support hover', async () => {
let onHoverStart = jest.fn();
let onHoverChange = jest.fn();
Expand Down
6 changes: 6 additions & 0 deletions packages/react-aria-components/test/ListBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ describe('ListBox', () => {
expect(getAllByRole('option').map(o => o.textContent)).toEqual(['Hi']);
});

it('should support autoFocus', () => {
let {getByRole} = renderListbox({autoFocus: true});
let listbox = getByRole('listbox');
expect(document.activeElement).toBe(listbox);
});

it('should support hover', async () => {
let hoverStartSpy = jest.fn();
let hoverChangeSpy = jest.fn();
Expand Down
102 changes: 100 additions & 2 deletions packages/react-aria-components/test/Tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
* governing permissions and limitations under the License.
*/

import {Button, Collection, Tab, TabList, TabPanel, Tabs} from '../';
import {fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal';
import React from 'react';
import {Tab, TabList, TabPanel, Tabs} from '../';
import React, {useState} from 'react';
import {TabsExample} from '../stories/Tabs.stories';
import {User} from '@react-aria/test-utils';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -497,4 +497,102 @@ describe('Tabs', () => {
expect(innerTabs[0]).toHaveTextContent('One');
expect(innerTabs[1]).toHaveTextContent('Two');
});

it('can add tabs and keep the current selected key', async () => {
let onSelectionChange = jest.fn();
function Example(props) {
let [tabs, setTabs] = useState([
{id: 1, title: 'Tab 1', content: 'Tab body 1'},
{id: 2, title: 'Tab 2', content: 'Tab body 2'},
{id: 3, title: 'Tab 3', content: 'Tab body 3'}
]);

const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);

let addTab = () => {
const tabId = tabs.length + 1;

setTabs((prevTabs) => [
...prevTabs,
{
id: tabId,
title: `Tab ${tabId}`,
content: `Tab body ${tabId}`
}
]);

// Use functional update to ensure you're working with the most recent state
setSelectedTabId(tabId);
};

let removeTab = () => {
if (tabs.length > 1) {
setTabs((prevTabs) => {
const updatedTabs = prevTabs.slice(0, -1);
// Update selectedTabId to the last remaining tab's ID if the current selected tab is removed
const newSelectedTabId = updatedTabs[updatedTabs.length - 1].id;
setSelectedTabId(newSelectedTabId);
return updatedTabs;
});
}
};

const onSelectionChange = (value) => {
setSelectedTabId(value);
props.onSelectionChange(value);
};

return (
<Tabs selectedKey={selectedTabId} onSelectionChange={onSelectionChange}>
<div style={{display: 'flex'}}>
<TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}>
{(item) => (
<Tab>
{({isSelected}) => (
<p
style={{
color: isSelected ? 'red' : 'black'
}}>
{item.title}
</p>
)}
</Tab>
)}
</TabList>
<div className="button-group">
<Button onPress={addTab}>Add tab</Button>
<Button onPress={removeTab}>Remove tab</Button>
</div>
</div>
<Collection items={tabs}>
{(item) => (
<TabPanel
style={{
borderTop: '2px solid black'
}}>
{item.content}
</TabPanel>
)}
</Collection>
</Tabs>
);
}
let {getAllByRole} = render(<Example onSelectionChange={onSelectionChange} />);
let tabs = getAllByRole('tab');
await user.tab();
await user.keyboard('{ArrowRight}');
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
await user.tab();
onSelectionChange.mockClear();
await user.keyboard('{Enter}');
expect(onSelectionChange).not.toHaveBeenCalled();
tabs = getAllByRole('tab');
expect(tabs[3]).toHaveAttribute('aria-selected', 'true');

await user.tab();
await user.keyboard('{Enter}');
expect(onSelectionChange).not.toHaveBeenCalled();
tabs = getAllByRole('tab');
expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
});
});

0 comments on commit 7a42b9c

Please sign in to comment.