in src/segmented-control/segmented-control.tsx [238:446]
function InternalSegment({
childKey,
childIndex,
activeKey,
activeSegmentRef,
updateHighlight,
parseKeyDown,
activateOnFocus,
uid,
disabled,
sharedStylingProps,
onChange,
setKeyUpdated,
...props
}) {
const key = childKey || String(childIndex);
const isActive = key == activeKey;
const {
artwork: Artwork,
overrides = {},
segmentRef,
onClick,
label,
description,
badge,
badgeHint,
...restProps
} = props;
// A way to share our internal activeSegmentRef via the "segmentRef" prop.
const ref = React.useRef();
React.useImperativeHandle(segmentRef, () => {
return isActive ? activeSegmentRef.current : ref.current;
});
// Track segment dimensions in a ref after each render
// This is used to compare params when the resize observer fires
const segmentLayoutParams = React.useRef({ length: 0, distance: 0 });
React.useEffect(() => {
segmentLayoutParams.current = getLayoutParams(
isActive ? activeSegmentRef.current : ref.current
);
});
// We need to potentially update the active segment highlight when the width or
// placement changes for a segment so we listen for resize updates in each segment.
React.useEffect(() => {
if (window.ResizeObserver) {
const observer = new window.ResizeObserver((entries) => {
if (entries[0] && entries[0].target) {
const segmentLayoutParamsAfterResize = getLayoutParams(entries[0].target);
if (
segmentLayoutParamsAfterResize.length !== segmentLayoutParams.current.length ||
segmentLayoutParamsAfterResize.distance !== segmentLayoutParams.current.distance
) {
setKeyUpdated(1);
updateHighlight();
}
}
});
observer.observe(isActive ? activeSegmentRef.current : ref.current);
return () => {
observer.disconnect();
};
}
}, [activeKey]);
React.useEffect(updateHighlight, [label]);
// Collect overrides
const {
Segment: SegmentOverrides,
ArtworkContainer: ArtworkContainerOverrides,
LabelBlock: LabelBlockContainerOverrides,
Label: LabelOverrides,
Description: DescriptionOverrides,
Badge: BadgeOverrides,
BadgeHint: BadgeHintOverrides,
} = overrides;
const [Segment, SegmentProps] = getOverrides(SegmentOverrides, StyledSegment);
const [LabelBlockContainer, LabelBlockContainerProps] = getOverrides(
LabelBlockContainerOverrides,
StyledLabelBlock
);
const [ArtworkContainer, ArtworkContainerProps] = getOverrides(
ArtworkContainerOverrides,
StyledArtworkContainer
);
const [LabelContainer, LabelContainerProps] = getOverrides(LabelOverrides, StyledLabel);
const [DescriptionContainer, DescriptionContainerProps] = getOverrides(
DescriptionOverrides,
StyledDescription
);
const [BadgeContainer, BadgeContainerProps] = getOverrides(BadgeOverrides, StyledBadge);
const [BadgeHintContainer, BadgeHintContainerProps] = getOverrides(
BadgeHintOverrides,
StyledBadgeHint
);
// Keyboard focus styling
const [focusVisible, setFocusVisible] = React.useState(false);
const handleFocus = React.useCallback((event: SyntheticEvent) => {
if (isFocusVisible(event)) {
setFocusVisible(true);
}
}, []);
const handleBlur = React.useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(event: SyntheticEvent) => {
if (focusVisible !== false) {
setFocusVisible(false);
}
},
[focusVisible]
);
// Keyboard focus management
// @ts-expect-error todo(flow->ts): deps are required
const handleKeyDown = React.useCallback((event) => {
// WAI-ARIA 1.1
// https://www.w3.org/TR/wai-aria-practices-1.1/#segmentpanel
// We use directional keys to iterate focus through SegmentedControl.
// Find all segments eligible for focus
const availableSegmentedControl = [...event.target.parentNode.childNodes].filter(
(node) => !node.disabled && node.getAttribute('role') === 'option'
);
// Exit early if there are no other segments available
if (availableSegmentedControl.length === 1) return;
// Find segment to focus, looping to start/end of list if necessary
const currentSegmentIndex = availableSegmentedControl.indexOf(event.target);
const action = parseKeyDown(event);
if (action) {
let nextSegment: HTMLButtonElement | undefined | null;
if (action === KEYBOARD_ACTION.previous) {
if (availableSegmentedControl[currentSegmentIndex - 1]) {
nextSegment = availableSegmentedControl[currentSegmentIndex - 1];
} else {
nextSegment = availableSegmentedControl[availableSegmentedControl.length - 1];
}
} else if (action === KEYBOARD_ACTION.next) {
if (availableSegmentedControl[currentSegmentIndex + 1]) {
nextSegment = availableSegmentedControl[currentSegmentIndex + 1];
} else {
nextSegment = availableSegmentedControl[0];
}
}
if (nextSegment) {
// Focus the segment
nextSegment.focus();
// Optionally activate the segment
if (activateOnFocus) {
nextSegment.click();
}
}
}
});
return (
<Segment
data-baseweb="segment"
key={key}
id={getSegmentId(uid, key)}
role="option"
onKeyDown={handleKeyDown}
aria-selected={isActive}
tabIndex={isActive ? '0' : '-1'}
ref={isActive ? activeSegmentRef : ref}
disabled={!isActive && disabled}
type="button" // so it doesn't trigger a submit when used inside forms
$focusVisible={focusVisible}
$isActive={isActive}
$hasArtwork={!!Artwork}
$hasLabel={!!label}
{...sharedStylingProps}
{...restProps}
{...SegmentProps}
onClick={(event) => {
if (typeof onChange === 'function') onChange({ activeKey: key });
if (typeof onClick === 'function') onClick(event);
}}
onFocus={forkFocus({ ...restProps, ...SegmentProps }, handleFocus)}
onBlur={forkBlur({ ...restProps, ...SegmentProps }, handleBlur)}
>
<LabelBlockContainer {...LabelBlockContainerProps}>
{!!Artwork && (
<ArtworkContainer
data-baseweb="artwork-container"
{...sharedStylingProps}
{...ArtworkContainerProps}
>
<Artwork size={20} color="contentPrimary" />
</ArtworkContainer>
)}
{!!label && <LabelContainer {...LabelContainerProps}>{label ? label : key}</LabelContainer>}
{!!badge && <BadgeContainer {...BadgeContainerProps}>{badge}</BadgeContainer>}
{badgeHint && <BadgeHintContainer {...BadgeHintContainerProps} />}
</LabelBlockContainer>
{description ? (
<DescriptionContainer {...DescriptionOverrides} {...DescriptionContainerProps}>
{description}
</DescriptionContainer>
) : null}
</Segment>
);
}