-
Notifications
You must be signed in to change notification settings - Fork 538
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
AvatarStack: Add keyboard support to AvatarStack
#5134
Conversation
🦋 Changeset detectedLatest commit: 321553a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
👋 Hi, this pull request contains changes to the source code that github/github depends on. If you are GitHub staff, we recommend testing these changes with github/github using the integration workflow. Thanks! |
size-limit report 📦
|
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.
Looking great! Left a couple comments/questions/suggestions 👀
@@ -0,0 +1,23 @@ | |||
const interactiveElements = [ |
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.
One package we could use for this: https://www.npmjs.com/package/focusable-selectors (either as a dependency or inline with accreditation) that has a pretty robust list 👀
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.
Definitely! Added some more selectors that could be handy.
'[tabindex="0"]', | ||
] | ||
|
||
export function getInteractiveNodes(node: HTMLElement | null, ignoreSelectors?: string) { |
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.
One style suggestion, since we're returning a boolean
this could be hasInteractiveNodes
as opposed to getInteractiveNodes
which might seem like we are returning the list of interactive nodes within the given parent node.
export function getInteractiveNodes(node: HTMLElement | null, ignoreSelectors?: string) { | |
export function hasInteractiveContent(node: HTMLElement | null, ignoreSelectors?: string): boolean { |
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.
That's a good callout. I changed the names of the function + file!
@@ -195,6 +203,9 @@ const AvatarStack = ({ | |||
className, | |||
sx: sxProp = defaultSxProp, | |||
}: AvatarStackProps) => { | |||
const [hasInteractiveChildren, setHasInteractiveChildren] = useState<boolean | undefined>(false) | |||
const AvatarStackContainer = useRef(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.
style nit (and definitely non blocking lol) we could use lowercase for useRef
here since we typically only use pascal case for component names or modules
const AvatarStackContainer = useRef(null) | |
const avatarStackContainer = useRef(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.
Didn't realize I was using pascal case 🤦 Should be updated now!
useEffect(() => { | ||
const hasInteractive = getInteractiveNodes(AvatarStackContainer.current, '[tabindex]') | ||
setHasInteractiveChildren(hasInteractive) | ||
}, [children]) |
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.
Would this be equivalent to:
useEffect(() => {
// ...
})
I wasn't sure if children
would change each render or not if they are not memoized. If that's the case, would it be worth running as a mutation observer, potentially?
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.
We could! My assumption is that we'd want to utilize the mutation observer inside of hasInteractiveNodes
?
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 you could use it in there if the function was updated to have a callback/subscriber for when it changed or could use the mutation observer directly and call hasInteractiveNodes
in it, if that makes sense. Put another way:
}, [children]) | |
useEffect(() => { | |
const observer = new MutationObserver((entries) => { | |
setHasInteractiveChildren(hasInteractiveContent(ref.current)); | |
}); | |
observer.observer(ref.current, { | |
/* whatever options make sense */ | |
}); | |
return () => { | |
observer.disconnect(); | |
}; | |
}, []); |
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 see, this is great! Thank you for the example too. I'll rework this a bit.
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.
After implementing this, I realize that the need for checking the children for changes is probably very slim 😅 I removed the children
dependency so it should only run on the initial render, as that's the only time we'd actually want to run this function/check.
🟢 golden-jobs completed with status |
👋 Hi from github/github! Your integration PR is ready: https://github.com/github/github/pull/348018 |
Requires small changes in Dotcom for tests, https://github.com/github/github/pull/348018/commits/833ad355c270f37218f3bcc4071319846b0e552a. Removes empty space that |
useEffect(() => { | ||
const hasInteractive = hasInteractiveNodes(avatarStackContainer.current, 'div[tabindex]') | ||
setHasInteractiveChildren(hasInteractive) | ||
}, []) |
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.
With the move to not running on children
changing, wanted to ask if this is something that should be setup in a mutation observer for when contents change in case the "hasInteractiveNodes" check could change?
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.
We could! I was thinking that we'd know whether to make the container interactive or not based on the initial render as I don't think there are cases where interactive items are rendered in after the first render. But we could add a mutation observer just to be safe.
Closes https://github.com/github/primer/issues/3348
Changelog
New
AvatarStack
Changed
Removed
Rollout strategy
Testing & Reviewing
Merge checklist