From bb12911a602e464fc95fa0eb138cf8c8384d067b Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:32:44 -0800 Subject: [PATCH 1/9] Merge branch 'main' into tree-section --- packages/@react-spectrum/s2/src/TreeView.tsx | 76 +++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index defed595152..4f95bf9b6ab 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -32,14 +32,14 @@ import { import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; import Chevron from '../ui-icons/Chevron'; -import {DOMRef, forwardRefType, GlobalDOMAttributes, Key, LoadingState} from '@react-types/shared'; +import {DOMRef, forwardRefType, GlobalDOMAttributes, Key, LoadingState, SelectionBehavior} from '@react-types/shared'; import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react'; +import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; import {Text, TextContext} from './Content'; import {useActionBarContainer} from './ActionBar'; import {useDOMRef} from '@react-spectrum/utils'; @@ -53,7 +53,15 @@ interface S2TreeProps { renderActionBar?: (selectedKeys: 'all' | Set) => ReactElement } -export interface TreeViewProps extends Omit, 'style' | 'className' | 'render' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps { +interface TreeViewStyleProps { + /** + * How selection should be displayed. + * @default 'checkbox' + */ + selectionStyle?: 'highlight' | 'checkbox' +} + +export interface TreeViewProps extends Omit, 'style' | 'className' | 'render' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks' | keyof GlobalDOMAttributes>, UnsafeStyles, S2TreeProps, TreeViewStyleProps { /** Spectrum-defined styles, returned by the `style()` macro. */ styles?: StylesPropWithHeight } @@ -109,11 +117,14 @@ const tree = style({ } }); +let InternalTreeViewContext = createContext<{selectionStyle?: 'highlight' | 'checkbox'}>({}); + + /** * A tree view provides users with a way to navigate nested hierarchical information. */ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function TreeView(props: TreeViewProps, ref: DOMRef) { - let {children, UNSAFE_className, UNSAFE_style} = props; + let {children, selectionStyle = 'checkbox', UNSAFE_className, UNSAFE_style} = props; let scale = useScale(); let renderer; @@ -137,20 +148,22 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr rowHeight: scale === 'large' ? 50 : 40 }}> - 0 ? actionBarHeight + 8 : 0, - scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 - }} - className={tree} - selectionBehavior="toggle" - selectedKeys={selectedKeys} - defaultSelectedKeys={undefined} - onSelectionChange={onSelectionChange} - ref={scrollRef}> - {props.children} - + + 0 ? actionBarHeight + 8 : 0, + scrollPaddingBottom: actionBarHeight > 0 ? actionBarHeight + 8 : 0 + }} + className={tree} + selectionBehavior={selectionStyle === 'highlight' ? 'replace' : 'toggle'} + selectedKeys={selectedKeys} + defaultSelectedKeys={undefined} + onSelectionChange={onSelectionChange} + ref={scrollRef}> + {props.children} + + {actionBar} @@ -164,10 +177,10 @@ const rowBackgroundColor = { isHovered: colorMix('gray-25', 'gray-900', 7), isPressed: colorMix('gray-25', 'gray-900', 10), isSelected: { - default: colorMix('gray-25', 'gray-900', 7), - isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 10), - isHovered: colorMix('gray-25', 'gray-900', 10), - isPressed: colorMix('gray-25', 'gray-900', 10) + default: colorMix('gray-100', 'gray-900', 7), + isFocusVisibleWithin: colorMix('gray-200', 'gray-900', 10), + isHovered: colorMix('gray-200', 'gray-900', 10), + isPressed: colorMix('gray-200', 'gray-900', 10) }, forcedColors: { default: 'Background' @@ -201,7 +214,8 @@ const treeRow = style({ default: 'focus-ring', forcedColors: 'Highlight' } - } + }, + borderRadius: 'sm' }); const treeCellGrid = style({ @@ -209,6 +223,7 @@ const treeCellGrid = style({ width: 'full', height: 'full', boxSizing: 'border-box', + borderRadius: 'sm', alignContent: 'center', alignItems: 'center', gridTemplateColumns: ['auto', 'auto', 'auto', 'auto', 'auto', '1fr', 'minmax(0, auto)', 'auto'], @@ -216,7 +231,14 @@ const treeCellGrid = style({ gridTemplateAreas: [ 'drag-handle checkbox level-padding expand-button icon content actions actionmenu' ], - backgroundColor: '--rowBackgroundColor', + backgroundColor: { + default: '--rowBackgroundColor', + selectionStyle: { + highlight: { + isSelected: colorMix('gray-25', 'blue-900', 10) + } + } + }, paddingEnd: 4, // account for any focus rings on the last item in the cell color: { isDisabled: { @@ -320,6 +342,8 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode } = props; let scale = useScale(); + let {selectionStyle} = useContext(InternalTreeViewContext); + return ( {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isSelected, id, state}) => { @@ -331,8 +355,8 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode } let isFirst = state.collection.getFirstKey() === id; return ( -
- {selectionMode !== 'none' && selectionBehavior === 'toggle' && ( +
+ {selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle !== 'highlight' && ( // TODO: add transition?
From a0accf193d630c5af0ad913619ca9ed292a60352 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:45:29 -0700 Subject: [PATCH 2/9] feat: add highlight selection to TreeView --- packages/@react-spectrum/s2/src/TreeView.tsx | 184 ++++++++++++++++--- 1 file changed, 155 insertions(+), 29 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 4f95bf9b6ab..a3f0fa66d4b 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -26,13 +26,14 @@ import { TreeItemContentProps, TreeLoadMoreItem, TreeLoadMoreItemProps, + TreeState, useContextProps, Virtualizer } from 'react-aria-components'; import {centerBaseline} from './CenterBaseline'; import {Checkbox} from './Checkbox'; import Chevron from '../ui-icons/Chevron'; -import {DOMRef, forwardRefType, GlobalDOMAttributes, Key, LoadingState, SelectionBehavior} from '@react-types/shared'; +import {DOMRef, forwardRefType, GlobalDOMAttributes, Key, LoadingState} from '@react-types/shared'; import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {IconContext} from './Icon'; // @ts-ignore @@ -175,12 +176,11 @@ const rowBackgroundColor = { default: '--s2-container-bg', isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), isHovered: colorMix('gray-25', 'gray-900', 7), - isPressed: colorMix('gray-25', 'gray-900', 10), isSelected: { - default: colorMix('gray-100', 'gray-900', 7), - isFocusVisibleWithin: colorMix('gray-200', 'gray-900', 10), - isHovered: colorMix('gray-200', 'gray-900', 10), - isPressed: colorMix('gray-200', 'gray-900', 10) + default: 'gray-100', + isFocusVisibleWithin: 'gray-200', + isHovered: 'gray-200', + isPressed: 'gray-200' }, forcedColors: { default: 'Background' @@ -188,6 +188,8 @@ const rowBackgroundColor = { } as const; const treeRow = style({ + ...focusRing(), + outlineOffset: -2, position: 'relative', display: 'flex', height: 40, @@ -199,7 +201,7 @@ const treeRow = style({ isSelected: baseColor('neutral'), forcedColors: 'ButtonText' }, - outlineStyle: 'none', + // outlineStyle: 'none', cursor: { default: 'default', isLink: 'pointer' @@ -208,14 +210,34 @@ const treeRow = style({ type: 'backgroundColor', value: rowBackgroundColor }, - '--rowFocusIndicatorColor': { - type: 'outlineColor', - value: { - default: 'focus-ring', - forcedColors: 'Highlight' - } + // '--rowFocusIndicatorColor': { + // type: 'outlineColor', + // value: { + // default: 'focus-ring', + // forcedColors: 'Highlight' + // } + // }, + '--borderRadiusTreeItem': { + type: 'borderTopStartRadius', + value: 'sm' }, borderRadius: 'sm' + // borderTopStartRadius: { + // default: '--borderRadiusTreeItem', + // isPreviousSelected: 'none' + // }, + // borderTopEndRadius: { + // default: '--borderRadiusTreeItem', + // isPreviousSelected: 'none' + // }, + // borderBottomStartRadius: { + // default: '--borderRadiusTreeItem', + // isNextSelected: 'none' + // }, + // borderBottomEndRadius: { + // default: '--borderRadiusTreeItem', + // isNextSelected: 'none' + // } }); const treeCellGrid = style({ @@ -231,14 +253,6 @@ const treeCellGrid = style({ gridTemplateAreas: [ 'drag-handle checkbox level-padding expand-button icon content actions actionmenu' ], - backgroundColor: { - default: '--rowBackgroundColor', - selectionStyle: { - highlight: { - isSelected: colorMix('gray-25', 'blue-900', 10) - } - } - }, paddingEnd: 4, // account for any focus rings on the last item in the cell color: { isDisabled: { @@ -260,6 +274,78 @@ const treeCellGrid = style({ default: 'focus-ring', forcedColors: 'Highlight' } + }, + '--borderColor': { + type: 'borderColor', + value: { + default: 'transparent', + selectionStyle: { + highlight: 'blue-900' + }, + forcedColors: 'ButtonBorder' + } + } +}); + +const treeRowBackground = style({ + position: 'absolute', + zIndex: -1, + inset: 0, + backgroundColor: { + default: '--rowBackgroundColor', + selectionStyle: { + highlight: { + default: '--rowBackgroundColor', + isSelected: { + default: colorMix('gray-25', 'blue-900', 10), + isHovered: colorMix('gray-25', 'blue-900', 15), + isFocusVisible: colorMix('gray-25', 'blue-900', 15) + } + } + } + }, + borderTopStartRadius: { + default: '--borderRadiusTreeItem', + isPreviousSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + } + }, + borderTopEndRadius: { + default: '--borderRadiusTreeItem', + isPreviousSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + } + }, + borderBottomStartRadius: { + default: '--borderRadiusTreeItem', + isNextSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + } + }, + borderBottomEndRadius: { + default: '--borderRadiusTreeItem', + isNextSelected: { + default: '--borderRadiusTreeItem', + isSelected: 'none' + } + }, + borderTopWidth: { + default: 1, + isPreviousSelected: 0 + }, + borderBottomWidth: { + default: 1, + isNextSelected: 0 + }, + borderStartWidth: 1, + borderEndWidth: 1, + borderStyle: 'solid', + borderColor: { + default: 'transparent', + isSelected: '--borderColor' } }); @@ -326,7 +412,8 @@ export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { {...props} className={(renderProps) => treeRow({ ...renderProps, - isLink: !!href + isLink: !!href, + isPreviousSelected: isPrevSelected(renderProps.id, renderProps.state) }) + (renderProps.isFocusVisible ? ' ' + treeRowFocusIndicator : '')} /> ); }; @@ -346,16 +433,17 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode return ( - {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isSelected, id, state}) => { - let isNextSelected = false; + {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isSelected, id, state, isHovered, isFocusVisible}) => { + // let isNextSelected = false; let isNextFocused = false; - let keyAfter = state.collection.getKeyAfter(id); - if (keyAfter != null) { - isNextSelected = state.selectionManager.isSelected(keyAfter); - } + // let keyAfter = state.collection.getKeyAfter(id); + // if (keyAfter != null) { + // isNextSelected = state.selectionManager.isSelected(keyAfter); + // } let isFirst = state.collection.getFirstKey() === id; return ( -
+
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle !== 'highlight' && ( // TODO: add transition?
@@ -483,3 +571,41 @@ function ExpandableRowChevron(props: ExpandableRowChevronProps) { ); } + +function isNextSelected(id: Key | undefined, state: TreeState) { + if (id == null || !state) { + return false; + } + let keyAfter = state.collection.getKeyAfter(id); + + // skip nodes that are not item nodes because the selection manager will map non-item nodes to their parent item before checking selection + let node = keyAfter ? state.collection.getItem(keyAfter) : null; + while (node && node.type !== 'item' && keyAfter) { + keyAfter = state.collection.getKeyAfter(keyAfter); + node = keyAfter ? state.collection.getItem(keyAfter) : null; + } + + return keyAfter != null && state.selectionManager.isSelected(keyAfter); +} + +function isPrevSelected(id: Key | undefined, state: TreeState) { + if (id == null || !state) { + return false; + } + let keyBefore = state.collection.getKeyBefore(id); + return keyBefore != null && state.selectionManager.isSelected(keyBefore); +} + +// function isFirstItem(id: Key | undefined, state: TreeState) { +// if (id == null || !state) { +// return false; +// } +// return state.collection.getFirstKey() === id; +// } + +// function isLastItem(id: Key | undefined, state: TreeState) { +// if (id == null || !state) { +// return false; +// } +// return state.collection.getLastKey() === id; +// } From ba3ff24a559da1a55b94ce07d4c7a3b7e527be78 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:32:15 -0700 Subject: [PATCH 3/9] clean up --- packages/@react-spectrum/s2/src/TreeView.tsx | 60 +++----------------- 1 file changed, 8 insertions(+), 52 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index a3f0fa66d4b..e527ff98195 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -120,7 +120,6 @@ const tree = style({ let InternalTreeViewContext = createContext<{selectionStyle?: 'highlight' | 'checkbox'}>({}); - /** * A tree view provides users with a way to navigate nested hierarchical information. */ @@ -176,11 +175,12 @@ const rowBackgroundColor = { default: '--s2-container-bg', isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 7), isHovered: colorMix('gray-25', 'gray-900', 7), + isPressed: colorMix('gray-25', 'gray-900', 7), isSelected: { - default: 'gray-100', - isFocusVisibleWithin: 'gray-200', - isHovered: 'gray-200', - isPressed: 'gray-200' + default: colorMix('gray-25', 'gray-900', 7), + isFocusVisibleWithin: colorMix('gray-25', 'gray-900', 10), + isHovered: colorMix('gray-25', 'gray-900', 10), + isPressed: colorMix('gray-25', 'gray-900', 10) }, forcedColors: { default: 'Background' @@ -201,7 +201,6 @@ const treeRow = style({ isSelected: baseColor('neutral'), forcedColors: 'ButtonText' }, - // outlineStyle: 'none', cursor: { default: 'default', isLink: 'pointer' @@ -210,34 +209,11 @@ const treeRow = style({ type: 'backgroundColor', value: rowBackgroundColor }, - // '--rowFocusIndicatorColor': { - // type: 'outlineColor', - // value: { - // default: 'focus-ring', - // forcedColors: 'Highlight' - // } - // }, '--borderRadiusTreeItem': { type: 'borderTopStartRadius', value: 'sm' }, borderRadius: 'sm' - // borderTopStartRadius: { - // default: '--borderRadiusTreeItem', - // isPreviousSelected: 'none' - // }, - // borderTopEndRadius: { - // default: '--borderRadiusTreeItem', - // isPreviousSelected: 'none' - // }, - // borderBottomStartRadius: { - // default: '--borderRadiusTreeItem', - // isNextSelected: 'none' - // }, - // borderBottomEndRadius: { - // default: '--borderRadiusTreeItem', - // isNextSelected: 'none' - // } }); const treeCellGrid = style({ @@ -295,7 +271,8 @@ const treeRowBackground = style({ default: '--rowBackgroundColor', selectionStyle: { highlight: { - default: '--rowBackgroundColor', + isHovered: colorMix('gray-25', 'gray-900', 7), + isPressed: colorMix('gray-25', 'gray-900', 7), isSelected: { default: colorMix('gray-25', 'blue-900', 10), isHovered: colorMix('gray-25', 'blue-900', 15), @@ -434,15 +411,8 @@ export const TreeViewItemContent = (props: TreeViewItemContentProps): ReactNode return ( {({isExpanded, hasChildItems, selectionMode, selectionBehavior, isDisabled, isSelected, id, state, isHovered, isFocusVisible}) => { - // let isNextSelected = false; - let isNextFocused = false; - // let keyAfter = state.collection.getKeyAfter(id); - // if (keyAfter != null) { - // isNextSelected = state.selectionManager.isSelected(keyAfter); - // } - let isFirst = state.collection.getFirstKey() === id; return ( -
+
{selectionMode !== 'none' && selectionBehavior === 'toggle' && selectionStyle !== 'highlight' && ( // TODO: add transition? @@ -595,17 +565,3 @@ function isPrevSelected(id: Key | undefined, state: TreeState) { let keyBefore = state.collection.getKeyBefore(id); return keyBefore != null && state.selectionManager.isSelected(keyBefore); } - -// function isFirstItem(id: Key | undefined, state: TreeState) { -// if (id == null || !state) { -// return false; -// } -// return state.collection.getFirstKey() === id; -// } - -// function isLastItem(id: Key | undefined, state: TreeState) { -// if (id == null || !state) { -// return false; -// } -// return state.collection.getLastKey() === id; -// } From 1bb38f3a38690e612bdddf59fd0266dcd9b90d45 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:58:06 -0700 Subject: [PATCH 4/9] fixes --- packages/@react-spectrum/s2/src/TreeView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index e527ff98195..4a6c367865d 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -271,11 +271,11 @@ const treeRowBackground = style({ default: '--rowBackgroundColor', selectionStyle: { highlight: { - isHovered: colorMix('gray-25', 'gray-900', 7), - isPressed: colorMix('gray-25', 'gray-900', 7), + default: '--rowBackgroundColor', isSelected: { default: colorMix('gray-25', 'blue-900', 10), isHovered: colorMix('gray-25', 'blue-900', 15), + isPressed: colorMix('gray-25', 'blue-900', 15), isFocusVisible: colorMix('gray-25', 'blue-900', 15) } } From f037a065140a9a326a1c5f06448c77173200ba68 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:59:43 -0700 Subject: [PATCH 5/9] update comment --- packages/@react-spectrum/s2/src/TreeView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 4a6c367865d..16c72347c8c 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -548,7 +548,7 @@ function isNextSelected(id: Key | undefined, state: TreeState) { } let keyAfter = state.collection.getKeyAfter(id); - // skip nodes that are not item nodes because the selection manager will map non-item nodes to their parent item before checking selection + // We need to skip non-item nodes because the selection manager will map non-item nodes to their parent before checking selection let node = keyAfter ? state.collection.getItem(keyAfter) : null; while (node && node.type !== 'item' && keyAfter) { keyAfter = state.collection.getKeyAfter(keyAfter); From ace0682013def2418511e746fd5f23fb61400eb6 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:12:02 -0700 Subject: [PATCH 6/9] add highlight selection to docs --- packages/dev/s2-docs/pages/s2/TreeView.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/s2/TreeView.mdx b/packages/dev/s2-docs/pages/s2/TreeView.mdx index f8043e4e813..2bcb0ecaacb 100644 --- a/packages/dev/s2-docs/pages/s2/TreeView.mdx +++ b/packages/dev/s2-docs/pages/s2/TreeView.mdx @@ -14,7 +14,7 @@ export const description = 'Displays hierarchical data with selection and collap {docs.exports.TreeView.description} -```tsx render docs={docs.exports.TreeView} links={docs.links} props={['selectionMode']} initialProps={{selectionMode: 'multiple'}} type="s2" wide +```tsx render docs={docs.exports.TreeView} links={docs.links} props={['selectionMode', 'selectionStyle']} initialProps={{selectionMode: 'multiple', selectionStyle: 'checkbox'}} type="s2" wide "use client"; import {TreeView, TreeViewItem, TreeViewItemContent} from '@react-spectrum/s2'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; From 2bada047c4cad672f842edaae7182f63dd474735 Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:15:02 -0700 Subject: [PATCH 7/9] update hcm --- packages/@react-spectrum/s2/src/TreeView.tsx | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 16c72347c8c..b9310d1a2d0 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -190,6 +190,15 @@ const rowBackgroundColor = { const treeRow = style({ ...focusRing(), outlineOffset: -2, + outlineColor: { + default: 'focus-ring', + forcedColors: { + default: 'Highlight', + selectionStyle: { + highlight: 'ButtonBorder' + } + } + }, position: 'relative', display: 'flex', height: 40, @@ -234,6 +243,14 @@ const treeCellGrid = style({ isDisabled: { default: 'gray-400', forcedColors: 'GrayText' + }, + forcedColors: { + default: 'ButtonText', + selectionStyle: { + highlight: { + isSelected: 'HighlightText' + } + } } }, '--rowSelectedBorderColor': { @@ -260,7 +277,8 @@ const treeCellGrid = style({ }, forcedColors: 'ButtonBorder' } - } + }, + forcedColorAdjust: 'none' }); const treeRowBackground = style({ @@ -279,6 +297,14 @@ const treeRowBackground = style({ isFocusVisible: colorMix('gray-25', 'blue-900', 15) } } + }, + forcedColors: { + default: 'Background', + selectionStyle: { + highlight: { + isSelected: 'Highlight' + } + } } }, borderTopStartRadius: { @@ -383,6 +409,7 @@ export const TreeViewItem = (props: TreeViewItemProps): ReactNode => { let { href } = props; + let {selectionStyle} = useContext(InternalTreeViewContext); return ( { className={(renderProps) => treeRow({ ...renderProps, isLink: !!href, + selectionStyle, isPreviousSelected: isPrevSelected(renderProps.id, renderProps.state) }) + (renderProps.isFocusVisible ? ' ' + treeRowFocusIndicator : '')} /> ); From 9f377d247e2d0c69e2968f5324edcc75f497420a Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:19:41 -0700 Subject: [PATCH 8/9] update chromatic --- .../@react-spectrum/s2/chromatic/TreeView.stories.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/chromatic/TreeView.stories.tsx b/packages/@react-spectrum/s2/chromatic/TreeView.stories.tsx index b718f1702c0..9c152d539dd 100644 --- a/packages/@react-spectrum/s2/chromatic/TreeView.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/TreeView.stories.tsx @@ -143,7 +143,7 @@ export const TreeStatic: StoryObj = { render: (args) => }; -export const TreeSelection: StoryObj = { +export const TreeCheckboxSelection: StoryObj = { ...TreeStatic, args: { selectionMode: 'multiple', @@ -151,6 +151,15 @@ export const TreeSelection: StoryObj = { } }; +export const TreeHighlightSelection: StoryObj = { + ...TreeStatic, + args: { + selectionMode: 'multiple', + selectionStyle: 'highlight', + defaultSelectedKeys: ['projects-2', 'projects-3'] + } +}; + export const TreeDisableSelection: StoryObj = { ...TreeStatic, args: { From 4a65c35f907cdc196b292b40ad0db277251fed1b Mon Sep 17 00:00:00 2001 From: Yihui Liao <44729383+yihuiliao@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:13:40 -0700 Subject: [PATCH 9/9] fix forced colors error --- packages/@react-spectrum/s2/src/TreeView.tsx | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index b9310d1a2d0..df2aa24a7a3 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -192,10 +192,10 @@ const treeRow = style({ outlineOffset: -2, outlineColor: { default: 'focus-ring', - forcedColors: { - default: 'Highlight', - selectionStyle: { - highlight: 'ButtonBorder' + forcedColors: 'Highlight', + selectionStyle: { + highlight: { + forcedColors: 'ButtonBorder' } } }, @@ -244,11 +244,11 @@ const treeCellGrid = style({ default: 'gray-400', forcedColors: 'GrayText' }, - forcedColors: { - default: 'ButtonText', - selectionStyle: { - highlight: { - isSelected: 'HighlightText' + forcedColors: 'ButtonText', + selectionStyle: { + highlight: { + isSelected: { + forcedColors: 'HighlightText' } } } @@ -287,6 +287,7 @@ const treeRowBackground = style({ inset: 0, backgroundColor: { default: '--rowBackgroundColor', + forcedColors: 'Background', selectionStyle: { highlight: { default: '--rowBackgroundColor', @@ -294,15 +295,8 @@ const treeRowBackground = style({ default: colorMix('gray-25', 'blue-900', 10), isHovered: colorMix('gray-25', 'blue-900', 15), isPressed: colorMix('gray-25', 'blue-900', 15), - isFocusVisible: colorMix('gray-25', 'blue-900', 15) - } - } - }, - forcedColors: { - default: 'Background', - selectionStyle: { - highlight: { - isSelected: 'Highlight' + isFocusVisible: colorMix('gray-25', 'blue-900', 15), + forcedColors: 'Highlight' } } }