-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Sections in table #4210
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
Sections in table #4210
Conversation
…thout note: this is pretty broken, awaiting feedback from spectrum since it seems like there might be some cases that need to be handled
commited them so they wouldnt just be in my stash
using this information to get rid of the extra wrapping row group when there are sections
parent key of the section is the body but this key isnt in the keymap
renders extra whitespace for some reason
toggling selection on and off in a table with sections behaves strangely
pausing here since collectionbuilder will be changed in the future
…to a section as per feedback from spectrum design meeting
…xist also make ListView section have title as a required prop
…eaders" This reverts commit ec61a57.
|
Build successful! 🎉 |
includes an api update to keyboard delegate methods and table/grid hooks return the keyboard delegate now
fixes case where row column header was a valid drop target, empty section drop indicator, erroneous skipping of after drop position when going from empty section to previous row drop position
this means we dont have to export keyboardDelegate from useTable/useGrid and instead we can have keyboard DnD keyboard navigation centralized in useDroppableCollection by providing itemFilter to the keyAbove/Below getters in Listlayout which ListView and TableView use. Also fixes various bugs I discovered such as focusing a non-valid drop target if the last focused key in the droppable collection was a column header and other bugs with pageup/down during keyboard DnD
|
Build successful! 🎉 |
|
Build successful! 🎉 |
this lead to weird behavior where you actually created new sections. Only dropping ON the section should be allowed, the other drop positions should come from after the preceeding row or before the following row in the section
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
|
Build successful! 🎉 |
| /** A delegate object that implements behavior for keyboard focus movement. */ | ||
| keyboardDelegate?: KeyboardDelegate<unknown> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update to useDropIndicator api, see discussion here for background.
| // TODO: I've changed it so it will announce "insert after"/"insert before" target if the drop position is between the target and a following/preceeding section | ||
| // instead of announcing "insert between target and NEXT/PREV_SECTION_ROW". Gather opinions | ||
| let keyBefore = keyboardDelegate != null ? keyboardDelegate.getKeyAbove(target.key) : collection.getKeyBefore(target.key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
open to opinions here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update so it announces the section name as well, make it announce as "before item A in section BLAH"
| // VoiceOver on MacOS doesn't announce TableView/ListView sections when navigating with arrow keys so we do this ourselves | ||
| // TODO: NVDA announces the section title when navigating into it with arrow keys as "SECTION TITLE grouping" by defualt, so removing isMac() doubles up on the section title announcement | ||
| // a bit. However, this does add an announcement for the number of rows in a section which might be | ||
| // Mobile screen readers don't cause this announcement to fire until focus happens on a row via double tap which is pretty strange | ||
| useUpdateEffect(() => { | ||
| if (isMac() && focusedItem != null && selectionManager.isFocused && sectionKey !== lastSection.current) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open to opinions here if people feel like we wanna make this custom announcement in other non-Mac screenreaders
| // TODO: Chrome VO states "empty row group", doesn't happen in Safari VO. Doesn't happen in NVDA or TalkBack | ||
| role: 'rowgroup', | ||
| 'aria-labelledby': headerId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As stated here, Chrome VO doesn't have the best experience when navigating with arrows keys. Using control + option + arrow keys doesn't run into this problem and may be the preferred way that VO users will navigate through the table in general though. Will require some more exploration with different aria configurations but IMO this is ok for a first iteration (e.g. trying aria-posinset, aria-level, and aria-setsize instead of aria-rowindex resulted in a subpar experience)
|
|
||
| // Override TS for TableSection to require title prop. | ||
| const SpectrumTableSection = TableSection as <T>(props: SpectrumTableSectionProps<T>) => JSX.Element; | ||
| export {SpectrumTableSection as TableSection}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note we are exporting a TableSection and not reusing the Section collection. This feels like it makes more sense since all of Table's collection components are table specific anyways
| } | ||
|
|
||
| getKeyAbove(key: Key): Key | null { | ||
| getKeyAbove(key: Key, itemFilter: (item: Node<T>) => boolean = item => item.type === 'item'): Key | null { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the API change here to the getKey methods for ListLayout. This is so we can include sections as valid return keys from these methods ONLY during drag and drop. I'm a bit torn if modifying ListLayout is the best place to do this, but it does allow ListView's DnD to use the same code this way since they both provide a layout to useDroppableCollection.
Alternatives:
- create a Table DnD specific keyboard delegate
- Where would this live? Is it too specific? Opted not to do this for these reasons
- Modifying the Table/GridKeyboardDelegate
getKeygetters instead
| // Skip persisted rows that overlap with visible cells. | ||
| while (persistedRowIndices && persistIndex < persistedRowIndices.length && persistedRowIndices[persistIndex] < i) { | ||
| } else { | ||
| // Note: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A variety of notes here to remind myself of the format of various pieces of data, happy to remove if people feel like it isn't helpful
| // TODO: discuss how best to indicate that a row/item is not selectable without making it disabled | ||
| // Some general item prop (isInert, notSelectable)? isSectionHeader might be too specific. | ||
| // Maybe not an item prop? Maybe I shouldn't be making the section header an actual row/item in the collection | ||
| if (!item || (item.type === 'cell' && !this.allowsCellSelection) || item.props?.isSectionHeader) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open question, maybe it would be useful to have a generic prop that marks a node as non-selectable? Perhaps a non-factor when moving to RAC collections
|
|
||
| export interface KeyboardDelegate { | ||
| export interface KeyboardDelegate<T> { | ||
| // TODO: what do we think about this update to the keyboard delegate api? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned previously, changes in this PR include a update to the options provided to KeyboardDelegate's getKey getters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try doing T extends object instead to make this a non-breaking change for someone who made their own keyboard delegate
EDIT: Maybe I could make it unknown instead? Or change it so it is typeFilter: (type) => boolean? Is it actually a breaking change? Wouldn't someone implementing a KeyboardDelegate need a generic for their collection typing?
|
Build successful! 🎉 |
| export { | ||
| TableHeader, | ||
| TableBody, | ||
| Section, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep this in case someone was using it
as per discussion with team, would be nice to have some context for insertion indicators immediatly before or after a section. Also addressed some review comments
|
Build successful! 🎉 |
|
## API Changes
unknown top level export { type: 'identifier', name: 'Column' } @react-aria/autocompleteAriaSearchAutocompleteOptions AriaSearchAutocompleteOptions<T> {
inputRef: RefObject<HTMLInputElement>
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
listBoxRef: RefObject<HTMLElement>
popoverRef: RefObject<HTMLDivElement>
}it changed:
@react-aria/comboboxAriaComboBoxOptions AriaComboBoxOptions<T> {
buttonRef?: RefObject<Element>
inputRef: RefObject<HTMLInputElement>
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
listBoxRef: RefObject<HTMLElement>
popoverRef: RefObject<Element>
}it changed:
@react-aria/dndDroppableCollectionOptions DroppableCollectionOptions {
dropTargetDelegate: DropTargetDelegate
- keyboardDelegate: KeyboardDelegate
+ keyboardDelegate: KeyboardDelegate<unknown>
}it changed:
DropIndicatorProps DropIndicatorProps {
+ keyboardDelegate?: KeyboardDelegate<unknown>
target: DropTarget
}it changed:
@react-aria/griduseGridchanged by:
useGrid<T> {
- props: GridProps
+ props: GridProps<T>
state: GridState<T, GridCollection<T>>
ref: RefObject<HTMLElement>
returnVal: undefined
}GridProps-GridProps {
- focusMode?: 'row' | 'cell' = 'row'
- getRowText?: (Key) => string = (key) => state.collection.getItem(key)?.textValue
- isVirtualized?: boolean
- keyboardDelegate?: KeyboardDelegate
- onCellAction?: (Key) => void
- onRowAction?: (Key) => void
- scrollRef?: RefObject<HTMLElement>
-}
+it changed:
GridRowProps GridRowProps<T> {
isVirtualized?: boolean
- node: Node<T>
+ node: GridNode<T>
shouldSelectOnPressUp?: boolean
}it changed:
useGridSectionAnnouncement-
+useGridSectionAnnouncement<T> {
+ state: GridSelectionState<T>
+ returnVal: undefined
+}undefined-
+GridProps<T> {
+ focusMode?: 'row' | 'cell' = 'row'
+ getRowText?: (Key) => string = (key) => state.collection.getItem(key)?.textValue
+ isVirtualized?: boolean
+ keyboardDelegate?: KeyboardDelegate<T>
+ onCellAction?: (Key) => void
+ onRowAction?: (Key) => void
+ scrollRef?: RefObject<HTMLElement>
+}@react-aria/gridlistAriaGridListOptions AriaGridListOptions<T> {
disabledBehavior?: DisabledBehavior
isVirtualized?: boolean
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
onAction?: (Key) => void
}it changed:
@react-aria/listboxAriaListBoxProps AriaListBoxOptions<T> {
isVirtualized?: boolean
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
shouldFocusOnHover?: boolean
shouldSelectOnPressUp?: boolean
shouldUseVirtualFocus?: boolean
}@react-aria/menuAriaMenuOptions AriaMenuOptions<T> {
isVirtualized?: boolean
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
}it changed:
@react-aria/selectAriaSelectOptions AriaSelectOptions<T> {
- keyboardDelegate?: KeyboardDelegate
+ keyboardDelegate?: KeyboardDelegate<T>
}it changed:
@react-aria/selectionuseSelectableCollectionchanged by:
-useSelectableCollection {
- options: AriaSelectableCollectionOptions
- returnVal: undefined
-}
+useSelectableListchanged by:
-useSelectableList {
- props: AriaSelectableListOptions
- returnVal: undefined
-}
+AriaSelectableCollectionOptions-AriaSelectableCollectionOptions {
- allowsTabNavigation?: boolean
- autoFocus?: boolean | FocusStrategy = false
- disallowEmptySelection?: boolean = false
- disallowSelectAll?: boolean = false
- disallowTypeAhead?: boolean = false
- isVirtualized?: boolean
- keyboardDelegate: KeyboardDelegate
- ref: RefObject<HTMLElement>
- scrollRef?: RefObject<HTMLElement>
- selectOnFocus?: boolean = false
- selectionManager: MultipleSelectionManager
- shouldFocusWrap?: boolean = false
- shouldUseVirtualFocus?: boolean
-}
+it changed:
AriaSelectableListOptions-AriaSelectableListOptions {
- allowsTabNavigation?: boolean
- autoFocus?: boolean | FocusStrategy = false
- collection: Collection<Node<unknown>>
- disabledKeys: Set<Key>
- disallowEmptySelection?: boolean = false
- disallowTypeAhead?: boolean = false
- isVirtualized?: boolean
- keyboardDelegate?: KeyboardDelegate
- ref?: RefObject<HTMLElement>
- selectOnFocus?: boolean = false
- selectionManager: MultipleSelectionManager
- shouldFocusWrap?: boolean = false
- shouldUseVirtualFocus?: boolean
-}
+it changed:
AriaTypeSelectOptions AriaTypeSelectOptions {
- keyboardDelegate: KeyboardDelegate
+ keyboardDelegate: KeyboardDelegate<unknown>
onTypeSelect?: (Key) => void
selectionManager: MultipleSelectionManager
}it changed:
undefined-
+useSelectableCollection<T> {
+ options: AriaSelectableCollectionOptions<T>
+ returnVal: undefined
+}undefined-
+useSelectableList<T> {
+ props: AriaSelectableListOptions<T>
+ returnVal: undefined
+}undefined-
+AriaSelectableCollectionOptions<T> {
+ allowsTabNavigation?: boolean
+ autoFocus?: boolean | FocusStrategy = false
+ disallowEmptySelection?: boolean = false
+ disallowSelectAll?: boolean = false
+ disallowTypeAhead?: boolean = false
+ isVirtualized?: boolean
+ keyboardDelegate: KeyboardDelegate<T>
+ ref: RefObject<HTMLElement>
+ scrollRef?: RefObject<HTMLElement>
+ selectOnFocus?: boolean = false
+ selectionManager: MultipleSelectionManager
+ shouldFocusWrap?: boolean = false
+ shouldUseVirtualFocus?: boolean
+}undefined-
+AriaSelectableListOptions<T> {
+ allowsTabNavigation?: boolean
+ autoFocus?: boolean | FocusStrategy = false
+ collection: Collection<Node<unknown>>
+ disabledKeys: Set<Key>
+ disallowEmptySelection?: boolean = false
+ disallowTypeAhead?: boolean = false
+ isVirtualized?: boolean
+ keyboardDelegate?: KeyboardDelegate<T>
+ ref?: RefObject<HTMLElement>
+ selectOnFocus?: boolean = false
+ selectionManager: MultipleSelectionManager
+ shouldFocusWrap?: boolean = false
+ shouldUseVirtualFocus?: boolean
+}@react-aria/tableGridRowProps GridRowProps<T> {
isVirtualized?: boolean
- node: Node<T>
+ node: GridNode<T>
shouldSelectOnPressUp?: boolean
}it changed:
useTableSectionchanged by:
-
+useTableSection<T> {
+ props: AriaTableSectionProps<T>
+ state: TableState<T>
+ returnVal: undefined
+}AriaTableSectionProps-
+AriaTableSectionProps<T> {
+ isVirtualized?: boolean
+ node: GridNode<T>
+}it changed:
TableSectionAria-
+TableSectionAria {
+ gridCellProps: DOMAttributes
+ rowGroupProps: DOMAttributes
+ rowProps: DOMAttributes
+}it changed:
@react-stately/layoutListLayout ListLayout<T> {
allowDisabledKeyFocus: boolean
buildChild: (Node<T>, number, number) => LayoutNode
buildCollection: () => Array<LayoutNode>
buildItem: (Node<T>, number, number) => LayoutNode
buildNode: (Node<T>, number, number) => LayoutNode
buildSection: (Node<T>, number, number) => LayoutNode
collection: Collection<Node<T>>
constructor: (ListLayoutOptions<T>) => void
disabledKeys: Set<Key>
getContentSize: () => void
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
getFinalLayoutInfo: (LayoutInfo) => void
- getFirstKey: () => Key | null
+ getFirstKey: (any, any, (Node<T>) => boolean) => Key | null
getInitialLayoutInfo: (LayoutInfo) => void
- getKeyAbove: (Key) => Key | null
- getKeyBelow: (Key) => Key | null
+ getKeyAbove: (Key, (Node<T>) => boolean) => Key | null
+ getKeyBelow: (Key, (Node<T>) => boolean) => Key | null
getKeyForSearch: (string, Key) => Key | null
- getKeyPageAbove: (Key) => Key | null
- getKeyPageBelow: (Key) => Key | null
- getLastKey: () => Key | null
+ getKeyPageAbove: (Key, (Node<T>) => boolean) => Key | null
+ getKeyPageBelow: (Key, (Node<T>) => boolean) => Key | null
+ getLastKey: (any, any, (Node<T>) => boolean) => Key | null
getLayoutInfo: (Key) => void
getVisibleLayoutInfos: (Rect) => void
isLoading: boolean
isValid: (Node<T>, number) => void
updateItemSize: (Key, Size) => void
updateLayoutNode: (Key, LayoutInfo, LayoutInfo) => void
validate: (InvalidationContext<Node<T>, unknown>) => void
}
TableLayout TableLayout<T> {
addVisibleLayoutInfos: (Array<LayoutInfo>, LayoutNode, Rect) => void
binarySearch: (Array<LayoutNode>, Point, 'x' | 'y') => void
buildBody: (number) => LayoutNode
buildCell: (GridNode<T>, number, number) => LayoutNode
buildCollection: () => Array<LayoutNode>
buildColumn: (GridNode<T>, number, number) => LayoutNode
buildHeader: () => LayoutNode
buildHeaderRow: (GridNode<T>, number, number) => LayoutNode
buildNode: (GridNode<T>, number, number) => LayoutNode
buildPersistedIndices: () => void
buildRow: (GridNode<T>, number, number) => LayoutNode
+ buildSection: (Node<T>, number, number) => LayoutNode
collection: TableCollection<T>
columnLayout: TableColumnLayout<T>
columnWidths: Map<Key, number>
constructor: (TableLayoutOptions<T>) => void
endResize: () => void
getColumnMaxWidth: (Key) => number
getColumnMinWidth: (Key) => number
getColumnWidth: (Key) => number
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
getEstimatedHeight: (GridNode<T>, number, number, number) => void
getInitialLayoutInfo: (LayoutInfo) => void
getRenderedColumnWidth: (GridNode<T>) => void
getResizerPosition: () => Key
getVisibleLayoutInfos: (Rect) => void
isLoading: any
lastCollection: TableCollection<T>
lastPersistedKeys: Set<Key>
persistedIndices: Map<Key, Array<number>>
resizingColumn: Key | null
setChildHeights: (Array<LayoutNode>, number) => void
startResize: (Key) => void
stickyColumnIndices: Array<number>
uncontrolledColumns: Map<Key, GridNode<unknown>>
uncontrolledWidths: Map<Key, ColumnSize>
updateResizedColumns: (Key, number) => Map<Key, ColumnSize>
wasLoading: any
}
@react-stately/tableTableSection-
+TableSection<T> {
+ props: TableSectionProps<T>
+ returnVal: undefined
+} |
Closes
✅ Pull Request Checklist:
📝 Test Instructions:
Test the various table section stories. For DnD, look for the "complex drag between tables (sections)" story in the Drag and Drop -> Util Handlers section
Known issues:
🧢 Your Project:
RSP