UNPKG

react-elegant-ui

Version:

Elegant UI components, made by BEM best practices for react

246 lines 9.66 kB
var __assign = this && this.__assign || function () { __assign = Object.assign || function (t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = this && this.__rest || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __read = this && this.__read || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useComponentRegistry } from '../../lib/di'; import { makeChain } from '../../lib/makeChain'; import { Keys, isKeyCode } from '../../lib/keyboard'; import { mergeProps } from '../../lib/merge'; import { getDisplayName } from '../../lib/getDisplayName'; import { useRefMix } from '../../hooks/useRefMix'; import { useToggleable } from '../../hooks/behavior/useToggleable'; import { cnSelect, Select as BaseSelect } from './Select'; import { SelectTriggerContext } from './Trigger/Select-Trigger'; export * from './Select'; /** * Implementation of select for desktop * * It implement as HOC to allow use any `BaseSelect` implementation */ var withDesktopSelect = function (BaseSelect) { return function (_a) { var nocollapse = _a.nocollapse, addonBeforeMenu = _a.addonBeforeMenu, addonAfterMenu = _a.addonAfterMenu, props = __rest(_a, ["nocollapse", "addonBeforeMenu", "addonAfterMenu"]); var options = props.options, value = props.value, opened = props.opened, disabled = props.disabled, addonAfter = props.addonAfter, innerRef = props.innerRef, triggerRefExternal = props.triggerRef, setValue = props.setValue, setOpenedExternal = props.setOpened; var isMultiple = Array.isArray(value); var pickKeys = useMemo(function () { return [Keys.ENTER, Keys.SPACE]; }, []); // Nodes refs var selectRef = useRef(null); var triggerRef = useRef(null); // Opened state management var _b = useToggleable({ state: opened, setState: setOpenedExternal }), toggleOpened = _b.toggle, setOpened = _b.setState; // Value setter var setValueProxy = useCallback(function (value) { if (setValue !== undefined) { setValue(value); } }, [setValue]); // Enable ignore keyboard press events for trigger while listbox is open var pressLock = useRef(false); // It should be disabled only by keyup or blur event on press key var disablePressLock = useCallback(function (force) { // Never unlock for multiple type. User must close it by escape if (force || !isMultiple) { pressLock.current = false; } }, [isMultiple]); var updatePressLock = useCallback(function () { if (opened) { pressLock.current = true; } }, [opened]); useEffect(updatePressLock, [updatePressLock]); // Focus to trigger by close var focusToTrigger = useRef(false); useEffect(function () { if (!opened && focusToTrigger.current && triggerRef.current !== null) { triggerRef.current.focus(); } focusToTrigger.current = false; }, [opened]); // Close by pick when type is not multiple var onPick = useCallback(function () { if (!isMultiple && !nocollapse) { focusToTrigger.current = true; setOpened(false); } }, [isMultiple, nocollapse, setOpened]); // Handle close event var onCloseHandler = useCallback(function (_, source) { focusToTrigger.current = source === 'esc'; disablePressLock(true); setOpened(false); }, [disablePressLock, setOpened]); // Set handler for close by unfocus // NOTE: actually by focus on other elements, but maybe should handle blur? useEffect(function () { if (selectRef.current === null || !opened || disabled) return; var hostNode = selectRef.current; var rootNode = hostNode.getRootNode(); var shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : null; // Close when focus outside root of component var closeByFocusOutside = function (evt) { // Skip empty target if (!(evt.target instanceof Node)) return; // Skip global event while target is ShadowRoot // It will handle in next call and target will contain real node, instead ShadowRoot host if (shadowHost !== null && evt.target === shadowHost) return; if (!hostNode.contains(evt.target)) { setOpened(false); } }; // Add global handler document.addEventListener('focusin', closeByFocusOutside); // Add handler for ShadowRoot for work even with `mode: close` if (shadowHost !== null) { rootNode.addEventListener('focusin', closeByFocusOutside); } return function () { document.removeEventListener('focusin', closeByFocusOutside); if (shadowHost !== null) { rootNode.addEventListener('focusin', closeByFocusOutside); } }; }, [opened, disabled, setOpened, disablePressLock]); // Toggle by press var onPressHandler = useCallback(function (evt) { // Skip synthetic press event (by enter only) due to focus after press if (evt.pointerType === 'virtual') return; toggleOpened(); }, [toggleOpened]); // Toggle by keyboard var onKeyDownHandler = useCallback(function (evt) { if (disabled) return; if (isKeyCode(evt.key, [Keys.UP, Keys.DOWN])) { // Prevent scroll of parent evt.preventDefault(); // Toggle by release key if (evt.type === 'keyup') { setOpened(true); } } else if (isKeyCode(evt.nativeEvent.code, pickKeys)) { // Prevent keyboard events while key is down and until keyup or blur events if (evt.type === 'keydown' && pressLock.current) { evt.stopPropagation(); } else if (evt.type === 'keyup') { disablePressLock(); } } }, [disablePressLock, disabled, pickKeys, setOpened]); var _c = __read(useState(), 2), cursorIdRef = _c[0], cursorIdRefSet = _c[1]; var cursorNodeId = opened ? cursorIdRef !== null && cursorIdRef !== void 0 ? cursorIdRef : undefined : undefined; // Inject props to `Trigger` var SelectTriggerCtxObj = useContext(SelectTriggerContext); var onKeyDownCapture = SelectTriggerCtxObj.onKeyDownCapture, onKeyUpCapture = SelectTriggerCtxObj.onKeyUpCapture, onBlurCapture = SelectTriggerCtxObj.onBlurCapture, onFocusCapture = SelectTriggerCtxObj.onFocusCapture; var SelectTriggerContextMix = useMemo(function () { return mergeProps(SelectTriggerCtxObj, { 'aria-activedescendant': cursorNodeId, onPress: onPressHandler, onKeyDownCapture: makeChain(onKeyDownHandler, onKeyDownCapture), onKeyUpCapture: makeChain(onKeyDownHandler, onKeyUpCapture), // Force disable pressLock while blur onBlurCapture: makeChain(function () { disablePressLock(true); }, onBlurCapture), // Restore pressLock after focus on inner elements onFocusCapture: makeChain(updatePressLock, onFocusCapture) }); }, [SelectTriggerCtxObj, cursorNodeId, onPressHandler, onKeyDownHandler, onKeyDownCapture, onKeyUpCapture, onBlurCapture, updatePressLock, onFocusCapture, disablePressLock]); // Ref mixes var innerRefMix = useRefMix(selectRef, innerRef); var triggerRefMix = useRefMix(triggerRef, triggerRefExternal); // Get deps components var _d = useComponentRegistry(cnSelect()), List = _d.List, Popup = _d.Popup; // TODO: check acessability, maybe need bind menu to button by `id` return /*#__PURE__*/React.createElement(SelectTriggerContext.Provider, { value: SelectTriggerContextMix }, /*#__PURE__*/React.createElement(BaseSelect, __assign({}, props, { options: options, value: value, opened: opened, disabled: disabled, innerRef: innerRefMix, triggerRef: triggerRefMix, addonAfter: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Popup, { target: "anchor", anchor: selectRef, visible: opened, onClose: onCloseHandler }, addonBeforeMenu, /*#__PURE__*/React.createElement(List, { visible: opened, type: isMultiple ? 'checkbox' : 'radio', disabled: disabled, isFocused: opened, items: options, value: value, setValue: setValueProxy, pickKeys: pickKeys, onPick: onPick, cursorIdRef: cursorIdRefSet }), addonAfterMenu), addonAfter) }))); }; }; export var Select = withDesktopSelect(BaseSelect); Select.displayName = getDisplayName(BaseSelect);