Simplify Composer buttons (#7678)

* Render a CollapsibleButton's children (needed by UploadButton)

* Make UploadButton ready to live inside an overflow menu

* Always show overflow menu in composer: main buttons are emoji and attach

* Re-order composer buttons as per design

* Re-word composer button captions to be simple nouns

* Don't rotate More options button when clicked

* Move the composer menu and dialogs 16px in from right

* Reduce shadow on composer More menu

* From review: remove else clause

* From review: take input out of button

* Update test snapshots

* Update snapshots
This commit is contained in:
Andy Balaam 2022-02-02 09:30:53 +00:00 committed by GitHub
parent c011fb7475
commit f5226f9d5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 98 deletions

View file

@ -41,7 +41,7 @@ limitations under the License.
} }
.mx_ContextualMenu_right { .mx_ContextualMenu_right {
right: 0; right: 16px;
} }
.mx_ContextualMenu.mx_ContextualMenu_withChevron_right { .mx_ContextualMenu.mx_ContextualMenu_withChevron_right {

View file

@ -21,7 +21,7 @@ limitations under the License.
border-top: 1px solid $primary-hairline-color; border-top: 1px solid $primary-hairline-color;
position: relative; position: relative;
padding-left: 42px; padding-left: 42px;
padding-right: 6px; padding-right: 16px;
} }
.mx_MessageComposer_replaced_wrapper { .mx_MessageComposer_replaced_wrapper {
@ -271,11 +271,6 @@ limitations under the License.
mask-image: url('$(res)/img/image-view/more.svg'); mask-image: url('$(res)/img/image-view/more.svg');
} }
.mx_MessageComposer_closeButtonMenu::before {
transform: rotate(90deg);
transform-origin: center;
}
.mx_MessageComposer_sendMessage { .mx_MessageComposer_sendMessage {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
@ -417,4 +412,6 @@ limitations under the License.
min-width: 150px; min-width: 150px;
width: max-content; width: max-content;
padding: 5px 10px 5px 0; padding: 5px 10px 5px 0;
box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.25);
border-radius: 8px;
} }

View file

@ -72,13 +72,11 @@ export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition
}, },
); );
// TODO: replace ContextMenuTooltipButton with a unified representation of
// the header buttons and the right panel buttons
return <React.Fragment> return <React.Fragment>
<CollapsibleButton <CollapsibleButton
className={className} className={className}
onClick={openMenu} onClick={openMenu}
title={_t("Share location")} title={_t("Location")}
/> />
{ contextMenu } { contextMenu }

View file

@ -25,7 +25,7 @@ interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
title: string; title: string;
} }
export const CollapsibleButton = ({ title, className, ...props }: ICollapsibleButtonProps) => { export const CollapsibleButton = ({ title, children, className, ...props }: ICollapsibleButtonProps) => {
const inOverflowMenu = !!useContext(OverflowMenuContext); const inOverflowMenu = !!useContext(OverflowMenuContext);
if (inOverflowMenu) { if (inOverflowMenu) {
return <MenuItem return <MenuItem
@ -33,10 +33,17 @@ export const CollapsibleButton = ({ title, className, ...props }: ICollapsibleBu
className={classNames("mx_CallContextMenu_item", className)} className={classNames("mx_CallContextMenu_item", className)}
> >
{ title } { title }
{ children }
</MenuItem>; </MenuItem>;
} }
return <AccessibleTooltipButton {...props} title={title} className={className} />; return <AccessibleTooltipButton
{...props}
title={title}
className={className}
>
{ children }
</AccessibleTooltipButton>;
}; };
export default CollapsibleButton; export default CollapsibleButton;

View file

@ -59,53 +59,47 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient: MatrixClient = useContext(MatrixClientContext); const matrixClient: MatrixClient = useContext(MatrixClientContext);
const { room, roomId } = useContext(RoomContext); const { room, roomId } = useContext(RoomContext);
return ( if (props.haveRecording) {
props.haveRecording return null;
? null
: props.narrowMode
? narrowMode(props, room, roomId, matrixClient)
: wideMode(props, room, roomId, matrixClient)
);
};
function wideMode(
props: IProps,
room: Room,
roomId: string,
matrixClient: MatrixClient,
): ReactElement {
return <>
{ pollButton(props, room) }
{ uploadButton(props, roomId) }
{ showLocationButton(props, room, roomId, matrixClient) }
{ emojiButton(props) }
{ showStickersButton(props) }
{ voiceRecordingButton(props) }
</>;
} }
function narrowMode( let mainButtons: ReactElement[];
props: IProps, let moreButtons: ReactElement[];
room: Room, if (props.narrowMode) {
roomId: string, mainButtons = [
matrixClient: MatrixClient, emojiButton(props),
): ReactElement { ];
moreButtons = [
uploadButton(props, roomId),
showStickersButton(props),
voiceRecordingButton(props),
pollButton(room),
showLocationButton(props, room, roomId, matrixClient),
];
} else {
mainButtons = [
emojiButton(props),
uploadButton(props, roomId),
];
moreButtons = [
showStickersButton(props),
voiceRecordingButton(props),
pollButton(room),
showLocationButton(props, room, roomId, matrixClient),
];
}
mainButtons = mainButtons.filter((x: ReactElement) => x);
moreButtons = moreButtons.filter((x: ReactElement) => x);
const moreOptionsClasses = classNames({ const moreOptionsClasses = classNames({
mx_MessageComposer_button: true, mx_MessageComposer_button: true,
mx_MessageComposer_buttonMenu: true, mx_MessageComposer_buttonMenu: true,
mx_MessageComposer_closeButtonMenu: props.isMenuOpen, mx_MessageComposer_closeButtonMenu: props.isMenuOpen,
}); });
const moreButtons = [
pollButton(props, room),
showLocationButton(props, room, roomId, matrixClient),
emojiButton(props),
showStickersButton(props),
voiceRecordingButton(props),
].filter(x => x);
return <> return <>
{ uploadButton(props, roomId) } { mainButtons }
<AccessibleTooltipButton <AccessibleTooltipButton
className={moreOptionsClasses} className={moreOptionsClasses}
onClick={props.toggleButtonMenu} onClick={props.toggleButtonMenu}
@ -123,7 +117,7 @@ function narrowMode(
</ContextMenu> </ContextMenu>
) } ) }
</>; </>;
} };
function emojiButton(props: IProps): ReactElement { function emojiButton(props: IProps): ReactElement {
return <EmojiButton return <EmojiButton
@ -174,7 +168,7 @@ const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) =>
<CollapsibleButton <CollapsibleButton
className={className} className={className}
onClick={openMenu} onClick={openMenu}
title={_t("Add emoji")} title={_t("Emoji")}
/> />
{ contextMenu } { contextMenu }
@ -219,7 +213,7 @@ class UploadButton extends React.Component<IUploadButtonProps> {
dis.dispatch({ action: 'require_registration' }); dis.dispatch({ action: 'require_registration' });
return; return;
} }
this.uploadInput.current.click(); this.uploadInput.current?.click();
}; };
private onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => { private onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
@ -249,12 +243,12 @@ class UploadButton extends React.Component<IUploadButtonProps> {
render() { render() {
const uploadInputStyle = { display: 'none' }; const uploadInputStyle = { display: 'none' };
return ( return <>
<AccessibleTooltipButton <CollapsibleButton
className="mx_MessageComposer_button mx_MessageComposer_upload" className="mx_MessageComposer_button mx_MessageComposer_upload"
onClick={this.onUploadClick} onClick={this.onUploadClick}
title={_t('Upload file')} title={_t('Attachment')}
> />
<input <input
ref={this.uploadInput} ref={this.uploadInput}
type="file" type="file"
@ -262,8 +256,7 @@ class UploadButton extends React.Component<IUploadButtonProps> {
multiple multiple
onChange={this.onUploadFileInputChange} onChange={this.onUploadFileInputChange}
/> />
</AccessibleTooltipButton> </>;
);
} }
} }
@ -275,13 +268,7 @@ function showStickersButton(props: IProps): ReactElement {
key="controls_stickers" key="controls_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers" className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)} onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={ title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
props.narrowMode
? _t("Send a sticker")
: props.isStickerPickerOpen
? _t("Hide Stickers")
: _t("Show Stickers")
}
/> />
: null : null
); );
@ -296,12 +283,12 @@ function voiceRecordingButton(props: IProps): ReactElement {
key="voice_message_send" key="voice_message_send"
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage" className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick} onClick={props.onRecordStartEndClick}
title={_t("Send voice message")} title={_t("Voice Message")}
/> />
); );
} }
function pollButton(props: IProps, room: Room): ReactElement { function pollButton(room: Room): ReactElement {
return <PollButton key="polls" room={room} />; return <PollButton key="polls" room={room} />;
} }
@ -311,6 +298,7 @@ interface IPollButtonProps {
class PollButton extends React.PureComponent<IPollButtonProps> { class PollButton extends React.PureComponent<IPollButtonProps> {
static contextType = OverflowMenuContext; static contextType = OverflowMenuContext;
public context!: React.ContextType<typeof OverflowMenuContext>;
private onCreateClick = () => { private onCreateClick = () => {
this.context?.(); // close overflow menu this.context?.(); // close overflow menu
@ -350,7 +338,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
<CollapsibleButton <CollapsibleButton
className="mx_MessageComposer_button mx_MessageComposer_poll" className="mx_MessageComposer_button mx_MessageComposer_poll"
onClick={this.onCreateClick} onClick={this.onCreateClick}
title={_t("Create poll")} title={_t("Poll")}
/> />
); );
} }

View file

@ -1694,13 +1694,12 @@
"You do not have permission to post to this room": "You do not have permission to post to this room", "You do not have permission to post to this room": "You do not have permission to post to this room",
"%(seconds)ss left": "%(seconds)ss left", "%(seconds)ss left": "%(seconds)ss left",
"Send voice message": "Send voice message", "Send voice message": "Send voice message",
"Add emoji": "Add emoji", "Emoji": "Emoji",
"Upload file": "Upload file", "Hide stickers": "Hide stickers",
"Send a sticker": "Send a sticker", "Sticker": "Sticker",
"Hide Stickers": "Hide Stickers", "Voice Message": "Voice Message",
"Show Stickers": "Show Stickers",
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.", "You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
"Create poll": "Create poll", "Poll": "Poll",
"Bold": "Bold", "Bold": "Bold",
"Italics": "Italics", "Italics": "Italics",
"Strikethrough": "Strikethrough", "Strikethrough": "Strikethrough",
@ -2095,7 +2094,6 @@
"Invalid file%(extra)s": "Invalid file%(extra)s", "Invalid file%(extra)s": "Invalid file%(extra)s",
"Error decrypting image": "Error decrypting image", "Error decrypting image": "Error decrypting image",
"Show image": "Show image", "Show image": "Show image",
"Sticker": "Sticker",
"Image": "Image", "Image": "Image",
"Join the conference at the top of this room": "Join the conference at the top of this room", "Join the conference at the top of this room": "Join the conference at the top of this room",
"Join the conference from the room information card on the right": "Join the conference from the room information card on the right", "Join the conference from the room information card on the right": "Join the conference from the room information card on the right",
@ -2153,10 +2151,11 @@
"Submit logs": "Submit logs", "Submit logs": "Submit logs",
"Can't load this message": "Can't load this message", "Can't load this message": "Can't load this message",
"toggle event": "toggle event", "toggle event": "toggle event",
"Share location": "Share location", "Location": "Location",
"We couldnt send your location": "We couldnt send your location", "We couldnt send your location": "We couldnt send your location",
"Element could not send your location. Please try again later.": "Element could not send your location. Please try again later.", "Element could not send your location. Please try again later.": "Element could not send your location. Please try again later.",
"Could not fetch location": "Could not fetch location", "Could not fetch location": "Could not fetch location",
"Share location": "Share location",
"Element was denied permission to fetch your location. Please allow location access in your browser settings.": "Element was denied permission to fetch your location. Please allow location access in your browser settings.", "Element was denied permission to fetch your location. Please allow location access in your browser settings.": "Element was denied permission to fetch your location. Please allow location access in your browser settings.",
"Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.",
"Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.",
@ -2295,6 +2294,7 @@
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.", "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.", "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
"Create poll": "Create poll",
"Create Poll": "Create Poll", "Create Poll": "Create Poll",
"Failed to post poll": "Failed to post poll", "Failed to post poll": "Failed to post poll",
"Sorry, the poll you tried to create was not posted.": "Sorry, the poll you tried to create was not posted.", "Sorry, the poll you tried to create was not posted.": "Sorry, the poll you tried to create was not posted.",
@ -3280,7 +3280,6 @@
"Commands": "Commands", "Commands": "Commands",
"Command Autocomplete": "Command Autocomplete", "Command Autocomplete": "Command Autocomplete",
"Community Autocomplete": "Community Autocomplete", "Community Autocomplete": "Community Autocomplete",
"Emoji": "Emoji",
"Emoji Autocomplete": "Emoji Autocomplete", "Emoji Autocomplete": "Emoji Autocomplete",
"Notify the whole room": "Notify the whole room", "Notify the whole room": "Notify the whole room",
"Room Notification": "Room Notification", "Room Notification": "Room Notification",

View file

@ -34,23 +34,45 @@ const MessageComposerButtons = TestUtils.wrapInMatrixClientContext(
); );
describe("MessageComposerButtons", () => { describe("MessageComposerButtons", () => {
it("Renders all buttons in wide mode", () => { it("Renders emoji and upload buttons in wide mode", () => {
const buttons = wrapAndRender( const buttons = wrapAndRender(
<MessageComposerButtons <MessageComposerButtons
isMenuOpen={false} isMenuOpen={false}
narrowMode={false} narrowMode={false}
showLocationButton={true} showLocationButton={true}
showStickersButton={true} showStickersButton={true}
toggleButtonMenu={() => {}}
/>, />,
); );
expect(buttonLabels(buttons)).toEqual([ expect(buttonLabels(buttons)).toEqual([
"Create poll", "Emoji",
"Upload file", "Attachment",
"Share location", "More options",
"Add emoji", ]);
"Show Stickers", });
"Send voice message",
it("Renders other buttons in menu in wide mode", () => {
const buttons = wrapAndRender(
<MessageComposerButtons
isMenuOpen={true}
narrowMode={false}
showLocationButton={true}
showStickersButton={true}
toggleButtonMenu={() => {}}
/>,
);
expect(buttonLabels(buttons)).toEqual([
"Emoji",
"Attachment",
"More options",
[
"Sticker",
"Voice Message",
"Poll",
"Location",
],
]); ]);
}); });
@ -61,11 +83,12 @@ describe("MessageComposerButtons", () => {
narrowMode={true} narrowMode={true}
showLocationButton={true} showLocationButton={true}
showStickersButton={true} showStickersButton={true}
toggleButtonMenu={() => {}}
/>, />,
); );
expect(buttonLabels(buttons)).toEqual([ expect(buttonLabels(buttons)).toEqual([
"Upload file", "Emoji",
"More options", "More options",
]); ]);
}); });
@ -82,13 +105,13 @@ describe("MessageComposerButtons", () => {
); );
expect(buttonLabels(buttons)).toEqual([ expect(buttonLabels(buttons)).toEqual([
"Upload file", "Emoji",
"More options", "More options",
[ [
"Create poll", "Attachment",
"Share location", "Sticker",
"Add emoji", "Poll",
"Send a sticker", "Location",
], ],
]); ]);
}); });