baseui
Version:
A React Component library implementing the Base design language
401 lines (396 loc) • 17 kB
JavaScript
"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;