Add disabled button state to rich text editor (#9930)

* add disabled css state
* conditionally apply disabled css state
* hides disabled tooltips
pull/28217/head
alunturner 2023-01-19 09:24:29 +00:00 committed by GitHub
parent b47588fc5c
commit 8a2e386531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 15 deletions

View File

@ -50,6 +50,12 @@ limitations under the License.
} }
} }
.mx_FormattingButtons_disabled {
.mx_FormattingButtons_Icon {
color: $quinary-content;
}
}
.mx_FormattingButtons_Icon { .mx_FormattingButtons_Icon {
--size: 16px; --size: 16px;
height: var(--size); height: var(--size);

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React, { MouseEventHandler, ReactNode } from "react"; import React, { MouseEventHandler, ReactNode } from "react";
import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg"; import { FormattingFunctions, AllActionStates, ActionState } from "@matrix-org/matrix-wysiwyg";
import classNames from "classnames"; import classNames from "classnames";
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg"; import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
@ -53,21 +53,23 @@ function Tooltip({ label, keyCombo }: TooltipProps): JSX.Element {
interface ButtonProps extends TooltipProps { interface ButtonProps extends TooltipProps {
icon: ReactNode; icon: ReactNode;
isActive: boolean; actionState: ActionState;
onClick: MouseEventHandler<HTMLButtonElement>; onClick: MouseEventHandler<HTMLButtonElement>;
} }
function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps): JSX.Element { function Button({ label, keyCombo, onClick, actionState, icon }: ButtonProps): JSX.Element {
return ( return (
<AccessibleTooltipButton <AccessibleTooltipButton
element="button" element="button"
onClick={onClick as (e: ButtonEvent) => void} onClick={onClick as (e: ButtonEvent) => void}
title={label} title={label}
className={classNames("mx_FormattingButtons_Button", { className={classNames("mx_FormattingButtons_Button", {
mx_FormattingButtons_active: isActive, mx_FormattingButtons_active: actionState === "reversed",
mx_FormattingButtons_Button_hover: !isActive, mx_FormattingButtons_Button_hover: actionState === "enabled",
mx_FormattingButtons_disabled: actionState === "disabled",
})} })}
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />} tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
forceHide={actionState === "disabled"}
alignment={Alignment.Top} alignment={Alignment.Top}
> >
{icon} {icon}
@ -85,53 +87,53 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
return ( return (
<div className="mx_FormattingButtons"> <div className="mx_FormattingButtons">
<Button <Button
isActive={actionStates.bold === "reversed"} actionState={actionStates.bold}
label={_td("Bold")} label={_td("Bold")}
keyCombo={{ ctrlOrCmdKey: true, key: "b" }} keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
onClick={() => composer.bold()} onClick={() => composer.bold()}
icon={<BoldIcon className="mx_FormattingButtons_Icon" />} icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.italic === "reversed"} actionState={actionStates.italic}
label={_td("Italic")} label={_td("Italic")}
keyCombo={{ ctrlOrCmdKey: true, key: "i" }} keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
onClick={() => composer.italic()} onClick={() => composer.italic()}
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />} icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.underline === "reversed"} actionState={actionStates.underline}
label={_td("Underline")} label={_td("Underline")}
keyCombo={{ ctrlOrCmdKey: true, key: "u" }} keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
onClick={() => composer.underline()} onClick={() => composer.underline()}
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />} icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.strikeThrough === "reversed"} actionState={actionStates.strikeThrough}
label={_td("Strikethrough")} label={_td("Strikethrough")}
onClick={() => composer.strikeThrough()} onClick={() => composer.strikeThrough()}
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />} icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.unorderedList === "reversed"} actionState={actionStates.unorderedList}
label={_td("Bulleted list")} label={_td("Bulleted list")}
onClick={() => composer.unorderedList()} onClick={() => composer.unorderedList()}
icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />} icon={<BulletedListIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.orderedList === "reversed"} actionState={actionStates.orderedList}
label={_td("Numbered list")} label={_td("Numbered list")}
onClick={() => composer.orderedList()} onClick={() => composer.orderedList()}
icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />} icon={<NumberedListIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.inlineCode === "reversed"} actionState={actionStates.inlineCode}
label={_td("Code")} label={_td("Code")}
keyCombo={{ ctrlOrCmdKey: true, key: "e" }} keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
onClick={() => composer.inlineCode()} onClick={() => composer.inlineCode()}
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />} icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
/> />
<Button <Button
isActive={actionStates.link === "reversed"} actionState={actionStates.link}
label={_td("Link")} label={_td("Link")}
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")} onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
icon={<LinkIcon className="mx_FormattingButtons_Icon" />} icon={<LinkIcon className="mx_FormattingButtons_Icon" />}

View File

@ -62,6 +62,7 @@ const renderComponent = (props = {}) => {
const classes = { const classes = {
active: "mx_FormattingButtons_active", active: "mx_FormattingButtons_active",
hover: "mx_FormattingButtons_Button_hover", hover: "mx_FormattingButtons_Button_hover",
disabled: "mx_FormattingButtons_disabled",
}; };
describe("FormattingButtons", () => { describe("FormattingButtons", () => {
@ -87,6 +88,16 @@ describe("FormattingButtons", () => {
}); });
}); });
it("Each button should have disabled class when disabled", () => {
const disabledActionStates = createActionStates("disabled");
renderComponent({ actionStates: disabledActionStates });
Object.values(testCases).forEach((testCase) => {
const { label } = testCase;
expect(screen.getByLabelText(label)).toHaveClass(classes.disabled);
});
});
it("Should call wysiwyg function on button click", async () => { it("Should call wysiwyg function on button click", async () => {
renderComponent(); renderComponent();
@ -98,14 +109,26 @@ describe("FormattingButtons", () => {
} }
}); });
it("Each button should display the tooltip on mouse over", async () => { it("Each button should display the tooltip on mouse over when not disabled", async () => {
renderComponent(); renderComponent();
for (const testCase of Object.values(testCases)) { for (const testCase of Object.values(testCases)) {
const { label } = testCase; const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label)); await userEvent.hover(screen.getByLabelText(label));
expect(await screen.findByText(label)).toBeTruthy(); expect(screen.getByText(label)).toBeInTheDocument();
}
});
it("Each button should not display the tooltip on mouse over when disabled", async () => {
const disabledActionStates = createActionStates("disabled");
renderComponent({ actionStates: disabledActionStates });
for (const testCase of Object.values(testCases)) {
const { label } = testCase;
await userEvent.hover(screen.getByLabelText(label));
expect(screen.queryByText(label)).not.toBeInTheDocument();
} }
}); });