-
Notifications
You must be signed in to change notification settings - Fork 91
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
refactor(accordions): uplift for react v18 #1582
refactor(accordions): uplift for react v18 #1582
Conversation
Children.toArray(children).map((child, index) => ( | ||
<StepContext.Provider | ||
key={index} | ||
// eslint-disable-next-line react/jsx-no-constructed-context-values |
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.
Could you extract const value: IStepContext = { ... };
to the top of this function to get rid of this eslint-disable
?
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.
Moving the variable doesn't get rid of the eslint-disable
rule unfortunately. The rule states that context value should be wrapped in a useMemo
to avoid re-renders- being that it is already in a useMemo
it's not permitted nor does it give any benefit.
role: null as any, | ||
ref: mergeRefs([panelRef, ref]), | ||
index, | ||
value: sectionValue, | ||
isBare, | ||
isCompact, | ||
isExpanded, | ||
isAnimated, | ||
...props | ||
})} | ||
} as any) as 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.
I think {...getPanelProps() as HTMLAttributes<HTMLElement>}
would be preferable over the use of any
here.
@@ -76,7 +75,7 @@ const HeaderComponent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement | |||
onMouseOver: composeEventHandlers(onMouseOver, () => setIsHovered(true)), | |||
onMouseOut: composeEventHandlers(onMouseOut, () => setIsHovered(false)), | |||
...other | |||
})} | |||
} as any) as 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.
prefer {...getHeaderProps() as HTMLAttributes<HTMLDivElement>}
vs overuse of any
.
<SectionContext.Provider key={index} value={index}> | ||
{child} | ||
</SectionContext.Provider> |
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'd like to hear a little bit more about this children wrapping vs. the previous context setup for this component. Should there be concerns about a more brittle DOM structure that is unable to handle intervening nodes?
Also, if this sticks, can isValidElement
be used to eliminate the any
casting? Ref:
/** | |
* Convert an array of `Option` and `OptGroup` children to a valid `options` | |
* data structure for `useCombobox` (collect `tagProps` along the way). | |
* | |
* @param children The `children` prop from `Combobox`. | |
* @param optionTagProps A collection that maps option values to tag props. | |
* | |
* @returns A valid `IUseComboboxProps['options']` data structure. | |
*/ | |
export const toOptions = ( | |
children: ReactNode, | |
optionTagProps: Record<string, IOptionProps['tagProps']> | |
) => | |
Children.toArray(children).reduce((options: IUseComboboxProps['options'], option) => { | |
const retVal = options; | |
if (isValidElement(option)) { | |
if ('value' in option.props) { | |
retVal.push(toOption(option.props)); | |
optionTagProps[toString(option.props)] = option.props.tagProps; | |
} else { | |
const props: IOptGroupProps = option.props; | |
const groupOptions = toOptions(props.children, optionTagProps) as IOption[]; | |
retVal.push({ label: props.label, options: groupOptions }); | |
} | |
} | |
return retVal; | |
}, []); |
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 idea here is that we expect:
<accordion>
<section />
<section />
<div>
<section />
</div>
</accordion>
would give each group the context it needs. This of course relies on the children to take this into account. It actually provides more flexibility when it comes to unique DOM structure requirements, so long as the top level children follow the given pattern.
@@ -76,7 +75,7 @@ const HeaderComponent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement | |||
onMouseOver: composeEventHandlers(onMouseOver, () => setIsHovered(true)), | |||
onMouseOut: composeEventHandlers(onMouseOut, () => setIsHovered(false)), | |||
...other | |||
})} | |||
} as Omit<HTMLAttributes<HTMLDivElement>, 'role' | 'aria-level'> & { 'aria-level': NonNullable<any> }) as 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.
The prop inclusion plus other
spread is over-complicating the TS here. It's important to stick closer to the underlying container definition and not use any
. Here's a solution:
<StyledHeader
isFocused={isFocused}
isExpanded={isExpanded}
isCollapsible={isCollapsible}
{...getHeaderProps({
ref,
'aria-level': ariaLevel,
onClick: composeEventHandlers(onClick, onTriggerClick),
onFocus: composeEventHandlers(onFocus, onHeaderFocus),
onBlur: composeEventHandlers(onBlur, () => setIsFocused(false)),
onMouseOver: composeEventHandlers(onMouseOver, () => setIsHovered(true)),
onMouseOut: composeEventHandlers(onMouseOut, () => setIsHovered(false)),
role: role === undefined || role === null ? role : 'heading',
...other
}) as HTMLAttributes<HTMLDivElement>}
>
...break role
out in the component props above.
isBare, | ||
isCompact, | ||
isExpanded, | ||
isAnimated, | ||
...props | ||
})} | ||
} as Omit<HTMLAttributes<HTMLElement>, 'role'> & { value: number }) as 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.
Similar here. Solution is:
<StyledPanel
isBare={isBare}
isCompact={isCompact}
isExpanded={isExpanded}
isAnimated={isAnimated}
{...getPanelProps({
role: role === undefined ? null : 'region',
ref: mergeRefs([panelRef, ref]),
value: sectionValue,
...props
}) as HTMLAttributes<HTMLElement>}
>
Change const panelRef = useRef<HTMLElement>(null);
above and extract role
from the component props.
import { Step } from './components/Step'; | ||
import { Label } from './components/Label'; | ||
import { Content } from './components/Content'; | ||
|
||
const StepperComponent = forwardRef<HTMLOListElement, IStepperProps>( | ||
({ isHorizontal, activeIndex, ...props }, ref) => { | ||
const currentIndexRef = useRef(0); | ||
({ isHorizontal, activeIndex, children, ...props }, ref) => { |
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.
({ isHorizontal, activeIndex, children, ...props }, ref) => { | |
({ isHorizontal, activeIndex = DEFAULT_ACTIVE_INDEX, children, ...props }, ref) => { |
...where const DEFAULT_ACTIVE_INDEX = 0;
is defined above and also applied below as...
StepperComponent.defaultProps = {
activeIndex: DEFAULT_ACTIVE_INDEX
};
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.
Updated! About defaultProps
, setting a default value for activeIndex
serves the same purpose, does it not?
const stepperContext = useMemo( | ||
() => ({ | ||
isHorizontal: isHorizontal || false, | ||
activeIndex: activeIndex!, | ||
currentIndexRef | ||
activeIndex: activeIndex! |
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.
activeIndex: activeIndex! | |
activeIndex |
...with DEFAULT_ACTIVE_INDEX
set above.
StepperComponent.defaultProps = { | ||
activeIndex: 0 | ||
}; |
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 needs to be retained for runtime checking.
Description
Refactoring the
Accordion
andStepper
for React v18 compatibility.Detail
This PR updates the two component sets in the
accordions
family:Stepper
: replaces theref
for ordinal counting ofStepper.Step
s with theStepContext
that wraps eachStepper.Step
(or child, if wrapped) with a pre-formed context value computed from the current step value - the context contains properties used throughout eachStep
(and the nested components) and it's current value.Accordion
: replaces theref
for ordinal counting ofAccordion.Section
s with theSectionContext
which will wrap theAccordion.Section
child (or any wrapping element). TheSectionContext
contains the section value as the context value as it was prior. There was an uplift of@zendeskgarden/container-accordion
to v3 and adopted the new API foruseAccordion
as well.Fixes #1462
Checklist
design updates will be Garden Designer approved (add the designer as a reviewer)yarn start
)renders as expected with Bedrock CSS (?bedrock
)includes new unit tests. Maintain existing coverage (always >= 96%)