UNPKG

baseui

Version:

A React Component library implementing the Base design language

401 lines (396 loc) • 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _constants = require("./constants"); var _utils = require("./utils"); var _reactUid = require("react-uid"); 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); } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* 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. */ // Files // Types const DEFAULT_PROPS = { // keeping it in defaultProps to satisfy Flow initialState: { // We start the index at -1 to indicate that no highlighting exists initially highlightedIndex: -1, isFocused: false }, typeAhead: true, keyboardControlNode: { current: null }, stateReducer: (changeType, changes) => changes, onItemSelect: () => {}, getRequiredItemProps: () => ({}), // @ts-ignore children: () => null, // from nested-menus context addMenuToNesting: () => {}, removeMenuFromNesting: () => {}, getParentMenu: () => {}, getChildMenu: () => {}, nestedMenuHoverIndex: -1, isNestedMenuVisible: () => false, forceHighlight: false }; class MenuStatefulContainerInner extends React.Component { constructor(...args) { super(...args); _defineProperty(this, "state", { // @ts-expect-error todo(flow->ts): probably MenuStatefulContainer should be used instead of this.constructor ...this.constructor.defaultProps.initialState, ...this.props.initialState }); // We need to have access to the root component user renders // to correctly facilitate keyboard scrolling behavior _defineProperty(this, "rootRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "keyboardControlNode", this.props.keyboardControlNode.current); // One array to hold all list item refs _defineProperty(this, "refList", []); // list of ids applied to list items. used to set aria-activedescendant _defineProperty(this, "optionIds", []); //characters input from keyboard, will automatically be clear after some time _defineProperty(this, "typeAheadChars", ''); //count time for each continuous keyboard input _defineProperty(this, "typeAheadTimeOut", null); _defineProperty(this, "onKeyDown", event => { switch (event.key) { case _constants.KEY_STRINGS.ArrowUp: case _constants.KEY_STRINGS.ArrowDown: case _constants.KEY_STRINGS.ArrowLeft: case _constants.KEY_STRINGS.ArrowRight: case _constants.KEY_STRINGS.Home: case _constants.KEY_STRINGS.End: this.handleArrowKey(event); break; case _constants.KEY_STRINGS.Enter: if (event.keyCode === 229) { // ref. // https://github.com/JedWatson/react-select/blob/e12b42b0e7598ec4a96a1a6480e0b2b4c7dc03e3/packages/react-select/src/Select.js#L1209 break; } this.handleEnterKey(event); break; default: if (this.props.typeAhead) { // @ts-ignore clearTimeout(this.typeAheadTimeOut); this.handleAlphaDown(event); } break; } }); _defineProperty(this, "handleAlphaDown", event => { const rootRef = this.props.rootRef ? this.props.rootRef : this.rootRef; const prevIndex = this.state.highlightedIndex; this.typeAheadChars += event.key; this.typeAheadTimeOut = setTimeout(() => { this.typeAheadChars = ''; }, 500); var nextIndex = prevIndex; var list = this.getItems(); if (list.length === 0 || !('label' in list[0])) return; var notMatch = true; for (let n = 0; n < list.length; n++) { let label = list[n].label; if (label && label.toUpperCase && label.toUpperCase().indexOf(this.typeAheadChars.toUpperCase()) === 0) { nextIndex = n; notMatch = false; break; } } if (notMatch) { for (let n = 0; n < list.length; n++) { let label = list[n].label; if (label && label.toUpperCase && label.toUpperCase().indexOf(this.typeAheadChars.toUpperCase()) > 0) { nextIndex = n; break; } } } this.internalSetState(_constants.STATE_CHANGE_TYPES.character, { highlightedIndex: nextIndex }); if (this.refList[nextIndex]) { (0, _utils.scrollItemIntoView)(this.refList[nextIndex].current, // @ts-ignore rootRef.current, nextIndex === 0, nextIndex === list.length - 1); } }); // Handler for arrow keys _defineProperty(this, "handleArrowKey", event => { const rootRef = this.props.rootRef ? this.props.rootRef : this.rootRef; const prevIndex = this.state.highlightedIndex; let nextIndex = prevIndex; if (event.key === _constants.KEY_STRINGS.ArrowUp) { event.preventDefault(); nextIndex = Math.max(0, prevIndex - 1); this.internalSetState(_constants.STATE_CHANGE_TYPES.moveUp, { highlightedIndex: nextIndex }); } else if (event.key === _constants.KEY_STRINGS.ArrowDown) { event.preventDefault(); nextIndex = Math.min(prevIndex + 1, this.getItems().length - 1); this.internalSetState(_constants.STATE_CHANGE_TYPES.moveDown, { highlightedIndex: nextIndex }); } else if (event.key === _constants.KEY_STRINGS.Home) { event.preventDefault(); nextIndex = 0; this.internalSetState(_constants.STATE_CHANGE_TYPES.moveUp, { highlightedIndex: nextIndex }); } else if (event.key === _constants.KEY_STRINGS.End) { event.preventDefault(); nextIndex = this.getItems().length - 1; this.internalSetState(_constants.STATE_CHANGE_TYPES.moveDown, { highlightedIndex: nextIndex }); } else if (event.key === _constants.KEY_STRINGS.ArrowLeft) { if (this.props.getParentMenu) { const parent = this.props.getParentMenu(rootRef); if (parent && parent.current) { parent.current.focus(); } } } else if (event.key === _constants.KEY_STRINGS.ArrowRight) { if (this.props.getChildMenu) { const child = this.props.getChildMenu(rootRef); if (child && child.current) { child.current.focus(); } } } if (this.refList[nextIndex]) { (0, _utils.scrollItemIntoView)(this.refList[nextIndex].current, // @ts-ignore rootRef.current, nextIndex === 0, nextIndex === this.getItems().length - 1); } }); // Handler for enter key _defineProperty(this, "handleEnterKey", event => { const { onItemSelect } = this.props; const { highlightedIndex } = this.state; const items = this.getItems(); if (items[highlightedIndex] && onItemSelect && !items[highlightedIndex].disabled) { event.preventDefault(); onItemSelect({ item: items[highlightedIndex], event }); } }); _defineProperty(this, "handleItemClick", (index, item, event) => { if (this.props.onItemSelect && !item.disabled) { this.props.onItemSelect({ item, event }); this.internalSetState(_constants.STATE_CHANGE_TYPES.click, { highlightedIndex: index, activedescendantId: this.optionIds[index] }); } }); _defineProperty(this, "handleMouseEnter", index => { this.internalSetState(_constants.STATE_CHANGE_TYPES.mouseEnter, { highlightedIndex: index, activedescendantId: this.optionIds[index] }); }); // eslint-disable-next-line @typescript-eslint/no-unused-vars _defineProperty(this, "handleMouseLeave", event => {}); _defineProperty(this, "getRequiredItemProps", (item, index) => { let itemRef = this.refList[index]; if (!itemRef) { itemRef = /*#__PURE__*/React.createRef(); this.refList[index] = itemRef; this.optionIds[index] = this.props.uidSeed(index); } const { disabled: disabledVal, ...requiredItemProps } = this.props.getRequiredItemProps(item, index); const disabled = typeof disabledVal === 'boolean' ? disabledVal : !!item.disabled; return { id: requiredItemProps.id || this.optionIds[index], disabled, ref: itemRef, isFocused: this.state.isFocused, isHighlighted: this.state.highlightedIndex === index, resetMenu: this.resetMenu, // binds so that in-line functions can be avoided. this is to ensure // referential equality when option-list compares props in memoized component ...(disabled ? {} : { onClick: this.handleItemClick.bind(this, index, item), onMouseEnter: this.handleMouseEnter.bind(this, index) }), ...requiredItemProps }; }); _defineProperty(this, "focusMenu", event => { const rootRef = this.props.rootRef ? this.props.rootRef : this.rootRef; if (!this.state.isFocused && rootRef.current && // @ts-expect-error rootRef.current.contains(event.target)) { if (this.state.highlightedIndex < 0) { this.internalSetState(_constants.STATE_CHANGE_TYPES.focus, { isFocused: true, highlightedIndex: 0 }); } else { this.internalSetState(_constants.STATE_CHANGE_TYPES.focus, { isFocused: true }); } rootRef.current.focus(); } }); _defineProperty(this, "unfocusMenu", () => { this.internalSetState(_constants.STATE_CHANGE_TYPES.focus, { isFocused: false }); }); _defineProperty(this, "resetMenu", () => { this.internalSetState(_constants.STATE_CHANGE_TYPES.reset, { isFocused: false, highlightedIndex: -1, activedescendantId: null }); }); } getItems() { if (Array.isArray(this.props.items)) { return this.props.items; } const optgroups = Object.keys(this.props.items); return optgroups.reduce((output, optgroup) => { // @ts-ignore return output.concat(this.props.items[optgroup]); }, []); } componentDidMount() { const rootRef = this.props.rootRef ? this.props.rootRef : this.rootRef; if (typeof document !== 'undefined') { if (rootRef.current && this.state.highlightedIndex > -1 && this.refList[this.state.highlightedIndex]) { (0, _utils.scrollItemIntoView)(this.refList[this.state.highlightedIndex].current, rootRef.current, this.state.highlightedIndex === 0, this.state.highlightedIndex === this.getItems().length - 1, 'center'); } if (this.state.isFocused) { if (this.keyboardControlNode) { this.keyboardControlNode.addEventListener('keydown', this.onKeyDown); } } } this.props.addMenuToNesting && this.props.addMenuToNesting(rootRef); } componentWillUnmount() { const rootRef = this.props.rootRef ? this.props.rootRef : this.rootRef; if (typeof document !== 'undefined') { if (this.keyboardControlNode) this.keyboardControlNode.removeEventListener('keydown', this.onKeyDown); } if (this.props.removeMenuFromNesting) { this.props.removeMenuFromNesting(rootRef); } } componentDidUpdate(prevProps, prevState) { if (typeof document !== 'undefined') { if (!prevState.isFocused && this.state.isFocused) { if (this.keyboardControlNode) this.keyboardControlNode.addEventListener('keydown', this.onKeyDown); } else if (prevState.isFocused && !this.state.isFocused) { if (this.keyboardControlNode) this.keyboardControlNode.removeEventListener('keydown', this.onKeyDown); } } var range = this.getItems().length; if (this.props.forceHighlight && this.state.highlightedIndex === -1 && range > 0) { this.internalSetState(_constants.STATE_CHANGE_TYPES.enter, { highlightedIndex: 0 }); } if (range === 0 && this.state.highlightedIndex !== -1) { this.internalSetState(_constants.STATE_CHANGE_TYPES.enter, { highlightedIndex: -1 }); } else if (this.state.highlightedIndex >= range) { this.internalSetState(_constants.STATE_CHANGE_TYPES.enter, { highlightedIndex: 0 }); } if (this.props.isNestedMenuVisible && this.props.nestedMenuHoverIndex !== prevProps.nestedMenuHoverIndex && !this.props.isNestedMenuVisible(this.rootRef) && !this.props.forceHighlight) { this.setState({ highlightedIndex: -1 }); } } // Internal set state function that will also invoke stateReducer internalSetState(changeType, changes) { const { stateReducer } = this.props; if (this.props.onActiveDescendantChange && typeof changes.highlightedIndex === 'number' && this.state.highlightedIndex !== changes.highlightedIndex) { this.props.onActiveDescendantChange(this.optionIds[changes.highlightedIndex]); } this.setState(stateReducer(changeType, changes, this.state)); } render() { // omit the stateful-container's props and don't pass it down // to the children (stateless menu) const { // eslint-disable-next-line @typescript-eslint/no-unused-vars initialState, // eslint-disable-next-line @typescript-eslint/no-unused-vars stateReducer, // eslint-disable-next-line @typescript-eslint/no-unused-vars children, // eslint-disable-next-line @typescript-eslint/no-unused-vars onItemSelect, // eslint-disable-next-line @typescript-eslint/no-unused-vars addMenuToNesting, // eslint-disable-next-line @typescript-eslint/no-unused-vars removeMenuFromNesting, // eslint-disable-next-line @typescript-eslint/no-unused-vars getParentMenu, // eslint-disable-next-line @typescript-eslint/no-unused-vars getChildMenu, // eslint-disable-next-line @typescript-eslint/no-unused-vars forceHighlight, ...restProps } = this.props; return this.props.children({ ...restProps, rootRef: this.props.rootRef ? this.props.rootRef : this.rootRef, activedescendantId: this.optionIds[this.state.highlightedIndex], getRequiredItemProps: (item, index) => this.getRequiredItemProps(item, index), handleMouseLeave: this.handleMouseLeave, highlightedIndex: this.state.highlightedIndex, isFocused: this.state.isFocused, // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-ignore handleKeyDown: this.props.keyboardControlNode.current ? event => {} : this.onKeyDown, focusMenu: this.focusMenu, unfocusMenu: this.unfocusMenu }); } } // Remove when MenuStatefulContainer is converted to a functional component. _defineProperty(MenuStatefulContainerInner, "defaultProps", DEFAULT_PROPS); const MenuStatefulContainer = props => { return /*#__PURE__*/React.createElement(MenuStatefulContainerInner, _extends({ uidSeed: (0, _reactUid.useUIDSeed)() }, props)); }; MenuStatefulContainer.defaultProps = DEFAULT_PROPS; var _default = exports.default = MenuStatefulContainer;