UNPKG

baseui

Version:

A React Component library implementing the Base design language

393 lines (372 loc) • 14.8 kB
"use strict"; 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 = {}, uid: customUid, 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); }