diff --git a/packages/@react-spectrum/tabs/test/Tabs.test.js b/packages/@react-spectrum/tabs/test/Tabs.test.js
index 4b839486b94..e402a0995ad 100644
--- a/packages/@react-spectrum/tabs/test/Tabs.test.js
+++ b/packages/@react-spectrum/tabs/test/Tabs.test.js
@@ -890,4 +890,386 @@ describe('Tabs', function () {
fireEvent.keyDown(tabs[1], {key: 'ArrowRight'});
expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
});
+
+ describe('when using fragments', function () {
+ it('renders fragment with children properly', function () {
+ let container = render(
+
+
+
+ <>
+ - Tab 1
+ - Tab 2
+ >
+
+
+ <>
+ -
+ Tab 1 content
+
+ -
+ Tab 2 content
+
+ >
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(2);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders beginning fragment sibling properly', function () {
+ let container = render(
+
+
+
+ <>
+ - Tab 1
+ >
+ - Tab 2
+
+
+ <>
+ -
+ Tab 1 content
+
+ >
+ -
+ Tab 2 content
+
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(2);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders middle fragment sibling properly', function () {
+ let container = render(
+
+
+
+ - Tab 1
+ <>
+ - Tab 2
+ >
+ - Tab 3
+
+
+ -
+ Tab 1 content
+
+ <>
+ -
+ Tab 2 content
+
+ >
+ -
+ Tab 3 content
+
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(3);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders ending fragment sibling properly', function () {
+ let container = render(
+
+
+
+ - Tab 1
+ <>
+ - Tab 2
+ >
+
+
+ -
+ Tab 1 content
+
+ <>
+ -
+ Tab 2 content
+
+ >
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(2);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders list and panel fragment siblings in non-matching positions properly, list fragment first', function () {
+ let container = render(
+
+
+
+ <>
+ - Tab 1
+ >
+ - Tab 2
+
+
+ -
+ Tab 1 content
+
+ <>
+ -
+ Tab 2 content
+
+ >
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(2);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders list and panel fragment siblings in non-matching positions properly, panel fragment first', function () {
+ let container = render(
+
+
+
+ - Tab 1
+ <>
+ - Tab 2
+ >
+
+
+ <>
+ -
+ Tab 1 content
+
+ >
+ -
+ Tab 2 content
+
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(2);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent('Tab 1 content');
+ }
+ }
+ });
+
+ it('renders fragment with renderer properly', function () {
+ let container = render(
+
+
+
+ <>
+ {item => (
+
+ )}
+ >
+
+
+ <>
+ {item => (
+ -
+ {item.children}
+
+ )}
+ >
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(3);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent(defaultItems[0].children);
+ }
+ }
+ });
+
+ it('renders fragment with mapper properly', function () {
+ let container = render(
+
+
+
+ <>
+ {defaultItems.map(item => (
+
+ ))}
+ >
+
+
+ <>
+ {defaultItems.map(item => (
+ -
+ {item.children}
+
+ ))}
+ >
+
+
+
+ );
+
+ let tablist = container.getByRole('tablist');
+ expect(tablist).toBeTruthy();
+
+ expect(tablist).toHaveAttribute('aria-orientation', 'horizontal');
+
+ let tabs = within(tablist).getAllByRole('tab');
+ expect(tabs.length).toBe(3);
+
+ for (let tab of tabs) {
+ expect(tab).toHaveAttribute('tabindex');
+ expect(tab).toHaveAttribute('aria-selected');
+ let isSelected = tab.getAttribute('aria-selected') === 'true';
+ if (isSelected) {
+ expect(tab).toHaveAttribute('aria-controls');
+ let tabpanel = document.getElementById(tab.getAttribute('aria-controls'));
+ expect(tabpanel).toBeTruthy();
+ expect(tabpanel).toHaveAttribute('aria-labelledby', tab.id);
+ expect(tabpanel).toHaveAttribute('role', 'tabpanel');
+ expect(tabpanel).toHaveTextContent(defaultItems[0].children);
+ }
+ }
+ });
+ });
});
diff --git a/packages/@react-stately/collections/src/CollectionBuilder.ts b/packages/@react-stately/collections/src/CollectionBuilder.ts
index 3d7bae6c1e6..09f804818b4 100644
--- a/packages/@react-stately/collections/src/CollectionBuilder.ts
+++ b/packages/@react-stately/collections/src/CollectionBuilder.ts
@@ -27,10 +27,15 @@ export class CollectionBuilder {
return iterable(() => this.iterateCollection(props));
}
- private *iterateCollection(props: CollectionBase) {
+ private *iterateCollection(props: CollectionBase): Generator> {
let {children, items} = props;
- if (typeof children === 'function') {
+ if (React.isValidElement<{children: CollectionElement}>(children) && children.type === React.Fragment) {
+ yield* this.iterateCollection({
+ children: children.props.children,
+ items
+ });
+ } else if (typeof children === 'function') {
if (!items) {
throw new Error('props.children was a function but props.items is missing');
}
@@ -90,6 +95,25 @@ export class CollectionBuilder {
}
private *getFullNode(partialNode: PartialNode, state: CollectionBuilderState, parentKey?: Key, parentNode?: Node): Generator> {
+ if (React.isValidElement<{children: CollectionElement}>(partialNode.element) && partialNode.element.type === React.Fragment) {
+ let children: CollectionElement[] = [];
+
+ React.Children.forEach(partialNode.element.props.children, child => {
+ children.push(child);
+ });
+
+ let index = partialNode.index;
+
+ for (const child of children) {
+ yield* this.getFullNode({
+ element: child,
+ index: index++
+ }, state, parentKey, parentNode);
+ }
+
+ return;
+ }
+
// If there's a value instead of an element on the node, and a parent renderer function is available,
// use it to render an element for the value.
let element = partialNode.element;