@fluentui/react-northstar
Version:
A themable React component library.
531 lines (526 loc) • 23 kB
JavaScript
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
import _invoke from "lodash/invoke";
import _isNil from "lodash/isNil";
var _excluded = ["inline", "showActionMenu"];
import { chatMessageBehavior, IS_FOCUSABLE_ATTRIBUTE, keyboardKey, menuAsToolbarBehavior } from '@fluentui/accessibility';
import { getElementType, useAccessibility, useAutoControlled, useContextSelector, useFluentContext, useStyles, useTelemetry, useUnhandledProps, useMergedRefs, mergeVariablesOverrides } from '@fluentui/react-bindings';
import { Ref } from '@fluentui/react-component-ref';
import * as customPropTypes from '@fluentui/react-proptypes';
import cx from 'classnames';
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { childrenExist, commonPropTypes, createShorthand, createShorthandFactory, getOrGenerateIdFromShorthand, rtlTextContainer } from '../../utils';
import { getScrollParent, partitionPopperPropsFromShorthand, usePopper } from '../../utils/positioner';
import { Box } from '../Box/Box';
import { Flex } from '../Flex/Flex';
import { Label } from '../Label/Label';
import { Menu } from '../Menu/Menu';
import { PortalInner } from '../Portal/PortalInner';
import { Reaction } from '../Reaction/Reaction';
import { Text } from '../Text/Text';
import { useChatContextSelectors } from './chatContext';
import { ChatItemContext } from './chatItemContext';
import { ChatMessageDetails } from './ChatMessageDetails';
import { ChatMessageHeader } from './ChatMessageHeader';
import { ChatMessageReadStatus } from './ChatMessageReadStatus';
import { ChatMessageContent } from './ChatMessageContent';
export var chatMessageClassName = 'ui-chat__message';
export var chatMessageSlotClassNames = {
actionMenu: chatMessageClassName + "__actions",
author: chatMessageClassName + "__author",
badge: chatMessageClassName + "__badge",
bar: chatMessageClassName + "__bar",
body: chatMessageClassName + "__body",
bubble: chatMessageClassName + "__bubble",
bubbleInset: chatMessageClassName + "__bubble-inset",
compactBody: chatMessageClassName + "__compact-body",
reactionGroup: chatMessageClassName + "__reactions",
timestamp: chatMessageClassName + "__timestamp"
};
function partitionActionMenuPropsFromShorthand(value) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
var _ref = value,
inline = _ref.inline,
showActionMenu = _ref.showActionMenu,
props = _objectWithoutPropertiesLoose(_ref, _excluded);
return [props, inline != null ? inline : true, showActionMenu];
}
return [value, true, false];
}
/**
* A ChatMessage represents a single message in chat.
*/
export var ChatMessage = /*#__PURE__*/function () {
var ChatMessage = /*#__PURE__*/React.forwardRef(function (inputProps, ref) {
var context = useFluentContext();
var _useTelemetry = useTelemetry(ChatMessage.displayName, context.telemetry),
setStart = _useTelemetry.setStart,
setEnd = _useTelemetry.setEnd;
setStart();
var parentAttached = useContextSelector(ChatItemContext, function (v) {
return v.attached;
});
var chatProps = useChatContextSelectors({
density: function density(v) {
return v.density;
},
accessibility: function accessibility(v) {
return v.behaviors.message;
}
});
var props = Object.assign({}, inputProps, {
density: inputProps.density === undefined ? chatProps.density : inputProps.density,
accessibility: inputProps.accessibility === undefined ? chatProps.accessibility || chatMessageBehavior : inputProps.accessibility
});
var accessibility = props.accessibility,
_props$attached = props.attached,
attached = _props$attached === void 0 ? parentAttached : _props$attached,
author = props.author,
badge = props.badge,
badgePosition = props.badgePosition,
children = props.children,
className = props.className,
compactBody = props.compactBody,
content = props.content,
density = props.density,
design = props.design,
details = props.details,
header = props.header,
mine = props.mine,
positionActionMenu = props.positionActionMenu,
reactionGroup = props.reactionGroup,
reactionGroupPosition = props.reactionGroupPosition,
readStatus = props.readStatus,
styles = props.styles,
timestamp = props.timestamp,
overflow = props.unstable_overflow,
_props$unstable_layou = props.unstable_layout,
layout = _props$unstable_layou === void 0 ? 'default' : _props$unstable_layou,
variables = props.variables,
failed = props.failed,
bubble = props.bubble,
body = props.body,
bubbleInset = props.bubbleInset,
bubbleInsetContent = props.bubbleInsetContent,
headerContent = props.headerContent;
var isRefreshComfyLayout = layout === 'refresh' && density === 'comfy';
var _partitionPopperProps = partitionPopperPropsFromShorthand(props.actionMenu),
actionMenuOptions = _partitionPopperProps[0],
positioningProps = _partitionPopperProps[1];
var _partitionActionMenuP = partitionActionMenuPropsFromShorthand(actionMenuOptions),
actionMenu = _partitionActionMenuP[0],
inlineActionMenu = _partitionActionMenuP[1],
controlledShowActionMenu = _partitionActionMenuP[2];
var _useAutoControlled = useAutoControlled({
defaultValue: false,
value: controlledShowActionMenu
}),
showActionMenu = _useAutoControlled[0],
setShowActionMenu = _useAutoControlled[1];
var hasActionMenu = !_isNil(actionMenu);
var hasHeaderReactionGroup = !!reactionGroup && reactionGroupPosition === 'start';
var actionMenuId = React.useRef();
actionMenuId.current = getOrGenerateIdFromShorthand(chatMessageClassName + "-", actionMenu, actionMenuId.current);
var modifiers = React.useCallback(function (target, container) {
return positionActionMenu && [
// https://popper.js.org/docs/v2/modifiers/flip/
// Forces to flip only in "top-*" positions
{
name: 'flip',
options: {
fallbackPlacements: ['top']
}
}, overflow && {
name: 'preventOverflow',
options: {
boundary: getScrollParent(container)
}
}];
}, [positionActionMenu, overflow]);
var popperRef = React.useRef();
var _usePopper = usePopper(Object.assign({
align: 'end',
rtl: context.rtl,
position: 'above',
positionFixed: overflow,
enabled: hasActionMenu && positionActionMenu,
modifiers: modifiers
}, positioningProps, {
popperRef: useMergedRefs(positioningProps == null ? void 0 : positioningProps.popperRef, popperRef)
})),
actionsMenuTargetRef = _usePopper.targetRef,
actionsMenuRef = _usePopper.containerRef;
// `focused` state is used for show/hide actionMenu
var _React$useState = React.useState(false),
focused = _React$useState[0],
setFocused = _React$useState[1];
var getA11Props = useAccessibility(accessibility, {
actionHandlers: {
// prevents default FocusZone behavior, e.g., in ChatMessageBehavior, it prevents FocusZone from using arrow keys
// as navigation (only Tab key should work)
preventDefault: function preventDefault(event) {
// preventDefault only if event coming from inside the message
if (event.currentTarget !== event.target) {
event.preventDefault();
}
},
focus: function focus(event) {
var target = actionsMenuTargetRef.current;
if (target) {
target.focus();
event.stopPropagation();
}
}
},
debugName: ChatMessage.displayName,
mapPropsToBehavior: function mapPropsToBehavior() {
return {
hasActionMenu: hasActionMenu,
inlineActionMenu: inlineActionMenu,
actionMenuId: actionMenuId.current
};
},
rtl: context.rtl
});
var _useStyles = useStyles(ChatMessage.displayName, {
className: chatMessageClassName,
mapPropsToStyles: function mapPropsToStyles() {
return {
attached: attached,
badgePosition: badgePosition,
density: density,
focused: focused,
hasActionMenu: hasActionMenu,
hasBadge: !!badge,
hasHeaderReactionGroup: hasHeaderReactionGroup,
mine: mine,
showActionMenu: showActionMenu,
hasReactions: !!reactionGroup,
failed: failed,
layout: layout
};
},
mapPropsToInlineStyles: function mapPropsToInlineStyles() {
return {
className: className,
design: design,
styles: styles,
variables: variables
};
},
rtl: context.rtl
}),
classes = _useStyles.classes,
resolvedStyles = _useStyles.styles;
var handleFocus = function handleFocus(e) {
var _popperRef$current;
(_popperRef$current = popperRef.current) == null ? void 0 : _popperRef$current.updatePosition();
// react onFocus is called even when nested component receives focus (i.e. it bubbles)
// so when focus moves within actionMenu, the `focus` state in chatMessage remains true, and keeps actionMenu visible
setFocused(true);
_invoke(props, 'onFocus', e, props);
};
var handleBlur = function handleBlur(e) {
// `focused` controls is focused the whole `ChatMessage` or any of its children. When we're navigating
// with keyboard the focused element will be changed and there is no way to use `:focus` selector
var shouldPreserveFocusState = _invoke(e, 'currentTarget.contains', e.relatedTarget);
setFocused(shouldPreserveFocusState);
setShowActionMenu(false);
_invoke(props, 'onBlur', e, props);
};
var handleMouseEnter = function handleMouseEnter(e) {
var _popperRef$current2;
(_popperRef$current2 = popperRef.current) == null ? void 0 : _popperRef$current2.updatePosition();
if (hasActionMenu && !inlineActionMenu) {
setShowActionMenu(true);
}
_invoke(props, 'onMouseEnter', e, props);
};
var handleMouseLeave = function handleMouseLeave(e) {
if (!focused && hasActionMenu && !inlineActionMenu) {
setShowActionMenu(false);
}
_invoke(props, 'onMouseLeave', e, props);
};
var renderActionMenu = function renderActionMenu() {
var actionMenuElement = Menu.create(actionMenu, {
defaultProps: function defaultProps() {
var _ref2;
return _ref2 = {}, _ref2[IS_FOCUSABLE_ATTRIBUTE] = true, _ref2.accessibility = menuAsToolbarBehavior, _ref2.className = chatMessageSlotClassNames.actionMenu, _ref2.styles = resolvedStyles.actionMenu, _ref2;
},
overrideProps: {
id: actionMenuId.current
}
});
var content = actionMenuElement ? /*#__PURE__*/React.createElement(Ref, {
innerRef: actionsMenuRef
}, actionMenuElement) : actionMenuElement;
return inlineActionMenu || !content ? content : /*#__PURE__*/React.createElement(PortalInner, null, content);
};
var handleKeyDown = function handleKeyDown(e) {
if (hasActionMenu && !inlineActionMenu) {
var _actionsMenuRef$curre, _actionsMenuRef$curre2, _actionsMenuRef$curre3;
// reference: https://github.com/microsoft/fluentui/pull/17329
var toFocusItemInActionMenu = (_actionsMenuRef$curre = (_actionsMenuRef$curre2 = actionsMenuRef.current) == null ? void 0 : _actionsMenuRef$curre2.querySelector('[tabindex="0"]')) != null ? _actionsMenuRef$curre : (_actionsMenuRef$curre3 = actionsMenuRef.current) == null ? void 0 : _actionsMenuRef$curre3.querySelectorAll('[tabindex="-1"]:not([data-is-focusable="false"])')[0];
if (e.keyCode === keyboardKey.Enter) {
toFocusItemInActionMenu == null ? void 0 : toFocusItemInActionMenu.focus();
e.stopPropagation();
e.preventDefault();
}
if (e.keyCode === keyboardKey.Tab) {
// TAB/SHIFT+TAB cycles focus among actionMenu and focusable elements within chat message
var isShift = !!e.shiftKey;
var focusableElementsInsideMessage = e.currentTarget.querySelectorAll('[tabindex="-1"]:not([data-is-focusable="false"])');
var firstFocusableInsideMessage = focusableElementsInsideMessage[0];
var lastFocusableInsideMessage = focusableElementsInsideMessage[focusableElementsInsideMessage.length - 1];
if (e.target === toFocusItemInActionMenu) {
// focus is now inside action menu
// cycle focus into the first/last focusable element inside chat message
if (isShift) {
lastFocusableInsideMessage == null ? void 0 : lastFocusableInsideMessage.focus();
} else {
firstFocusableInsideMessage == null ? void 0 : firstFocusableInsideMessage.focus();
}
e.stopPropagation();
e.preventDefault();
} else {
var boundaryElementInsideMessage = isShift ? firstFocusableInsideMessage : lastFocusableInsideMessage;
if (e.target === boundaryElementInsideMessage) {
// focus is now on the first/last focusable element inside chat message
toFocusItemInActionMenu.focus(); // cycle focus back into action Menu
e.stopPropagation();
e.preventDefault();
}
}
}
}
_invoke(props, 'onKeyDown', e, props);
};
var childrenPropExists = childrenExist(children);
var rootClasses = childrenPropExists ? cx(classes.root, classes.content) : classes.root;
var ElementType = getElementType(props);
var unhandledProps = useUnhandledProps(ChatMessage.handledProps, props);
var badgeElement = Label.create(badge, {
defaultProps: function defaultProps() {
return {
className: chatMessageSlotClassNames.badge,
styles: resolvedStyles.badge
};
}
});
var reactionGroupElement = Reaction.Group.create(reactionGroup, {
defaultProps: function defaultProps() {
return {
className: chatMessageSlotClassNames.reactionGroup,
styles: resolvedStyles.reactionGroup
};
}
});
var actionMenuElement = renderActionMenu();
var authorElement = Text.create(author, {
defaultProps: function defaultProps() {
return {
size: density === 'comfy' ? 'small' : undefined,
styles: resolvedStyles.author,
className: chatMessageSlotClassNames.author
};
}
});
var timestampElement = Text.create(timestamp, {
defaultProps: function defaultProps() {
return {
size: 'small',
styles: resolvedStyles.timestamp,
timestamp: true,
className: chatMessageSlotClassNames.timestamp
};
}
});
var messageContent = createShorthand(ChatMessageContent, content, {
defaultProps: function defaultProps() {
return {
badgePosition: badgePosition,
density: density,
failed: failed,
hasBadge: !!badge,
mine: mine,
unstable_layout: layout
};
},
overrideProps: function overrideProps(predefinedProps) {
return {
variables: mergeVariablesOverrides(variables, predefinedProps.variables)
};
}
});
var detailsElement = createShorthand(ChatMessageDetails, details, {
defaultProps: function defaultProps() {
return {
attached: attached,
density: density,
hasHeaderReactionGroup: hasHeaderReactionGroup,
mine: mine
};
}
});
var readStatusElement = createShorthand(ChatMessageReadStatus, readStatus, {
defaultProps: function defaultProps() {
return {
density: density
};
}
});
var elements = /*#__PURE__*/React.createElement(React.Fragment, null);
if (density === 'compact') {
var headerElement = createShorthand(ChatMessageHeader, header);
var bodyElement = Box.create(compactBody || {}, {
defaultProps: function defaultProps() {
return getA11Props('compactBody', {
className: chatMessageSlotClassNames.compactBody,
styles: resolvedStyles.compactBody
});
},
overrideProps: function overrideProps() {
return {
content: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Flex.Item, {
grow: 1
}, /*#__PURE__*/React.createElement("div", null, authorElement, messageContent)), timestampElement, detailsElement, badgeElement)
};
}
});
elements = /*#__PURE__*/React.createElement(React.Fragment, null, actionMenuElement, /*#__PURE__*/React.createElement("div", {
className: chatMessageSlotClassNames.bar
}), headerElement, bodyElement, reactionGroupElement, readStatusElement);
} else if (isRefreshComfyLayout) {
var _headerElement = createShorthand(ChatMessageHeader, header || {}, {
defaultProps: function defaultProps() {
return {
styles: resolvedStyles.header,
content: /*#__PURE__*/React.createElement(React.Fragment, null, authorElement, headerContent, detailsElement)
};
}
});
var bubbleElement = Box.create(bubble || {}, {
defaultProps: function defaultProps() {
return getA11Props('bubble', {
className: chatMessageSlotClassNames.bubble,
styles: resolvedStyles.bubble
});
},
overrideProps: function overrideProps() {
return {
ref: actionsMenuTargetRef,
content: /*#__PURE__*/React.createElement(React.Fragment, null, actionMenuElement, messageContent, reactionGroupElement, readStatusElement),
onMouseEnter: function onMouseEnter(e) {
var _popperRef$current3;
(_popperRef$current3 = popperRef.current) == null ? void 0 : _popperRef$current3.updatePosition();
handleMouseEnter(e);
},
onMouseLeave: function onMouseLeave(e) {
handleMouseLeave(e);
}
};
}
});
var bubbleInsetElement = Box.create(bubbleInset || {}, {
defaultProps: function defaultProps() {
return {
as: 'span',
className: chatMessageSlotClassNames.bubbleInset,
styles: resolvedStyles.bubbleInset
};
},
overrideProps: function overrideProps() {
return {
content: /*#__PURE__*/React.createElement(React.Fragment, null, badgeElement, bubbleInsetContent, timestampElement)
};
}
});
var _bodyElement = Box.create(body || {}, {
defaultProps: function defaultProps() {
return getA11Props('body', {
className: chatMessageSlotClassNames.body,
styles: resolvedStyles.body
});
},
overrideProps: function overrideProps() {
return {
content: /*#__PURE__*/React.createElement(React.Fragment, null, bubbleElement, bubbleInsetElement)
};
}
});
elements = /*#__PURE__*/React.createElement(React.Fragment, null, _headerElement, _bodyElement);
} else {
var _headerElement2 = createShorthand(ChatMessageHeader, header || {}, {
overrideProps: function overrideProps() {
return {
content: /*#__PURE__*/React.createElement(React.Fragment, null, authorElement, timestampElement, detailsElement, reactionGroupPosition === 'start' && reactionGroupElement)
};
}
});
elements = /*#__PURE__*/React.createElement(React.Fragment, null, actionMenuElement, /*#__PURE__*/React.createElement("div", {
className: chatMessageSlotClassNames.bar
}), badgePosition === 'start' && badgeElement, _headerElement2, messageContent, reactionGroupPosition === 'end' && reactionGroupElement, badgePosition === 'end' && badgeElement, readStatusElement);
}
var element = /*#__PURE__*/React.createElement(Ref, {
innerRef: !isRefreshComfyLayout && actionsMenuTargetRef
}, getA11Props.unstable_wrapWithFocusZone( /*#__PURE__*/React.createElement(ElementType, getA11Props('root', Object.assign({
className: rootClasses,
onBlur: handleBlur,
onFocus: handleFocus,
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
onKeyDown: handleKeyDown,
ref: ref
}, rtlTextContainer.getAttributes({
forElements: [children]
}), unhandledProps)), childrenPropExists ? children : elements)));
setEnd();
return element;
});
ChatMessage.displayName = 'ChatMessage';
ChatMessage.defaultProps = {
badgePosition: 'end',
positionActionMenu: true,
reactionGroupPosition: 'start'
};
ChatMessage.propTypes = Object.assign({}, commonPropTypes.createCommon({
content: 'shorthand'
}), {
actionMenu: PropTypes.oneOfType([customPropTypes.itemShorthand, customPropTypes.collectionShorthand]),
attached: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['top', 'bottom'])]),
author: customPropTypes.itemShorthand,
badge: customPropTypes.itemShorthand,
badgePosition: PropTypes.oneOf(['start', 'end']),
compactBody: customPropTypes.itemShorthand,
density: PropTypes.oneOf(['comfy', 'compact']),
details: customPropTypes.itemShorthand,
header: customPropTypes.itemShorthand,
mine: PropTypes.bool,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
positionActionMenu: PropTypes.bool,
reactionGroup: PropTypes.oneOfType([customPropTypes.collectionShorthand, customPropTypes.itemShorthand]),
reactionGroupPosition: PropTypes.oneOf(['start', 'end']),
readStatus: customPropTypes.itemShorthand,
timestamp: customPropTypes.itemShorthand,
unstable_overflow: PropTypes.bool,
unstable_layout: PropTypes.oneOf(['default', 'refresh']),
failed: PropTypes.bool,
headerContent: PropTypes.node,
body: customPropTypes.itemShorthand,
bubble: customPropTypes.itemShorthand,
bubbleInset: customPropTypes.itemShorthand,
bubbleInsetContent: PropTypes.node
});
ChatMessage.handledProps = Object.keys(ChatMessage.propTypes);
ChatMessage.create = createShorthandFactory({
Component: ChatMessage,
mappedProp: 'content'
});
return ChatMessage;
}();
//# sourceMappingURL=ChatMessage.js.map