diff --git a/CHANGELOG.md b/CHANGELOG.md index e8ccd7f085..27346a1a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Removed obsolete `play-sound-mute` icon from Teams theme @amramornov-ms ([#1598](https://github.com/stardust-ui/react/pull/1598)) - Add svg pointing beak to the `Tooltip` component in Teams theme @mnajdova ([#1580](https://github.com/stardust-ui/react/pull/1580)) - Add new values to the `brand`, `onyx` colors and `background4` token for default and brand color schemes in Teams theme @mnajdova ([#1581](https://github.com/stardust-ui/react/pull/1581)) +- Add additional logic for showing/hiding the `actionMenu` inside the `ChatMessage` in Teams theme, based on a variable @mnajdova ([#1590](https://github.com/stardust-ui/react/pull/1590)) ### Documentation - Ensure docs content doesn't overlap with sidebar @kuzhelov ([#1568](https://github.com/stardust-ui/react/pull/1568)) diff --git a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ChatWithPopover.tsx b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ChatWithPopover.tsx index fd84294723..12b5cc5b43 100644 --- a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ChatWithPopover.tsx +++ b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ChatWithPopover.tsx @@ -1,124 +1,158 @@ -import { Chat, Provider, Avatar } from '@stardust-ui/react' +import { Chat, Provider, Avatar, ChatMessageProps } from '@stardust-ui/react' import * as React from 'react' import Popover from './Popover' +import ReactionPopup from './ReactionPopup' +import { Ref } from '@stardust-ui/react-component-ref' + +const reactions = [ + { + icon: 'thumbs up', + content: '1K', + key: 'likes', + variables: { meReacting: true }, + }, + { + icon: 'thumbs down', + content: 2, + key: 'dislikes', + }, +] + +const reactionsWithPopup = reactions.map(reaction => render => + render(reaction, (Component, props) => ), +) const janeAvatar = { image: 'public/images/avatar/small/ade.jpg', status: { color: 'green', icon: 'check' }, } -const ChatWithPopover = () => ( - ({ - '& a': { - color: siteVariables.colors.brand[600], - }, - }), - }, - Menu: { - root: { - background: '#fff', - transition: 'opacity 0.2s', - position: 'absolute', +const ChatWithPopover = () => { + return ( + ({ + '& a': { + color: siteVariables.colors.brand[600], + }, + }), + }, + Menu: { + root: { + background: '#fff', + transition: 'opacity 0.2s', + position: 'absolute', - '& a:focus': { - textDecoration: 'none', - color: 'inherit', - }, - '& a': { - color: 'inherit', - }, + '& a:focus': { + textDecoration: 'none', + color: 'inherit', + }, + '& a': { + color: 'inherit', + }, - '& .smile-emoji': { - position: 'absolute', - opacity: 0, - zIndex: -1, - }, + '& .smile-emoji': { + position: 'absolute', + opacity: 0, + zIndex: -1, + }, - '&.focused .smile-emoji': { - position: 'initial', - zIndex: 'initial', - opacity: 1, - }, + '&.focused .smile-emoji': { + position: 'initial', + zIndex: 'initial', + opacity: 1, + }, - '&:hover .smile-emoji': { - position: 'initial', - zIndex: 'initial', - opacity: 1, + '&:hover .smile-emoji': { + position: 'initial', + zIndex: 'initial', + opacity: 1, + }, }, }, }, - }, - }} - > - } - author="Jane Doe" - content={{ - content: ( - - Link Hover me to see the actions Some Link - - ), - }} - timestamp="Yesterday, 10:15 PM" - /> - ), - }, - gutter: { content: }, - }, - { - key: 'b', - message: { - content: ( - } - author="Jane Doe" - content={{ - content: ( - - Link Hover me to see the actions Some Link - - ), - }} - timestamp="Yesterday, 10:15 PM" - /> - ), + }} + > + + Link Hover me to see the actions Some Link + + ), + }} + reactionGroup={{ + items: reactionsWithPopup, + }} + timestamp="Yesterday, 10:15 PM" + /> + ), + }, + gutter: { content: }, }, - gutter: { content: }, - }, - { - key: 'c', - message: { - content: ( - } - author="Jane Doe" - content={{ - content: ( - - Link Hover me to see the actions Some Link - - ), - }} - timestamp="Yesterday, 10:15 PM" - /> - ), + { + key: 'b', + message: { + content: ( + + Link Hover me to see the actions Some Link + + ), + }} + reactionGroup={{ + items: reactionsWithPopup, + }} + timestamp="Yesterday, 10:15 PM" + /> + ), + }, + gutter: { content: }, }, - gutter: { content: }, - }, - ]} - /> - -) + ]} + /> + + ) +} + +const TeamsChatMessage: React.FC = (props: ChatMessageProps) => { + const [showActionMenu, setShowActionMenu] = React.useState(false) + const [forceShowActionMenu, setForceShowActionMenu] = React.useState(false) + const [chatMessageElement, setChatMessageElement] = React.useState(null) + + const handleBlur = e => !e.currentTarget.contains(e.relatedTarget) && setShowActionMenu(false) + + return ( + + + } + onMouseEnter={() => setShowActionMenu(true)} + onMouseLeave={() => !forceShowActionMenu && setShowActionMenu(false)} + onFocus={() => setShowActionMenu(true)} + onBlur={handleBlur} + variables={{ showActionMenu }} + /> + + ) +} export default ChatWithPopover diff --git a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/Popover.tsx b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/Popover.tsx index ca967cc6b1..145d588a11 100644 --- a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/Popover.tsx +++ b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/Popover.tsx @@ -4,6 +4,9 @@ import cx from 'classnames' export interface PopoverProps { className?: string + onForceShowActionMenuChange?: (val: boolean) => void + onShowActionMenuChange?: (val: boolean) => void + chatMessageElement?: HTMLElement } interface PopoverState { @@ -31,10 +34,20 @@ class Popover extends React.Component { this.setState({ focused: e.currentTarget.contains(e.relatedTarget) }) } + handleActionableItemClick = e => { + const { onShowActionMenuChange, chatMessageElement } = this.props + onShowActionMenuChange(false) + // Currently when the action menu is closed because of some actionable item is clicked, we focus the ChatMessage + // this was not in the spec, so it may be changed if the requirement is different + e.type === 'keydown' && chatMessageElement && chatMessageElement.focus() + } + render() { + const { onShowActionMenuChange, onForceShowActionMenuChange, ...rest } = this.props + delete rest.chatMessageElement return ( { icon: 'smile', className: 'smile-emoji', 'aria-label': 'smile one', + onClick: this.handleActionableItemClick, }, { key: 'smile2', icon: 'smile', className: 'smile-emoji', 'aria-label': 'smile two', + onClick: this.handleActionableItemClick, }, { key: 'smile3', icon: 'smile', className: 'smile-emoji', 'aria-label': 'smile three', + onClick: this.handleActionableItemClick, }, { key: 'a', icon: 'thumbs up', 'aria-label': 'thumbs up', + onClick: this.handleActionableItemClick, }, { key: 'c', icon: 'ellipsis horizontal', + onMenuOpenChange: (e, { menuOpen }) => { + onShowActionMenuChange(true) + onForceShowActionMenuChange(menuOpen) + }, 'aria-label': 'more options', indicator: false, menu: { diff --git a/docs/src/prototypes/chatMessages/MessageReactionsWithPopup/ReactionPopup.tsx b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx similarity index 72% rename from docs/src/prototypes/chatMessages/MessageReactionsWithPopup/ReactionPopup.tsx rename to docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx index cd2166ef05..bf1100042d 100644 --- a/docs/src/prototypes/chatMessages/MessageReactionsWithPopup/ReactionPopup.tsx +++ b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx @@ -9,13 +9,13 @@ const getAriaLabel = ({ content: numberOfPersons, icon: emojiType }: ReactionPro return `${numberOfPersons} people reacted this message with a ${emojiType} emoji. Open menu to see people who reacted.` } -class ReactionPopup extends React.Component { +class ReactionPopup extends React.Component { state = { open: false, } handleKeyDownOnMenu = e => { - if ((e.shiftKey && e.keyCode === keyboardKey.Tab) || e.keyCode === keyboardKey.Tab) { + if (e.keyCode === keyboardKey.Tab) { this.setState({ open: false }) } } @@ -35,16 +35,14 @@ class ReactionPopup extends React.Component { aria-label={getAriaLabel(this.props)} /> } - content={{ - content: ( - - ), - }} + content={ + + } inline on="hover" open={this.state.open} diff --git a/docs/src/prototypes/chatMessages/MessageReactionsWithPopup/index.tsx b/docs/src/prototypes/chatMessages/MessageReactionsWithPopup/index.tsx deleted file mode 100644 index 27fa00631c..0000000000 --- a/docs/src/prototypes/chatMessages/MessageReactionsWithPopup/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import * as React from 'react' -import * as _ from 'lodash' -import { Avatar, Chat } from '@stardust-ui/react' -import ReactionPopup from './ReactionPopup' - -const reactions = [ - { icon: 'thumbs up', content: '1K', key: 'likes', variables: { meReacting: true } }, - { icon: 'thumbs down', content: 2, key: 'dislikes' }, -] - -const reactionsWithPopup = _.map(reactions, reaction => render => - render(reaction, (Component, props) => ), -) - -const actionMenu = { - iconOnly: true, - items: [ - { key: 'like', icon: 'like', title: 'Like' }, - { key: 'more', icon: 'more', title: 'More actions' }, - ], -} - -const items = [ - { - attached: 'top', - contentPosition: 'end', - message: { - content: ( - - ), - }, - key: 'message-1', - }, - { - attached: 'bottom', - contentPosition: 'end', - key: 'message-2', - message: { - content: ( - - ), - }, - }, - { - gutter: { - content: , - }, - message: { - content: ( - - ), - }, - key: 'message-3', - }, -] - -const MessageReactionsWithPopup = () => - -export default MessageReactionsWithPopup diff --git a/docs/src/prototypes/chatMessages/index.tsx b/docs/src/prototypes/chatMessages/index.tsx index 30bbd585fd..eef71a9354 100644 --- a/docs/src/prototypes/chatMessages/index.tsx +++ b/docs/src/prototypes/chatMessages/index.tsx @@ -1,13 +1,12 @@ import * as React from 'react' import { PrototypeSection, ComponentPrototype } from '../Prototypes' -import MessageReactionsWithPopup from './MessageReactionsWithPopup' import ImportantAndMentionMessages from './ImportantAndMentionMessages' import ChatMessageWithPopover from './ChatMessageWithPopover' export default () => ( @@ -18,11 +17,5 @@ export default () => ( > - - - ) diff --git a/packages/react/src/components/Menu/MenuItem.tsx b/packages/react/src/components/Menu/MenuItem.tsx index 239bed3dc9..debf8ead5c 100644 --- a/packages/react/src/components/Menu/MenuItem.tsx +++ b/packages/react/src/components/Menu/MenuItem.tsx @@ -395,7 +395,7 @@ class MenuItem extends AutoControlledComponent, MenuIt } }) - if (shouldStopPropagation) { + if (forceTriggerFocus || shouldStopPropagation) { e.stopPropagation() } } diff --git a/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts b/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts index 2554393647..95ada041a1 100644 --- a/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts +++ b/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts @@ -1,4 +1,5 @@ import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' +import * as _ from 'lodash' import { default as ChatMessage, ChatMessageProps, @@ -53,12 +54,18 @@ const chatMessageStyles: ComponentSlotStylesInput< ...getBorderFocusStyles({ siteVariables, isFromKeyboard: p.isFromKeyboard }), - ':hover': { - [`& .${ChatMessage.slotClassNames.actionMenu}`]: { - opacity: 1, - width: 'auto', + // actions menu's appearance can be controlled by the value of showActionMenu variable - in this + // case this variable will serve the single source of truth on whether actions menu should be shown. + // Otherwise, if the variable is not provided, the default appearance logic will be used for actions menu. + ...(_.isNil(v.showActionMenu) && { + ':hover': { + [`& .${ChatMessage.slotClassNames.actionMenu}`]: { + opacity: 1, + width: 'auto', + }, }, - }, + }), + ...(p.attached === true && { [p.mine ? 'borderTopRightRadius' : 'borderTopLeftRadius']: 0, [p.mine ? 'borderBottomRightRadius' : 'borderBottomLeftRadius']: 0, @@ -82,13 +89,22 @@ const chatMessageStyles: ComponentSlotStylesInput< position: 'absolute', right: v.actionMenuPositionRight, top: v.actionMenuPositionTop, - overflow: p.focused ? 'visible' : 'hidden', - // hide and squash actions menu to prevent accidental hovers over its invisible area - opacity: p.focused ? 1 : 0, - width: p.focused ? 'auto' : 0, - }), + ...(_.isNil(v.showActionMenu) && { + overflow: p.focused ? 'visible' : 'hidden', + // hide and squash actions menu to prevent accidental hovers over its invisible area + opacity: p.focused ? 1 : 0, + width: p.focused ? 'auto' : 0, + }), + ...(!_.isNil(v.showActionMenu) && { + overflow: v.showActionMenu ? 'visible' : 'hidden', + // opacity should always be preferred over visibility in order to avoid accessibility bugs in + // JAWS behavior on Windows + opacity: v.showActionMenu ? 1 : 0, + width: v.showActionMenu ? 'auto' : 0, + }), + }), author: ({ props: p, variables: v }): ICSSInJSStyle => ({ ...((p.mine || p.attached === 'bottom' || p.attached === true) && screenReaderContainerStyles), color: v.authorColor, diff --git a/packages/react/src/themes/teams/components/Chat/chatMessageVariables.ts b/packages/react/src/themes/teams/components/Chat/chatMessageVariables.ts index f92b2e400c..c613769bd8 100644 --- a/packages/react/src/themes/teams/components/Chat/chatMessageVariables.ts +++ b/packages/react/src/themes/teams/components/Chat/chatMessageVariables.ts @@ -26,6 +26,7 @@ export interface ChatMessageVariables { isImportantColor: string badgeTextColor: string reactionGroupMarginLeft: string + showActionMenu?: boolean timestampColorMine: string } @@ -55,5 +56,6 @@ export default (siteVars): ChatMessageVariables => ({ isImportantColor: siteVars.colors.red[400], badgeTextColor: siteVars.colors.white, reactionGroupMarginLeft: pxToRem(12), + showActionMenu: undefined, timestampColorMine: siteVars.colors.grey[500], })