UNPKG

@momentum-ui/react-collaboration

Version:

Cisco Momentum UI Framework for React Collaboration Applications

244 lines 13.4 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; }; import React, { forwardRef, useRef, useEffect, useCallback, useLayoutEffect, useState, } from 'react'; import classnames from 'classnames'; import { v4 as uuidv4 } from 'uuid'; import './ListItemBase.style.scss'; import { DEFAULTS, KEYS, SHAPES, SIZES, STYLE } from './ListItemBase.constants'; import ListItemBaseSection from '../ListItemBaseSection'; import { verifyTypes } from '../../helpers/verifyTypes'; import FocusRing from '../FocusRing'; import { usePress } from '@react-aria/interactions'; import { useListContext } from '../List/List.utils'; import Text from '../Text'; import { getListItemBaseTabIndex } from './ListItemBase.utils'; import { useMutationObservable } from '../../hooks/useMutationObservable'; import { usePrevious } from '../../hooks/usePrevious'; import { getKeyboardFocusableElements } from '../../utils/navigation'; import { useFocusAndFocusWithinState } from '../../hooks/useFocusState'; import { Tooltip as MdcTooltip } from '@momentum-design/components/dist/react'; //TODO: Implement multi-line var ListItemBase = function (props, providedRef) { var className = props.className, children = props.children, _a = props.shape, shape = _a === void 0 ? DEFAULTS.SHAPE : _a, _b = props.size, size = _b === void 0 ? DEFAULTS.SIZE(shape || DEFAULTS.SHAPE) : _b, _c = props.isDisabled, isDisabled = _c === void 0 ? DEFAULTS.IS_DISABLED : _c, _d = props.isPadded, isPadded = _d === void 0 ? DEFAULTS.IS_PADDED : _d, _e = props.role, role = _e === void 0 ? DEFAULTS.ROLE : _e, _f = props.focusChild, focusChild = _f === void 0 ? DEFAULTS.FOCUS_CHILD : _f, isSelected = props.isSelected, style = props.style, itemIndex = props.itemIndex, _g = props.interactive, interactive = _g === void 0 ? DEFAULTS.INTERACTIVE : _g, onPress = props.onPress, lang = props.lang, _h = props.allowTextSelection, allowTextSelection = _h === void 0 ? DEFAULTS.ALLOW_TEXT_SELECTION : _h, onFocus = props.onFocus, onBlur = props.onBlur, onBlurWithin = props.onBlurWithin, onFocusWithin = props.onFocusWithin, tooltipProps = props.tooltipProps, rest = __rest(props, ["className", "children", "shape", "size", "isDisabled", "isPadded", "role", "focusChild", "isSelected", "style", "itemIndex", "interactive", "onPress", "lang", "allowTextSelection", "onFocus", "onBlur", "onBlurWithin", "onFocusWithin", "tooltipProps"]); var itemId = useState(rest.id || uuidv4())[0]; var content, start, middle, end; var listContext = useListContext(); var internalRef = useRef(); var _j = useFocusAndFocusWithinState({ onFocus: onFocus, onBlur: onBlur, onBlurWithin: onBlurWithin, onFocusWithin: onFocusWithin, }), focusProps = _j.focusProps, isFocusedWithin = _j.isFocusedWithin; var ref = internalRef; if (providedRef && typeof providedRef !== 'function') { ref = providedRef; } // When used in a popover, the ref will be a callback. // We need to update this callback ref, so the popover // knows about the dom element, but we can't use the callback // ref directly because we want it to be a useRef style ref // We useLayoutEffect so that it happens in time for tippy // to use the ref when adding event handlers useLayoutEffect(function () { if (providedRef) { if (typeof providedRef === 'function') { providedRef(ref.current); } } }); if (shape === SHAPES.isPilled && (size === SIZES[40] || size === SIZES[70])) { console.warn('ListItemBase: This variation is against the design spec. Rounded List Items can only be size 32 or 50.'); } if (Array.isArray(children)) { if (children.length > 3) { console.warn('ListItemBase: This component can only have at most 3 sections inside.'); } else { if (verifyTypes(children, ListItemBaseSection)) { start = children[0]; middle = children[1]; end = children[2]; content = (React.createElement(React.Fragment, null, start, middle, end)); } else if (verifyTypes(children, Text)) { content = children; } else { console.warn('ListItemBase: this component can only receive ListItemBaseSection as children.'); } } } else { content = children; } // The keyboard press events are not propagated // To make popovers work with click, we manually call the click event var internalOnPress = useCallback(function (event) { if (event.pointerType === 'keyboard') { ref.current.click(); } if (onPress) { onPress(event); } }, [onPress, ref]); var _k = usePress(__assign({ preventFocusOnPress: true, isDisabled: !interactive, onPress: internalOnPress }, rest)), pressProps = _k.pressProps, isPressed = _k.isPressed; // This is a workaround because react-aria is killing the mouse/pointer events // It determines whether to prevent default by whether the element is draggable or not // So we set it to draggable on mouse down and pointer down and then set it back to false // This allows text selection to work, which requires the pointer events but still allows // click to work via the usePress hook // see https://github.com/adobe/react-spectrum/issues/2956 // If react-aria ever fix this, this workaround can be removed var listItemPressProps = __assign(__assign({}, pressProps), { onMouseDown: function (event) { var _a; event.target.draggable = true; (_a = pressProps.onMouseDown) === null || _a === void 0 ? void 0 : _a.call(pressProps, event); event.target.draggable = false; }, onPointerDown: function (event) { var _a; event.target.draggable = true; (_a = pressProps.onPointerDown) === null || _a === void 0 ? void 0 : _a.call(pressProps, event); event.target.draggable = false; }, onKeyDown: function (event) { if (ref.current === document.activeElement || event.key === KEYS.TAB_KEY) { pressProps.onKeyDown(event); } } }); /** * Focus management */ var setCurrentFocus = listContext === null || listContext === void 0 ? void 0 : listContext.setCurrentFocus; var setUpdateFocusBlocked = listContext === null || listContext === void 0 ? void 0 : listContext.setUpdateFocusBlocked; var shouldFocusOnPress = (listContext === null || listContext === void 0 ? void 0 : listContext.shouldFocusOnPress) || false; var shouldItemFocusBeInset = (listContext === null || listContext === void 0 ? void 0 : listContext.shouldItemFocusBeInset) || DEFAULTS.SHOULD_ITEM_FOCUS_BE_INSET; var listFocusedWithin = listContext === null || listContext === void 0 ? void 0 : listContext.isFocusedWithin; var addFocusCallback = listContext === null || listContext === void 0 ? void 0 : listContext.addFocusCallback; var _l = useState(false), itemHasFocus = _l[0], setItemHasFocus = _l[1]; var listItemTabIndex = getListItemBaseTabIndex({ interactive: interactive, listContext: listContext, focus: itemHasFocus, }); var previousItemIndex = usePrevious(itemIndex); var previousItemHasFocus = usePrevious(itemHasFocus); // New elements have been added to the list, and the currently focused element has been pushed down // We don't want the focus to move to the new item, the one that now has this items original index // So we set the focus to new index of the originally focused item useLayoutEffect(function () { if (previousItemHasFocus && itemIndex !== previousItemIndex && listFocusedWithin) { setCurrentFocus(itemIndex); } }, [ itemHasFocus, itemIndex, listFocusedWithin, previousItemHasFocus, previousItemIndex, setCurrentFocus, ]); // makes sure that whenever an item is pressed, the list focus state gets updated as well useEffect(function () { if (setCurrentFocus && isPressed && shouldFocusOnPress && itemIndex !== undefined && !focusChild) { ref.current.focus(); setCurrentFocus(itemIndex); } }, [focusChild, isPressed, itemIndex, listContext, ref, setCurrentFocus, shouldFocusOnPress]); var updateTabIndexes = useCallback(function () { getKeyboardFocusableElements(ref.current, { includeTabbableOnly: false }) .filter(function (el) { return el.closest(".".concat(STYLE.wrapper)) === ref.current; }) .forEach(function (el) { return el.setAttribute('tabindex', isFocusedWithin || focusChild ? listItemTabIndex.toString() : '-1'); }); }, [ref, isFocusedWithin, focusChild, listItemTabIndex]); // We must not autofocus when rendering new elements // If a new element is rendered that has the same index as current focus (i.e. the focused element is replaced) // then it would otherwise try and focus, because focus is not blocked and the element is the current focus var isFirstRender = useRef(true); useEffect(function () { isFirstRender.current = false; }, []); var onFocusCallback = useCallback(function (focused, focusBlocked) { setItemHasFocus(focused); if (!focused || focusBlocked || isFirstRender.current) { return; } if (!ref.current) { return; } var firstFocusable = getKeyboardFocusableElements(ref.current, { includeTabbableOnly: false, }).filter(function (el) { return el.closest(".".concat(STYLE.wrapper)) === ref.current; })[0]; if (focusChild) { firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus(); } else { ref.current.focus(); } }, [focusChild, ref]); // This registers the list item base with the orientation based keyboard navigation hook // The hook will then tell the list item when it has focus and when the focus is blocked useLayoutEffect(function () { if (addFocusCallback) { addFocusCallback(itemIndex, onFocusCallback); return function () { addFocusCallback === null || addFocusCallback === void 0 ? void 0 : addFocusCallback(itemIndex, undefined); }; } }, [addFocusCallback, itemIndex, onFocusCallback]); // When the current focus moves from the things inside the list item // to the list item itself, we need to update the tab indexes of the things inside again useEffect(function () { if (listContext && !itemHasFocus && isFocusedWithin) { return; } updateTabIndexes(); }, [updateTabIndexes, listContext, itemHasFocus, isFocusedWithin]); useMutationObservable(ref.current, updateTabIndexes); useLayoutEffect(function () { if (itemHasFocus) { setUpdateFocusBlocked === null || setUpdateFocusBlocked === void 0 ? void 0 : setUpdateFocusBlocked(false); } }, [itemHasFocus, setUpdateFocusBlocked]); var listElement = (React.createElement(React.Fragment, null, React.createElement("li", __assign({ tabIndex: focusChild ? -1 : listItemTabIndex, style: style, ref: ref, "data-size": size, "data-disabled": isDisabled, "data-padded": isPadded, "data-shape": shape, "data-interactive": interactive && !focusChild, "data-allow-text-select": allowTextSelection, className: classnames(className, STYLE.wrapper, { active: isPressed || isSelected }), role: role, lang: lang }, focusProps, listItemPressProps, rest, { id: itemId }), content), !!(tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.content) && (React.createElement(MdcTooltip, __assign({ showArrow: true, placement: "top", triggerID: itemId }, tooltipProps), tooltipProps.content)))); if (focusChild) { return listElement; } return React.createElement(FocusRing, { isInset: shouldItemFocusBeInset }, listElement); }; /** * List Item Base component that can be used inside Lists/Menus */ var _ListItemBase = forwardRef(ListItemBase); _ListItemBase.displayName = 'ListItemBase'; export default _ListItemBase; //# sourceMappingURL=ListItemBase.js.map