Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 6 additions & 40 deletions packages/@react-aria/collections/src/Hidden.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
*/

import {forwardRefType} from '@react-types/shared';
import React, {Context, createContext, forwardRef, JSX, ReactElement, ReactNode, useContext, useRef} from 'react';
import {useLayoutEffect} from '@react-aria/utils';
import React, {Context, createContext, forwardRef, JSX, ReactElement, ReactNode, useContext} from 'react';

// React doesn't understand the <template> element, which doesn't have children like a normal element.
// It will throw an error during hydration when it expects the firstChild to contain content rendered
Expand All @@ -21,56 +20,35 @@ import {useLayoutEffect} from '@react-aria/utils';
// does the same for appendChild/removeChild/insertBefore as per the issue below
// See https://github.com/facebook/react/issues/19932
if (typeof HTMLTemplateElement !== 'undefined') {
const getFirstChild = Object.getOwnPropertyDescriptor(Node.prototype, 'firstChild')!.get!;
const originalAppendChild = Object.getOwnPropertyDescriptor(Node.prototype, 'appendChild')!.value!;
const originalRemoveChild = Object.getOwnPropertyDescriptor(Node.prototype, 'removeChild')!.value!;
const originalInsertBefore = Object.getOwnPropertyDescriptor(Node.prototype, 'insertBefore')!.value!;

Object.defineProperty(HTMLTemplateElement.prototype, 'firstChild', {
configurable: true,
enumerable: true,
get: function () {
if (this.dataset.reactAriaHidden) {
return this.content.firstChild;
} else {
return getFirstChild.call(this);
}
return this.content.firstChild;
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'appendChild', {
configurable: true,
enumerable: true,
value: function (node) {
if (this.dataset.reactAriaHidden) {
return this.content.appendChild(node);
} else {
return originalAppendChild.call(this, node);
}
return this.content.appendChild(node);
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'removeChild', {
configurable: true,
enumerable: true,
value: function (node) {
if (this.dataset.reactAriaHidden) {
return this.content.removeChild(node);
} else {
return originalRemoveChild.call(this, node);
}
return this.content.removeChild(node);
}
});

Object.defineProperty(HTMLTemplateElement.prototype, 'insertBefore', {
configurable: true,
enumerable: true,
value: function (node, child) {
if (this.dataset.reactAriaHidden) {
return this.content.insertBefore(node, child);
} else {
return originalInsertBefore.call(this, node, child);
}
return this.content.insertBefore(node, child);
}
});
}
Expand All @@ -79,18 +57,6 @@ export const HiddenContext: Context<boolean> = createContext<boolean>(false);

export function Hidden(props: {children: ReactNode}): JSX.Element {
let isHidden = useContext(HiddenContext);
let templateRef = useRef<HTMLTemplateElement>(null);
// somehow React might add children to the template and we never hit the reactAriaHidden parts of the above overrides
// so we need to move those children into the content of the template since templates can't have direct children
useLayoutEffect(() => {
let el = templateRef.current;
if (!el?.dataset.reactAriaHidden) {
return;
}
while (el.childNodes.length > 0) {
el.content.appendChild(el.childNodes[0]);
}
}, []);

if (isHidden) {
// Don't hide again if we are already hidden.
Expand All @@ -106,7 +72,7 @@ export function Hidden(props: {children: ReactNode}): JSX.Element {
// In SSR, portals are not supported by React. Instead, always render into a <template>
// element, which the browser will never display to the user. In addition, the
// content is not part of the accessible DOM tree, so it won't affect ids or other accessibility attributes.
return <template ref={templateRef} data-react-aria-hidden>{children}</template>;
return <template>{children}</template>;
}

/** Creates a component that forwards its ref and returns null if it is in a hidden subtree. */
Expand Down
51 changes: 49 additions & 2 deletions packages/dev/s2-docs/pages/react-aria/Tree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import vanillaDocs from 'docs:vanilla-starter/Tree';
import '../../tailwind/tailwind.css';
import Anatomy from 'react-aria-components/docs/TreeAnatomy.svg';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2'
import {VersionBadge} from '../../src/VersionBadge';

export const tags = ['data', 'tree', 'nested', 'hierarchy'];
export const relatedPages = [
Expand Down Expand Up @@ -237,6 +238,40 @@ import {Tree} from 'vanilla-starter/Tree';
</Tree>
```

### Sections <VersionBadge version="alpha" />

Use the `<TreeSection>` component to group options. A `<TreeHeader>` element may also be included to label the section. Sections without a header must have an `aria-label`.

```tsx render
"use client";
import {Tree, TreeHeader, TreeItem, TreeSection} from 'vanilla-starter/Tree';

<Tree aria-label="Files">
<TreeSection>
<TreeHeader>Photos</TreeHeader>
<TreeItem id="my-photos" title="My Photos">
<TreeItem id="photo-1" title="Photo 1" />
<TreeItem id="photo-2" title="Photo 2" />
</TreeItem>
<TreeItem id="shared-photos" title="Shared Photos">
<TreeItem id="shared-photo-1" title="Shared Photo 1" />
<TreeItem id="shared-photo-2" title="Shared Photo 2" />
</TreeItem>
</TreeSection>
<TreeSection>
<TreeHeader>Documents</TreeHeader>
<TreeItem id="my-documents" title="My Documents">
<TreeItem id="document-1" title="Document 1" />
<TreeItem id="document-2" title="Document 2" />
</TreeItem>
<TreeItem id="shared-documents" title="Shared Documents">
<TreeItem id="shared-document-1" title="Shared Document 1" />
<TreeItem id="shared-document-2" title="Shared Document 2" />
</TreeItem>
</TreeSection>
</Tree>
```

## Selection and actions

Use the `selectionMode` prop to enable single or multiple selection. The selected items can be controlled via the `selectedKeys` prop, matching the `id` prop of the items. The `onAction` event handles item actions. Items can be disabled with the `isDisabled` prop. See the [selection guide](selection?component=Tree) for more details.
Expand Down Expand Up @@ -367,7 +402,7 @@ function Example() {

<Anatomy role="img" aria-label="Anatomy diagram of a Tree, consisting of multiple items. Each item in the tree contains a selection checkbox, an expand toggle button, and the name of the item." />

```tsx links={{Tree: '#tree', TreeItem: '#treeitem', TreeItemContent: '#treeitemcontent', Button: 'Button', Checkbox: 'Checkbox', TreeLoadMoreItem: '#treeloadmoreitem', SelectionIndicator: 'selection#animated-selectionindicator'}}
```tsx links={{Tree: '#tree', TreeItem: '#treeitem', TreeItemContent: '#treeitemcontent', TreeSection: '#treesection', TreeHeader: '#treeheader', Button: 'Button', Checkbox: 'Checkbox', TreeLoadMoreItem: '#treeloadmoreitem', SelectionIndicator: 'selection#animated-selectionindicator'}}
<Tree>
<TreeItem>
<TreeItemContent>
Expand All @@ -378,8 +413,12 @@ function Example() {
<TreeItem>
{/* ... */}
</TreeItem>
<TreeLoadMoreItem />
</TreeItem>
<TreeSection>
<TreeHeader />
<TreeItem>{/* ... */}</TreeItem>
</TreeSection>
<TreeLoadMoreItem />
</Tree>
```

Expand All @@ -401,6 +440,14 @@ function Example() {

<PropTable component={docs.exports.TreeItemContent} links={docs.links} showDescription hideRenderProps />

### TreeSection

<PropTable component={docs.exports.TreeSection} links={docs.links} showDescription />

### TreeHeader

`<TreeHeader>` labels the section within a Tree. It accepts all DOM attributes.

### TreeLoadMoreItem

<PropTable
Expand Down
11 changes: 11 additions & 0 deletions starters/docs/src/Tree.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
--drag-button-width: 0px;
--checkbox-width: 0px;

.react-aria-TreeSection:not(:first-child) {
margin-top: var(--spacing-4);
}

.react-aria-TreeHeader {
font-size: var(--font-size-lg);
font-weight: 500;
padding: var(--spacing-1) var(--spacing-2);
color: var(--text-color);
}

&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: -1px;
Expand Down
16 changes: 15 additions & 1 deletion starters/docs/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
TreeItemProps as AriaTreeItemProps,
TreeProps,
TreeLoadMoreItem as AriaTreeLoadMoreItem,
TreeLoadMoreItemProps
TreeLoadMoreItemProps,
TreeSection as AriaTreeSection,
TreeHeader as AriaTreeHeader
} from 'react-aria-components';
import {ChevronRight, GripVertical} from 'lucide-react';
import {Checkbox} from './Checkbox';
Expand Down Expand Up @@ -67,3 +69,15 @@ export function TreeLoadMoreItem(props: TreeLoadMoreItemProps) {
</AriaTreeLoadMoreItem>
);
}

export function TreeSection<T extends object>(
props: React.ComponentProps<typeof AriaTreeSection>
) {
return <AriaTreeSection {...props} />;
}

export function TreeHeader(
props: React.ComponentProps<typeof AriaTreeHeader>
) {
return <AriaTreeHeader {...props} />;
}
Loading