UNPKG

@fluentui/react-northstar

Version:
531 lines (526 loc) 23 kB
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