Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: S2 treeview #7343

Merged
merged 44 commits into from
Jan 29, 2025
Merged

feat: S2 treeview #7343

merged 44 commits into from
Jan 29, 2025

Conversation

snowystinger
Copy link
Member

@snowystinger snowystinger commented Nov 8, 2024

Closes

This only implements two of the combinations of isDetached/isEmphasized/Checkbox Selection.

  • isDetached + checkbox selection or no selection -> seemed mostly clear cut
  • isEmphasized + checkbox selection or no selection -> matches table
    The others aren't well defined enough yet to implement.

What's left:

  • Selection states w/ detached+emphasized combos defined by Spectrum
  • Drag and Drop
  • thumbnail slot
  • skeleton/async loading
  • Sections / TreeView headers

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices

📝 Test Instructions:

🧢 Your Project:

@snowystinger snowystinger marked this pull request as ready for review December 3, 2024 05:09
@snowystinger snowystinger changed the title [WIP] - S2 treeview S2 treeview Dec 3, 2024
@rspbot
Copy link

rspbot commented Dec 3, 2024

@snowystinger snowystinger changed the title S2 treeview feature: S2 treeview Dec 3, 2024
@rspbot
Copy link

rspbot commented Jan 20, 2025

Copy link
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkbox seems a bit off center vertically when detached. And I think the gap between items is supposed to be larger according to Figma.

image

packages/@react-spectrum/s2/src/TreeView.tsx Outdated Show resolved Hide resolved
packages/@react-spectrum/s2/src/TreeView.tsx Outdated Show resolved Hide resolved
let {isExpanded, isDisabled} = fullProps;
let {direction} = useLocale();

// Will need to keep the chevron as a button for iOS VO at all times since VO doesn't focus the cell. Also keep as button if cellAction is defined by the user in the future
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not possible with RAC? What do we do in the RAC docs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this got copied over from v3, @LFDanLu any idea what this is about?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the chevron wasn't a button, iOS VO can't focus it and thus users can't toggle expansion of the row. If the tree row didn't have a row action, focusing the tree cell and double tapping would trigger expansion and thus this wouldn't be needed, but iOS doesn't focus grid cells in general, at least for iOS 17.

In RAC, it is up to the user to provide ALL the content of the TreeRow (including the chevron button) themselves hence why https://react-spectrum.adobe.com/react-aria/Tree.html#example has the a Button with the chevron slot. Here in S2/v3 we can afford to do that for the user thus we set it up here. If we want to provide a ChevronButton wrapper of sorts to expose from RAC we could go that route and ditch defining it at the S2/v3 level

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why are we using hooks here? Why is it a span instead of a button?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I remember correctly, it was for the tabIndex handling below. I forget if we had all of the button props that we had before like excludeFromTabOrder and preventFocusOnPress

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to work with the props @LFDanLu has suggested, but will need to verify on any Androids the team has

packages/@react-spectrum/s2/src/TreeView.tsx Show resolved Hide resolved
@rspbot
Copy link

rspbot commented Jan 22, 2025

@rspbot
Copy link

rspbot commented Jan 22, 2025

@rspbot
Copy link

rspbot commented Jan 23, 2025

@rspbot
Copy link

rspbot commented Jan 23, 2025

@rspbot
Copy link

rspbot commented Jan 23, 2025


let layout = useMemo(() => {
return new UNSTABLE_ListLayout({
rowHeight: isDetached ? 44 : 40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm seeing conflicting designs in Figma for the spacing between the detached rows, but the tokens file says that it should use tree-view-item-to-item-detached which is 2px whereas the design file has 8px between each row...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like it should be 2, i've moved it back to that. I'll raise with design after I have a build

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like it should be 2, i've moved it back to that. I'll raise with design after I have a build

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior and design looks good to me, looking at the API update PR before commenting further on the implementation. Seems like it was rather straight forward which is good to see, were there any deviations from RSP TreeView's implementation that I may not have noticed?

Comment on lines +75 to +78
// TODO: the below is needed so the borders of the top and bottom row isn't cut off if the TreeView is wrapped within a container by always reserving the 2px needed for the
// keyboard focus ring. Perhaps find a different way of rendering the outlines since the top of the item doesn't
// scroll into view due to how the ring is offset. Alternatively, have the tree render the top/bottom outline like it does in Listview
const tree = style({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment still valid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm i assume, i copied it from v3 it looks like, i didn't think about it much though

packages/@react-spectrum/s2/src/TreeView.tsx Show resolved Hide resolved
devongovett
devongovett previously approved these changes Jan 28, 2025
Copy link
Member

@devongovett devongovett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine if you want to merge and iterate

packages/@react-spectrum/s2/package.json Outdated Show resolved Hide resolved
packages/@react-spectrum/s2/src/TreeView.tsx Outdated Show resolved Hide resolved
let {isExpanded, isDisabled} = fullProps;
let {direction} = useLocale();

// Will need to keep the chevron as a button for iOS VO at all times since VO doesn't focus the cell. Also keep as button if cellAction is defined by the user in the future
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why are we using hooks here? Why is it a span instead of a button?

borderInlineStartColor: borderColor, // don't know why i can't use renamed
borderInlineEndColor: borderColor,
borderTopColor: borderColor,
borderBottomColor: borderColor,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need these? When do the borders have different colors? Adding these will increase the CSS size across all components so want to be sure it's actually needed. Looks like maybe you could set the width of one of the borders to zero instead of setting it to transparent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using 0/1 instead of 1 always with transparent causes elements to shift their position internal to the tree item

@@ -670,6 +672,10 @@ export const style = createTheme({
borderEndWidth: createRenamedProperty('borderInlineEndWidth', borderWidth),
borderTopWidth: borderWidth,
borderBottomWidth: borderWidth,
borderInlineStartColor: borderColor, // don't know why i can't use renamed
borderInlineEndColor: borderColor,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are keeping these then they should be named like our other logical properties: borderStartColor, borderEndColor

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, as the comment says, i don't know why I can't use the createRenamedProperty like I could for borderEndWidth

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-01-29 at 1 51 52 pm

@rspbot
Copy link

rspbot commented Jan 28, 2025

@rspbot
Copy link

rspbot commented Jan 29, 2025

@rspbot
Copy link

rspbot commented Jan 29, 2025

## API Changes

react-aria-components

/react-aria-components:UNSTABLE_Tree

 UNSTABLE_Tree <T extends {}> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | ({}) => ReactNode
   className?: string | ((TreeRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: Array<any>
-  disabledBehavior?: DisabledBehavior = 'selection'
+  disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   expandedKeys?: Iterable<Key>
   id?: string
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   selectionMode?: SelectionMode
   slot?: string | null
   style?: CSSProperties | ((TreeRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:TreeProps

 TreeProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: string | ((TreeRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: Array<any>
-  disabledBehavior?: DisabledBehavior = 'selection'
+  disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   expandedKeys?: Iterable<Key>
   id?: string
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   selectionMode?: SelectionMode
   slot?: string | null
   style?: CSSProperties | ((TreeRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:TreeItemContentRenderProps

 TreeItemContentRenderProps {
   allowsDragging?: boolean
   hasChildRows: boolean
+  id: Key
   isDisabled: boolean
   isDragging?: boolean
   isDropTarget?: boolean
   isExpanded: boolean
   isFocusVisible: boolean
   isFocusVisibleWithin: boolean
   isFocused: boolean
   isHovered: boolean
   isPressed: boolean
   isSelected: boolean
   level: number
   selectionBehavior: SelectionBehavior
   selectionMode: SelectionMode
+  state: TreeState<unknown>
 }

@react-spectrum/s2

/@react-spectrum/s2:TreeView

+TreeView {
+  UNSAFE_className?: string
+  UNSAFE_style?: CSSProperties
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  defaultExpandedKeys?: Iterable<Key>
+  defaultSelectedKeys?: 'all' | Iterable<Key>
+  dependencies?: Array<any>
+  disabledBehavior?: DisabledBehavior = 'all'
+  disabledKeys?: Iterable<Key>
+  disallowEmptySelection?: boolean
+  expandedKeys?: Iterable<Key>
+  id?: string
+  isDetached?: boolean
+  isEmphasized?: boolean
+  items?: Iterable<T>
+  onAction?: (Key) => void
+  onExpandedChange?: (Set<Key>) => any
+  onSelectionChange?: (Selection) => void
+  renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
+  selectedKeys?: 'all' | Iterable<Key>
+  selectionMode?: SelectionMode
+  slot?: string | null
+  styles?: StylesPropWithHeight
+}

/@react-spectrum/s2:TreeViewItem

+TreeViewItem <T extends {}> {
+  aria-label?: string
+  childItems?: Iterable<{}>
+  children: ReactNode
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  routerOptions?: RouterOptions
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+  value?: T
+}

/@react-spectrum/s2:TreeViewProps

+TreeViewProps {
+  UNSAFE_className?: string
+  UNSAFE_style?: CSSProperties
+  aria-describedby?: string
+  aria-details?: string
+  aria-label?: string
+  aria-labelledby?: string
+  autoFocus?: boolean | FocusStrategy
+  children?: ReactNode | (T) => ReactNode
+  defaultExpandedKeys?: Iterable<Key>
+  defaultSelectedKeys?: 'all' | Iterable<Key>
+  dependencies?: Array<any>
+  disabledBehavior?: DisabledBehavior = 'all'
+  disabledKeys?: Iterable<Key>
+  disallowEmptySelection?: boolean
+  expandedKeys?: Iterable<Key>
+  id?: string
+  isDetached?: boolean
+  isEmphasized?: boolean
+  items?: Iterable<T>
+  onAction?: (Key) => void
+  onExpandedChange?: (Set<Key>) => any
+  onSelectionChange?: (Selection) => void
+  renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
+  selectedKeys?: 'all' | Iterable<Key>
+  selectionMode?: SelectionMode
+  slot?: string | null
+  styles?: StylesPropWithHeight
+}

/@react-spectrum/s2:TreeViewItemProps

+TreeViewItemProps <T extends {} = {}> {
+  aria-label?: string
+  childItems?: Iterable<{}>
+  children: ReactNode
+  download?: boolean | string
+  hasChildItems?: boolean
+  href?: Href
+  hrefLang?: string
+  id?: Key
+  onHoverChange?: (boolean) => void
+  onHoverEnd?: (HoverEvent) => void
+  onHoverStart?: (HoverEvent) => void
+  ping?: string
+  referrerPolicy?: HTMLAttributeReferrerPolicy
+  rel?: string
+  routerOptions?: RouterOptions
+  target?: HTMLAttributeAnchorTarget
+  textValue: string
+  value?: T
+}

Copy link
Member

@LFDanLu LFDanLu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior for focusing the expand chevron seems to still work on iOS VO and Android talkback, and it is still properly skipped on keyboard. Only weird thing I saw in testing was that moving the talkback cursor caused the row to shift visibly but that seems to have been pre-exisiting since the virtualization commit. Not sure is causing that but happy for it to be followup since it is pretty low surface area

@snowystinger snowystinger added this pull request to the merge queue Jan 29, 2025
Merged via the queue into main with commit e3ed3c7 Jan 29, 2025
30 checks passed
@snowystinger snowystinger deleted the s2-treeview branch January 29, 2025 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants