Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix accessibility and consistency of MessageComposerButtons #7679

Merged
merged 2 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions res/css/views/rooms/_MessageComposer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,14 @@ limitations under the License.
line-height: var(--size);
width: auto;
padding-left: var(--size);
border-radius: 100%;
margin-right: 6px;

&:last-child {
margin-right: auto;
&:not(.mx_CallContextMenu_item) {
border-radius: 50%;
margin-right: 6px;

&:last-child {
margin-right: auto;
}
}

&::before {
Expand Down Expand Up @@ -407,6 +410,7 @@ limitations under the License.
align-items: center;
max-width: unset;
width: 100%;
margin: 7px 7px 7px 16px; // space out the buttons
}

.mx_MessageComposer_Menu .mx_ContextualMenu {
Expand Down
8 changes: 7 additions & 1 deletion src/components/structures/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,13 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
return menuOptions;
};

type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
type ContextMenuTuple<T> = [
boolean,
RefObject<T>,
(ev?: SyntheticEvent) => void,
(ev?: SyntheticEvent) => void,
(val: boolean) => void,
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
const button = useRef<T>(null);
Expand Down
23 changes: 13 additions & 10 deletions src/components/views/location/LocationButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ReactElement, useContext } from 'react';
import React, { ReactElement, SyntheticEvent, useContext } from 'react';
import classNames from 'classnames';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { logger } from "matrix-js-sdk/src/logger";
Expand All @@ -23,39 +23,43 @@ import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";

import { _t } from '../../../languageHandler';
import LocationPicker from './LocationPicker';
import { CollapsibleButton, ICollapsibleButtonProps } from '../rooms/CollapsibleButton';
import { CollapsibleButton } from '../rooms/CollapsibleButton';
import ContextMenu, { aboveLeftOf, useContextMenu, AboveLeftOf } from "../../structures/ContextMenu";
import Modal from '../../../Modal';
import QuestionDialog from '../dialogs/QuestionDialog';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { OverflowMenuContext } from "../rooms/MessageComposerButtons";

interface IProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
interface IProps {
roomId: string;
sender: RoomMember;
menuPosition: AboveLeftOf;
narrowMode: boolean;
}

export const LocationButton: React.FC<IProps> = (
{ roomId, sender, menuPosition, narrowMode },
) => {
export const LocationButton: React.FC<IProps> = ({ roomId, sender, menuPosition }) => {
const overflowMenuCloser = useContext(OverflowMenuContext);
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
const matrixClient = useContext(MatrixClientContext);

const _onFinished = (ev?: SyntheticEvent) => {
closeMenu(ev);
overflowMenuCloser?.();
};

let contextMenu: ReactElement;
if (menuDisplayed) {
const position = menuPosition ?? aboveLeftOf(
button.current.getBoundingClientRect());

contextMenu = <ContextMenu
{...position}
onFinished={closeMenu}
onFinished={_onFinished}
managed={false}
>
<LocationPicker
sender={sender}
onChoose={shareLocation(matrixClient, roomId, openMenu)}
onFinished={closeMenu}
onFinished={_onFinished}
/>
</ContextMenu>;
}
Expand All @@ -74,7 +78,6 @@ export const LocationButton: React.FC<IProps> = (
<CollapsibleButton
className={className}
onClick={openMenu}
narrowMode={narrowMode}
title={_t("Share location")}
/>

Expand Down
28 changes: 17 additions & 11 deletions src/components/views/rooms/CollapsibleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { ComponentProps } from 'react';
import React, { ComponentProps, useContext } from 'react';
import classNames from 'classnames';

import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { MenuItem } from "../../structures/ContextMenu";
import { OverflowMenuContext } from './MessageComposerButtons';

export interface ICollapsibleButtonProps
extends ComponentProps<typeof AccessibleTooltipButton>
{
narrowMode: boolean;
interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
title: string;
}

export const CollapsibleButton = ({ narrowMode, title, ...props }: ICollapsibleButtonProps) => {
return <AccessibleTooltipButton
{...props}
title={narrowMode ? undefined : title}
label={narrowMode ? title : undefined}
/>;
export const CollapsibleButton = ({ title, className, ...props }: ICollapsibleButtonProps) => {
const inOverflowMenu = !!useContext(OverflowMenuContext);
if (inOverflowMenu) {
return <MenuItem
{...props}
className={classNames("mx_CallContextMenu_item", className)}
>
{ title }
</MenuItem>;
}

return <AccessibleTooltipButton {...props} title={title} className={className} />;
};

export default CollapsibleButton;
12 changes: 10 additions & 2 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
};

private setStickerPickerOpen = (isStickerPickerOpen: boolean) => {
this.setState({ isStickerPickerOpen });
this.setState({
isStickerPickerOpen,
isMenuOpen: false,
});
};

private toggleButtonMenu = (): void => {
Expand Down Expand Up @@ -453,7 +456,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
menuPosition={menuPosition}
narrowMode={this.state.narrowMode}
relation={this.props.relation}
onRecordStartEndClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
onRecordStartEndClick={() => {
this.voiceRecordingButton.current?.onRecordStartEndClick();
if (this.state.narrowMode) {
this.toggleButtonMenu();
}
}}
setStickerPickerOpen={this.setStickerPickerOpen}
showLocationButton={this.state.showLocationButton}
showStickersButton={this.state.showStickersButton}
Expand Down
55 changes: 23 additions & 32 deletions src/components/views/rooms/MessageComposerButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ limitations under the License.
import classNames from 'classnames';
import { IEventRelation } from "matrix-js-sdk/src/models/event";
import { M_POLL_START } from "matrix-events-sdk";
import React, { ReactElement, useContext } from 'react';
import React, { createContext, ReactElement, useContext } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixClient } from 'matrix-js-sdk/src/client';

import { _t } from '../../../languageHandler';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { CollapsibleButton, ICollapsibleButtonProps } from './CollapsibleButton';
import ContextMenu, { aboveLeftOf, AboveLeftOf, MenuItem, useContextMenu } from '../../structures/ContextMenu';
import { CollapsibleButton } from './CollapsibleButton';
import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from '../../structures/ContextMenu';
import dis from '../../../dispatcher/dispatcher';
import EmojiPicker from '../emojipicker/EmojiPicker';
import ErrorDialog from "../dialogs/ErrorDialog";
Expand Down Expand Up @@ -52,6 +52,9 @@ interface IProps {
toggleButtonMenu: () => void;
}

type OverflowMenuCloser = () => void;
export const OverflowMenuContext = createContext<OverflowMenuCloser | null>(null);

const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient: MatrixClient = useContext(MatrixClientContext);
const { room, roomId } = useContext(RoomContext);
Expand Down Expand Up @@ -107,23 +110,16 @@ function narrowMode(
className={moreOptionsClasses}
onClick={props.toggleButtonMenu}
title={_t("More options")}
tooltip={false}
/>
{ props.isMenuOpen && (
<ContextMenu
onFinished={props.toggleButtonMenu}
{...props.menuPosition}
wrapperClassName="mx_MessageComposer_Menu"
>
{ moreButtons.map((button, index) => (
<MenuItem
className="mx_CallContextMenu_item"
key={index}
onClick={props.toggleButtonMenu}
>
{ button }
</MenuItem>
)) }
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
{ moreButtons }
</OverflowMenuContext.Provider>
</ContextMenu>
) }
</>;
Expand All @@ -134,18 +130,16 @@ function emojiButton(props: IProps): ReactElement {
key="emoji_button"
addEmoji={props.addEmoji}
menuPosition={props.menuPosition}
narrowMode={props.narrowMode}
/>;
}

interface IEmojiButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
interface IEmojiButtonProps {
addEmoji: (unicode: string) => boolean;
menuPosition: AboveLeftOf;
}

const EmojiButton: React.FC<IEmojiButtonProps> = (
{ addEmoji, menuPosition, narrowMode },
) => {
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition }) => {
const overflowMenuCloser = useContext(OverflowMenuContext);
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();

let contextMenu: React.ReactElement | null = null;
Expand All @@ -156,7 +150,10 @@ const EmojiButton: React.FC<IEmojiButtonProps> = (

contextMenu = <ContextMenu
{...position}
onFinished={closeMenu}
onFinished={() => {
closeMenu();
overflowMenuCloser?.();
}}
managed={false}
>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
Expand All @@ -177,7 +174,6 @@ const EmojiButton: React.FC<IEmojiButtonProps> = (
<CollapsibleButton
className={className}
onClick={openMenu}
narrowMode={narrowMode}
title={_t("Add emoji")}
/>

Expand Down Expand Up @@ -274,19 +270,18 @@ class UploadButton extends React.Component<IUploadButtonProps> {
function showStickersButton(props: IProps): ReactElement {
return (
props.showStickersButton
? <AccessibleTooltipButton
? <CollapsibleButton
id='stickersButton'
key="controls_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={
props.narrowMode
? null
? _t("Send a sticker")
: props.isStickerPickerOpen
? _t("Hide Stickers")
: _t("Show Stickers")
}
label={props.narrowMode ? _t("Send a sticker") : null}
/>
: null
);
Expand All @@ -302,25 +297,23 @@ function voiceRecordingButton(props: IProps): ReactElement {
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
onClick={props.onRecordStartEndClick}
title={_t("Send voice message")}
narrowMode={props.narrowMode}
/>
);
}

function pollButton(props: IProps, room: Room): ReactElement {
return <PollButton
key="polls"
room={room}
narrowMode={props.narrowMode}
/>;
return <PollButton key="polls" room={room} />;
}

interface IPollButtonProps extends Pick<ICollapsibleButtonProps, "narrowMode"> {
interface IPollButtonProps {
room: Room;
}

class PollButton extends React.PureComponent<IPollButtonProps> {
static contextType = OverflowMenuContext;

private onCreateClick = () => {
this.context?.(); // close overflow menu
const canSend = this.props.room.currentState.maySendEvent(
M_POLL_START.name,
MatrixClientPeg.get().getUserId(),
Expand Down Expand Up @@ -357,7 +350,6 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
<CollapsibleButton
className="mx_MessageComposer_button mx_MessageComposer_poll"
onClick={this.onCreateClick}
narrowMode={this.props.narrowMode}
title={_t("Create poll")}
/>
);
Expand All @@ -377,7 +369,6 @@ function showLocationButton(
roomId={roomId}
sender={room.getMember(matrixClient.getUserId())}
menuPosition={props.menuPosition}
narrowMode={props.narrowMode}
/>
: null
);
Expand Down
27 changes: 14 additions & 13 deletions test/components/views/rooms/MessageComposerButtons-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe("MessageComposerButtons", () => {
narrowMode={true}
showLocationButton={true}
showStickersButton={true}
toggleButtonMenu={() => {}}
/>,
);

Expand Down Expand Up @@ -159,28 +160,28 @@ function createRoomState(room: Room): IRoomState {

function buttonLabels(buttons: ReactWrapper): any[] {
// Note: Depends on the fact that the mini buttons use aria-label
// and the labels under More options use label
// and the labels under More options use textContent
const mainButtons = (
buttons
.find('div')
.map((button: ReactWrapper) => button.prop("aria-label"))
.find('div.mx_MessageComposer_button[aria-label]')
.map((button: ReactWrapper) => button.prop("aria-label") as string)
.filter(x => x)
);

let extraButtons = (
const extraButtons = (
buttons
.find('div')
.map((button: ReactWrapper) => button.prop("label"))
.find('.mx_MessageComposer_Menu div.mx_AccessibleButton[role="menuitem"]')
.map((button: ReactWrapper) => button.text())
.filter(x => x)
);
if (extraButtons.length === 0) {
extraButtons = [];
} else {
extraButtons = [extraButtons];
}

return [
const list: any[] = [
...mainButtons,
...extraButtons,
];

if (extraButtons.length > 0) {
list.push(extraButtons);
}

return list;
}