-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
TreeGrid: Convert to TypeScript #47516
Conversation
Size Change: +63 B (0%) Total Size: 1.31 MB
ℹ️ View Unchanged
|
Flaky tests detected in 2497ee6. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4077659498
|
<Button onClick={ onMoveUp } { ...props }> | ||
Move Up | ||
</Button> | ||
) } | ||
</TreeGridCell> | ||
<TreeGridCell> | ||
{ ( props ) => ( | ||
<Button onClick={ onMoveDown } { ...props }> | ||
Move Down | ||
</Button> |
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.
Simplified this code snippet a bit to cut down on the number of lines.
if ( ! focusablesInRow || ! focusablesInRow.length ) { | ||
return; | ||
} | ||
} ) as FocusableElement[]; |
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.
Anything that is returned from this function can be assumed to have a focus()
method.
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.
Why not use HTMLElement
directly instead of FocusableElement
?
Or at least, could we Pick< HTMLElement, 'focus' >
?
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.
You're right, I think I misunderstood something. Fixed in 52cbdd9
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.
Thank you!
I was wondering if we could push this further and see if more (if not all) of the Element
types in this component can be changed to HTMLElement
?
For example, I noticed that onFocusRow
/ onCollapseRow
/ onExpandRow
were previously documented as accepting an HTMLElement
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.
Good idea. Done in d30ea0b
@@ -65,7 +60,9 @@ function TreeGrid( | |||
|
|||
if ( | |||
hasModifierKeyPressed || | |||
! [ UP, DOWN, LEFT, RIGHT, HOME, END ].includes( keyCode ) | |||
! ( [ UP, DOWN, LEFT, RIGHT, HOME, END ] as number[] ).includes( |
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.
Apparently, importing these keycode constants implies that they are really const
s.
// Normally, doing this in a single file is ok.
const UP = 30;
const foo = (n: number) => {
return [UP].includes(n)
}
// But when `UP` is imported (I think) it becomes equivalent to
const UP = 30 as const;
// And then this won't pass
const foo = (n: number) => {
// Argument of type 'number' is not assignable to parameter of type '30'.
return [UP].includes(n)
}
🤷🤷🤷
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.
🤷
if ( ! treeGridElement.contains( activeElement ) ) { | ||
|
||
if ( | ||
! activeElement || |
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.
This early return is just to appease TS, but actual runtime behavior should not change because if activeElement
were falsey treeGridElement.contains( activeElement )
would also be falsey.
| { | ||
lastFocusedElement: Element | undefined; | ||
setLastFocusedElement: React.Dispatch< | ||
React.SetStateAction< Element | undefined > | ||
>; | ||
} | ||
| undefined | ||
>( undefined ); |
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.
It would be easy to set up a "safer" default for this context value (for example an empty object {}
), but a unit test implies that we should actually be throwing if the context is not properly initialized. So I'm leaving this as is (just more explicitly undefined I guess).
return children( allProps ); | ||
} | ||
|
||
if ( ! Component ) return 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.
Just an added guard, but nevertheless a runtime change.
Interestingly, there are no actual usages of this as
prop in the repo.
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.
Maybe there were when the as = Component
prop destructuring was introduced, but since then it was removed
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.
Did some more investigation — this component was introduced as a separate component from TreeGrid
and it seemed to have the as
prop from its beginnings: https://github.com/WordPress/gutenberg/pull/18014/files#diff-b3f07d9e77626753ea74b8a24a73ef0f82bc0a9d537617976b8ad34415f13ac0
The as
prop listed in the docs, in the Storybook examples, and even in the unit tests, but I don't think other components were using it at that point.
The component was later incorporated into TreeGrid
as part of #22373
children: { control: { type: null } }, | ||
}, | ||
parameters: { | ||
actions: { argTypesRegex: '^on.*' }, |
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 that this won't fire actions for onExpandRow
until we add a specific story that allows it to happen. I'm not going to do that in this PR because it is somewhat non-trivial.
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.
What is the specific config that we need to replicate? Is it worth adding it to the onExpandRow
prop docs?
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.
I think a good example would need to implement a simple version of a file browser where you can expand folder rows. Most of the functionality isn't baked into the component so we'd need to manage it ourselves (toggling visibility, state, styling, etc). We can write a better description for onExpandRow
once we better understand what it entails.
as?: React.ComponentType< RovingTabIndexItemPassThruProps >; | ||
[ key: string ]: any; |
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.
Honestly, in terms of TypeScript I think things could be a lot simpler if we didn't allow an as
prop, and didn't allow pass-through of arbitrary props. All the same things would be possible with just a children
render prop and no arbitrary pass-through props. We might want to revisit this API if these loose types cause more trouble than it's worth.
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.
Absolutely, let's keep this in mind
const meta: ComponentMeta< typeof TreeGrid > = { | ||
title: 'Components (Experimental)/TreeGrid', | ||
component: TreeGrid, | ||
subcomponents: { TreeGridRow, TreeGridCell }, |
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.
I am hesitant to add TreeGridItem
to the subcomponents table because:
- It wasn't even documented in the main readme.
- It seems quite low level, and I'm not sure if the few usages in the Gutenberg app are even warranted in the first place 🤔
- The docgen refuses to generate anything, probably because the types are too loose. (It needs to accept any prop, as they are passed through to arbitrary
children
/as
.)
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.
The only reason why I'd add it is to "manifest" its existence in the TreeGrid
family — ie., if you use TreeGrid
, you should TreeGridItem
too (since it enabled the roving tab index functionality)
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.
if you use
TreeGrid
, you shouldTreeGridItem
too (since it enabled the roving tab index functionality)
I think the original intent was for people to use TreeGridCell
(which is TreeGridItem
wrapped in a <td>
tag). Using TreeGridItem without the gridcell wrapper is an invitation for semantic issues. There are two instances in the Gutenberg app where a raw TreeGridItem
is used, and I have a feeling these are both problematic. If I understand the treegrid
spec correctly, there should not be multiple focusable elements in a gridcell.
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.
Makes sense. Let's keep it out of the docs, and probably prepare a follow-up to see if we can fix those TreeGridItem
usages and convert them to TreeGridCell
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.
Great work here!
I noticed that there a few mentions in the component's docs about the fact that the component internally renders a table
/ td
/ tr
— do you think we should keep those references, even if they expose the internal DOM structure of the component?
Also, we're missing a CHANGELOG entry 🗒️
if ( ! focusablesInRow || ! focusablesInRow.length ) { | ||
return; | ||
} | ||
} ) as FocusableElement[]; |
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.
Why not use HTMLElement
directly instead of FocusableElement
?
Or at least, could we Pick< HTMLElement, 'focus' >
?
@@ -65,7 +60,9 @@ function TreeGrid( | |||
|
|||
if ( | |||
hasModifierKeyPressed || | |||
! [ UP, DOWN, LEFT, RIGHT, HOME, END ].includes( keyCode ) | |||
! ( [ UP, DOWN, LEFT, RIGHT, HOME, END ] as number[] ).includes( |
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.
🤷
// TODO: The original implementation simply used `ref.current` here, assuming | ||
// that a forwarded ref would always be an object, which is not necessarily true. | ||
// This workaround maintains the original runtime behavior in a type-safe way, | ||
// but should be revisited. |
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.
Nice comment.
I guess the better way here could be to check if typeof ref === 'function'
(i.e. a callback ref) and then act accordingly. Definitely a task for a follow-up, and not this PR.
as?: React.ComponentType< RovingTabIndexItemPassThruProps >; | ||
[ key: string ]: any; |
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.
Absolutely, let's keep this in mind
children: { control: { type: null } }, | ||
}, | ||
parameters: { | ||
actions: { argTypesRegex: '^on.*' }, |
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.
What is the specific config that we need to replicate? Is it worth adding it to the onExpandRow
prop docs?
Another quick note — the "Move up" / "Move down" buttons in the Storybook example don't currently do anything when clicked — should we have them at least print something to console? |
Yes, I think those semantics are an essential part of the component, and thus part of the non-breaking contract. Otherwise it would just be the roving tabindex behavior.
🙈
I agree they're confusing. I swapped them out with text fields so they look less actionable. Wdyt? cd66379 |
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.
I think this looks better and less confusing, thank you!
Since there are a lot of open convos that are not actionable for this PR, here's a quick recap for our future selves:
Needs addressing in this PR:
Element
toHTMLElement
(convo)
To be addressed separately from this PRs: (low-priority)
- adding a specific Storybook example to highlight how to use the
onExpandRow
callback (convo) - consider removing the
as
prop (convo, convo) - see if we can clean remove all usages of
TreeGridItem
and replace them withTreeGridCell
(convo) - Make the
ref
check inRovingTabIndexItem
support callback refs too (convo)
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.
LGTM 🚀
Part of #35744
What?
Convert the TreeGrid component and its subcomponents to TypeScript, including stories and tests.
How?
See inline comments.
Testing Instructions
npm run storybook:dev
and check the Docs view for TreeGrid.