-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
[material-ui][ButtonGroup] Compute per-button position classes via context #46712
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
base: master
Are you sure you want to change the base?
[material-ui][ButtonGroup] Compute per-button position classes via context #46712
Conversation
…OM/JSX - Add register/unregister/getPosition to ButtonGroupContext - Button registers its root and applies first/middle/last classes from context - Merge grouped class from context; no :first-child/DOM-type heuristics - Works with wrapped/conditional children and custom Button components - Avoid leaking runtime props to the DOM - Add unit tests for single child, wrapped children, and dynamic visibility - Fixes regression since v5.14.9 where custom Buttons broke grouped styles - Fixes mui#39488
Netlify deploy previewhttps://deploy-preview-46712--material-ui.netlify.app/ Bundle size 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.
@theo-sim-dev The implementation looks promising, and I confirmed it works: https://stackblitz.com/edit/prgp5bra.
I’ve left a few initial comments. Let’s also hear what @siriwatknp thinks.
const id = Symbol('button-group-item'); | ||
orderCounter.current += 1; | ||
itemsRef.current.set(id, { node, order: orderCounter.current }); | ||
setVersion((v) => v + 1); |
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.
Let's use full words as variable names. This improves code readability. Same in multiple other places.
return { id }; | ||
}, []); | ||
|
||
const unregister = React.useCallback((h) => { |
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.
const unregister = React.useCallback((h) => { | |
const unregister = React.useCallback((handler) => { |
@@ -284,6 +282,43 @@ const ButtonGroup = React.forwardRef(function ButtonGroup(inProps, ref) { | |||
|
|||
const classes = useUtilityClasses(ownerState); | |||
|
|||
// Live set of buttons, in mount/render order | |||
const itemsRef = React.useRef(new Map()); // Map<symbol, { node, order }> |
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 itemsRef
is not an object
, but a Map
?
.map(([id, v]) => ({ id, ...v })) | ||
.sort((a, b) => a.order - b.order); | ||
|
||
if (arr.length <= 1) { |
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.
Shouldn’t this just check for exactly one button?
if (arr.length <= 1) { | |
if (arr.length === 1) { |
In what situation would the group have zero registered buttons? It seems that once a ButtonGroup
renders, there’s always at least one child registered; otherwise the group wouldn’t be useful. If 0
ever happened, it would only be during the brief unmount of all children, which isn’t a case that needs position styling anyway.
So arr.length === 1
should be enough.
register: groupRegister, | ||
unregister: groupUnregister, | ||
getPosition: groupGetPosition, | ||
version: groupVersion, |
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.
Is the version
state only meant to force re-renders? Also, I don’t see groupVersion
actually being used inside Button
.
const rootRef = React.useRef(null); | ||
const combinedRef = useForkRef(ref, rootRef); | ||
|
||
// Opaque handle returned by ButtonGroupContext.register(...) |
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 do you mean by Opaque handle
? What does it mean?
|
||
// Register on mount, unregister on unmount. | ||
// Passive effect avoids parent setState during layout-unmount -> re-entrancy loops in dev. | ||
React.useEffect(() => { |
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 we should use useEnhancedEffect
from utils
.
if (!groupRegister || !groupUnregister) { | ||
// Not inside a ButtonGroup, so no need to register | ||
return; | ||
} // not inside a ButtonGroup |
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.
Above comment is enough.
} // not inside a ButtonGroup | |
} |
// position class from getPosition() should be applied after re-render | ||
expect(btn).to.have.class(buttonGroupClasses.firstButton); | ||
expect(btn).not.to.have.class(buttonGroupClasses.middleButton); | ||
expect(btn).not.to.have.class(buttonGroupClasses.lastButton); |
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’m not clear on this—if there’s only one Button, why does it still get the firstButton
class?
@@ -12,6 +15,15 @@ interface ButtonGroupContextType { | |||
fullWidth?: boolean; | |||
size?: ButtonGroupProps['size']; | |||
variant?: ButtonGroupProps['variant']; | |||
|
|||
// Child button calls this once when it mounts | |||
register?: (node: HTMLElement | null) => ButtonGroupItemHandle; |
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 to support null
?
register?: (node: HTMLElement | null) => ButtonGroupItemHandle; | |
register?: (node: HTMLElement) => ButtonGroupItemHandle; |
Thank you for submitting the PR but the issue is related to styles. I think we should try to find a solution using CSS first. Now that we have |
@siriwatknp We support Firefox v115, which doesn't support |
Fixes #39488
Summary
Restore correct
first
,middle
andlast
styles inButtonGroup
with custom, wrapped, or conditionally renderedButton
s by using runtime registration via context (no JSX/DOM heuristics, no Emotion:first/last-child
dependence).Changes
ButtonGroup
: addregister
,unregister
,getPosition
context; ensuring not to leak these internal props to DOM.Button
: register actual root, apply position class from context, mergeclasses.grouped
.Tests
Others