@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
247 lines • 13.5 kB
JavaScript
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
*/
/**
* @deprecated Use the equivalent from momentum.design (NPM: `@momentum-design/components/dist/react`)
*/
var _ListItemBase = forwardRef(ListItemBase);
_ListItemBase.displayName = 'ListItemBase';
export default _ListItemBase;
//# sourceMappingURL=ListItemBase.js.map