baseui
Version:
A React Component library implementing the Base design language
394 lines (373 loc) • 14.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.SegmentedControl = SegmentedControl;
var React = _interopRequireWildcard(require("react"));
var _reactUid = require("react-uid");
var _styles = require("../styles");
var _overrides = require("../helpers/overrides");
var _focusVisible = require("../utils/focusVisible");
var _constants = require("./constants");
var _styledComponents = require("./styled-components");
var _utils = require("./utils");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/ /* global window */
const KEYBOARD_ACTION = {
next: 'next',
previous: 'previous'
};
const getLayoutParams = el => {
if (!el) {
return {
length: 0,
distance: 0
};
}
let {
width
} = el.getBoundingClientRect();
width = Math.floor(width);
// Note we're using getBoundingClientRect to take into account
// the borders.
return {
length: width,
distance: el.offsetLeft
};
};
const scrollParentToCentreTarget = targetNode => {
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight
} = targetNode.parentNode.getBoundingClientRect();
const {
x: childX,
y: childY,
width: childWidth,
height: childHeight
} = targetNode.getBoundingClientRect();
// get the position of the child centre, relative to parent
const childCentre = {
x: childX - parentX + childWidth / 2,
y: childY - parentY + childHeight / 2
};
// aim for the centre of the child to be the centre of the parent
const {
scrollLeft,
scrollTop
} = targetNode.parentNode;
const target = {
x: scrollLeft + childCentre.x - parentWidth / 2,
y: scrollTop + childCentre.y - parentHeight / 2
};
// ignore out of bounds, the browser will manage this for us
targetNode.parentNode.scroll(target.x, target.y);
};
function SegmentedControl({
activeKey = '0',
disabled = false,
children,
fill = _constants.FILL.intrinsic,
activateOnFocus = true,
onChange,
overrides = {},
// @ts-expect-error todo(ts-migration) TS2322 Type 'null' is not assignable to type 'string'.
uid: customUid = null,
width,
height
}) {
// Create unique id prefix for this segments component
const generatedUid = (0, _reactUid.useUID)();
const uid = customUid || generatedUid;
// Unpack overrides
const {
Root: RootOverrides,
Active: ActiveOverrides
} = overrides;
const [Root, RootProps] = (0, _overrides.getOverrides)(RootOverrides, _styledComponents.StyledRoot);
const [Active, ActiveProps] = (0, _overrides.getOverrides)(ActiveOverrides, _styledComponents.StyledActive);
const [SegmentList, SegmentListProps] = (0, _overrides.getOverrides)(overrides.SegmentList, _styledComponents.StyledSegmentList);
// Count key updates
// We disable a few things until after first mount:
// - the highlight animation, avoiding an initial slide-in
// - smooth scrolling active segment into view
const [keyUpdated, setKeyUpdated] = React.useState(0);
React.useEffect(() => {
setKeyUpdated(keyUpdated + 1);
}, [activeKey]);
// Positioning the highlight.
const activeSegmentRef = React.useRef();
const [highlightLayout, setHighlightLayout] = React.useState({
length: 0,
distance: 0
});
// Create a shared, memoized callback for segments to call on resize.
const updateHighlight = React.useCallback(() => {
if (activeSegmentRef.current) {
setHighlightLayout(getLayoutParams(activeSegmentRef.current));
}
}, [activeSegmentRef.current]);
// Update highlight on key
React.useEffect(updateHighlight, [activeSegmentRef.current]);
// Scroll active segment into view when the parent has scrollbar on mount and
// on key change (smooth scroll). Note, if the active key changes while
// the segment is not in view, the page will scroll it into view.
// TODO: replace with custom scrolling logic.
React.useEffect(() => {
// Flow needs this condition pulled out.
if (activeSegmentRef.current) {
if (
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeSegmentRef.current.parentNode.scrollWidth >
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeSegmentRef.current.parentNode.clientWidth) {
if (keyUpdated > 1) {
activeSegmentRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
} else {
scrollParentToCentreTarget(activeSegmentRef.current);
}
}
}
}, [activeSegmentRef.current]);
// Collect shared styling props
const sharedStylingProps = {
$fill: fill
};
// Helper for parsing directional keys
// TODO(WPT-6473): move to universal keycode aliases
const [, theme] = (0, _styles.useStyletron)();
const parseKeyDown = React.useCallback(event => {
if ((0, _utils.isRTL)(theme.direction)) {
switch (event.keyCode) {
case 39:
return KEYBOARD_ACTION.previous;
case 37:
return KEYBOARD_ACTION.next;
default:
return null;
}
} else {
switch (event.keyCode) {
case 37:
return KEYBOARD_ACTION.previous;
case 39:
return KEYBOARD_ACTION.next;
default:
return null;
}
}
}, [theme.direction]);
return /*#__PURE__*/React.createElement(Root, _extends({}, sharedStylingProps, RootProps, {
$width: width,
$height: height
}), /*#__PURE__*/React.createElement(SegmentList, _extends({
"data-baseweb": "segmented-list",
role: "listbox",
"aria-label": "segmented control"
}, SegmentListProps), React.Children.map(children, (child, index) => {
if (!child) return;
return /*#__PURE__*/React.createElement(InternalSegment, _extends({
childKey: child.key,
childIndex: index,
activeKey: activeKey,
activeSegmentRef: activeSegmentRef,
updateHighlight: updateHighlight,
parseKeyDown: parseKeyDown,
activateOnFocus: activateOnFocus,
uid: uid,
disabled: disabled,
sharedStylingProps: sharedStylingProps,
onChange: onChange,
setKeyUpdated: setKeyUpdated
}, child.props));
}), /*#__PURE__*/React.createElement(Active, _extends({
"data-baseweb": "segment-highlight",
$length: highlightLayout.length,
$distance: highlightLayout.distance
// This avoids the segment sliding in from the side on mount
,
$animate: keyUpdated > 1,
"aria-hidden": "true",
role: "presentation"
}, sharedStylingProps, ActiveProps))));
}
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] = (0, _overrides.getOverrides)(SegmentOverrides, _styledComponents.StyledSegment);
const [LabelBlockContainer, LabelBlockContainerProps] = (0, _overrides.getOverrides)(LabelBlockContainerOverrides, _styledComponents.StyledLabelBlock);
const [ArtworkContainer, ArtworkContainerProps] = (0, _overrides.getOverrides)(ArtworkContainerOverrides, _styledComponents.StyledArtworkContainer);
const [LabelContainer, LabelContainerProps] = (0, _overrides.getOverrides)(LabelOverrides, _styledComponents.StyledLabel);
const [DescriptionContainer, DescriptionContainerProps] = (0, _overrides.getOverrides)(DescriptionOverrides, _styledComponents.StyledDescription);
const [BadgeContainer, BadgeContainerProps] = (0, _overrides.getOverrides)(BadgeOverrides, _styledComponents.StyledBadge);
const [BadgeHintContainer, BadgeHintContainerProps] = (0, _overrides.getOverrides)(BadgeHintOverrides, _styledComponents.StyledBadgeHint);
// Keyboard focus styling
const [focusVisible, setFocusVisible] = React.useState(false);
const handleFocus = React.useCallback(event => {
if ((0, _focusVisible.isFocusVisible)(event)) {
setFocusVisible(true);
}
}, []);
const handleBlur = React.useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
event => {
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;
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 /*#__PURE__*/React.createElement(Segment, _extends({
"data-baseweb": "segment",
key: key,
id: (0, _utils.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: (0, _focusVisible.forkFocus)({
...restProps,
...SegmentProps
}, handleFocus),
onBlur: (0, _focusVisible.forkBlur)({
...restProps,
...SegmentProps
}, handleBlur)
}), /*#__PURE__*/React.createElement(LabelBlockContainer, LabelBlockContainerProps, !!Artwork && /*#__PURE__*/React.createElement(ArtworkContainer, _extends({
"data-baseweb": "artwork-container"
}, sharedStylingProps, ArtworkContainerProps), /*#__PURE__*/React.createElement(Artwork, {
size: 20,
color: "contentPrimary"
})), !!label && /*#__PURE__*/React.createElement(LabelContainer, LabelContainerProps, label ? label : key), !!badge && /*#__PURE__*/React.createElement(BadgeContainer, BadgeContainerProps, badge), badgeHint && /*#__PURE__*/React.createElement(BadgeHintContainer, BadgeHintContainerProps)), description ? /*#__PURE__*/React.createElement(DescriptionContainer, _extends({}, DescriptionOverrides, DescriptionContainerProps), description) : null);
}