Skip to content

Commit e495ef1

Browse files
authored
Merge branch 'main' into s2-treeview
2 parents 5d282b3 + fd7075c commit e495ef1

File tree

8 files changed

+142
-12
lines changed

8 files changed

+142
-12
lines changed

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

Lines changed: 5 additions & 1 deletion
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-spectrum/s2/api-diff.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ React Spectrum v3 [style props](https://react-spectrum.adobe.com/react-spectrum/
5959
| Prop | Spectrum 2 | RSP v3 | Comments |
6060
|------|------------|--------|----------|
6161
| size | 🟢 `'L' \| 'M' \| 'S' \| 'XL'` || |
62-
| variant | 🟢 `'accent' \| 'blue' \| 'brown' \| 'celery' \| 'charteuse' \| 'cinnamon' \| 'cyan' \| 'fuchsia' \| 'gray' \| 'green' \| 'indigo' \| 'informative' \| 'magenta' \| 'negative' \| 'neutral' \| 'notice' \| 'orange' \| 'pink' \| 'positive' \| 'purple' \| 'red' \| 'seafoam' \| 'silver' \| 'turquoise' \| 'yellow'` | 🔴 `'fuchsia' \| 'indigo' \| 'info' \| 'magenta' \| 'negative' \| 'neutral' \| 'positive' \| 'purple' \| 'seafoam' \| 'yellow'` | |
62+
| variant | 🟢 `'accent' \| 'blue' \| 'brown' \| 'celery' \| 'chartreuse' \| 'cinnamon' \| 'cyan' \| 'fuchsia' \| 'gray' \| 'green' \| 'indigo' \| 'informative' \| 'magenta' \| 'negative' \| 'neutral' \| 'notice' \| 'orange' \| 'pink' \| 'positive' \| 'purple' \| 'red' \| 'seafoam' \| 'silver' \| 'turquoise' \| 'yellow'` | 🔴 `'fuchsia' \| 'indigo' \| 'info' \| 'magenta' \| 'negative' \| 'neutral' \| 'positive' \| 'purple' \| 'seafoam' \| 'yellow'` | |
6363
## Button
6464

6565
| Prop | Spectrum 2 | RSP v3 | Comments |

packages/@react-spectrum/s2/chromatic/Badge.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default meta;
3030
let states = [
3131
{size: ['S', 'M', 'L', 'XL']},
3232
{fillStyle: ['bold', 'subtle', 'outline']},
33-
{variant: ['accent', 'informative', 'neutral', 'positive', 'notice', 'negative', 'gray', 'red', 'orange', 'yellow', 'charteuse', 'celery', 'green', 'seafoam', 'cyan', 'blue', 'indigo', 'purple', 'fuchsia', 'magenta', 'pink', 'turquoise', 'brown', 'cinnamon', 'silver']}
33+
{variant: ['accent', 'informative', 'neutral', 'positive', 'notice', 'negative', 'gray', 'red', 'orange', 'yellow', 'chartreuse', 'celery', 'green', 'seafoam', 'cyan', 'blue', 'indigo', 'purple', 'fuchsia', 'magenta', 'pink', 'turquoise', 'brown', 'cinnamon', 'silver']}
3434
];
3535

3636
let combinations = generatePowerset(states);

packages/@react-spectrum/s2/src/Badge.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@ export interface BadgeStyleProps {
3535
*
3636
* @default 'neutral'
3737
*/
38-
variant?: 'accent' | 'informative' | 'neutral' | 'positive' | 'notice' | 'negative' | 'gray' | 'red' | 'orange' | 'yellow' | 'charteuse' | 'celery' | 'green' | 'seafoam' | 'cyan' | 'blue' | 'indigo' | 'purple' | 'fuchsia' | 'magenta' | 'pink' | 'turquoise' | 'brown' | 'cinnamon' | 'silver',
38+
variant?: 'accent' | 'informative' | 'neutral' | 'positive' | 'notice' | 'negative' | 'gray' | 'red' | 'orange' | 'yellow' | 'chartreuse' | 'celery' | 'green' | 'seafoam' | 'cyan' | 'blue' | 'indigo' | 'purple' | 'fuchsia' | 'magenta' | 'pink' | 'turquoise' | 'brown' | 'cinnamon' | 'silver',
3939
/**
4040
* The fill of the badge.
4141
* @default 'bold'
4242
*/
43-
fillStyle?: 'bold' | 'subtle' | 'outline'
43+
fillStyle?: 'bold' | 'subtle' | 'outline',
44+
/**
45+
* Sets the text behavior for the contents.
46+
* @default 'wrap'
47+
*/
48+
overflowMode?: 'wrap' | 'truncate'
4449
}
4550

4651
export interface BadgeProps extends DOMProps, AriaLabelingProps, StyleProps, BadgeStyleProps, SlotProps {
@@ -86,7 +91,7 @@ const badge = style<BadgeStyleProps>({
8691
notice: 'black',
8792
orange: 'black',
8893
yellow: 'black',
89-
charteuse: 'black',
94+
chartreuse: 'black',
9095
celery: 'black'
9196
}
9297
},
@@ -108,7 +113,7 @@ const badge = style<BadgeStyleProps>({
108113
red: 'red',
109114
orange: 'orange',
110115
yellow: 'yellow',
111-
charteuse: 'chartreuse',
116+
chartreuse: 'chartreuse',
112117
celery: 'celery',
113118
green: 'green',
114119
seafoam: 'seafoam',
@@ -137,7 +142,7 @@ const badge = style<BadgeStyleProps>({
137142
red: 'red-subtle',
138143
orange: 'orange-subtle',
139144
yellow: 'yellow-subtle',
140-
charteuse: 'chartreuse-subtle',
145+
chartreuse: 'chartreuse-subtle',
141146
celery: 'celery-subtle',
142147
green: 'green-subtle',
143148
seafoam: 'seafoam-subtle',
@@ -191,6 +196,7 @@ export const Badge = forwardRef(function Badge(props: BadgeProps, ref: DOMRef<HT
191196
variant = 'neutral',
192197
size = 'S',
193198
fillStyle = 'bold',
199+
overflowMode = 'wrap',
194200
...otherProps
195201
} = props; // useProviderProps(props) in v3
196202
let domRef = useDOMRef(ref);
@@ -199,7 +205,16 @@ export const Badge = forwardRef(function Badge(props: BadgeProps, ref: DOMRef<HT
199205
return (
200206
<Provider
201207
values={[
202-
[TextContext, {styles: style({paddingY: '--labelPadding', order: 1})}],
208+
[TextContext, {
209+
styles: style({
210+
paddingY: '--labelPadding',
211+
order: 1,
212+
overflowX: 'hidden',
213+
overflowY: 'hidden',
214+
textOverflow: 'ellipsis',
215+
whiteSpace: {overflowMode: {truncate: 'nowrap', wrap: 'normal'}}
216+
})({overflowMode})
217+
}],
203218
[IconContext, {
204219
render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
205220
styles: style({size: fontRelative(20), marginStart: '--iconMargin', flexShrink: 0})

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 100 additions & 2 deletions
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)