Tooltip: Use AccessibleButton in reusable elements (#12461)

* Update reusable elements

* Update tests

* Make right as default tooltip placement

* Add tests
This commit is contained in:
Florian Duros 2024-04-29 19:19:05 +02:00 committed by GitHub
parent d5bf1022e9
commit 44e2a6d070
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 160 additions and 30 deletions

View file

@ -138,7 +138,7 @@ const AccessibleButton = forwardRef(function <T extends keyof JSX.IntrinsicEleme
triggerOnMouseDown, triggerOnMouseDown,
title, title,
caption, caption,
placement, placement = "right",
onTooltipOpenChange, onTooltipOpenChange,
...restProps ...restProps
}: Props<T>, }: Props<T>,

View file

@ -20,8 +20,7 @@ import classNames from "classnames";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { copyPlaintext } from "../../../utils/strings"; import { copyPlaintext } from "../../../utils/strings";
import { ButtonEvent } from "./AccessibleButton"; import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
interface IProps { interface IProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -53,11 +52,13 @@ const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true
return ( return (
<div className={combinedClassName}> <div className={combinedClassName}>
{children} {children}
<AccessibleTooltipButton <AccessibleButton
title={tooltip ?? _t("action|copy")} title={tooltip ?? _t("action|copy")}
onClick={onCopyClickInternal} onClick={onCopyClickInternal}
className="mx_CopyableText_copyButton" className="mx_CopyableText_copyButton"
onHideTooltip={onHideTooltip} onTooltipOpenChange={(open) => {
if (!open) onHideTooltip();
}}
/> />
</div> </div>
); );

View file

@ -21,7 +21,6 @@ import FocusLock from "react-focus-lock";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import MessageContextMenu from "../context_menus/MessageContextMenu"; import MessageContextMenu from "../context_menus/MessageContextMenu";
@ -38,6 +37,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { presentableTextForFile } from "../../../utils/FileUtils"; import { presentableTextForFile } from "../../../utils/FileUtils";
import AccessibleButton from "./AccessibleButton";
// Max scale to keep gaps around the image // Max scale to keep gaps around the image
const MAX_SCALE = 0.95; const MAX_SCALE = 0.95;
@ -513,14 +513,14 @@ export default class ImageView extends React.Component<IProps, IState> {
} }
const zoomOutButton = ( const zoomOutButton = (
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_zoomOut" className="mx_ImageView_button mx_ImageView_button_zoomOut"
title={_t("action|zoom_out")} title={_t("action|zoom_out")}
onClick={this.onZoomOutClick} onClick={this.onZoomOutClick}
/> />
); );
const zoomInButton = ( const zoomInButton = (
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_zoomIn" className="mx_ImageView_button mx_ImageView_button_zoomIn"
title={_t("action|zoom_in")} title={_t("action|zoom_in")}
onClick={this.onZoomInClick} onClick={this.onZoomInClick}
@ -553,23 +553,23 @@ export default class ImageView extends React.Component<IProps, IState> {
<div className="mx_ImageView_toolbar"> <div className="mx_ImageView_toolbar">
{zoomOutButton} {zoomOutButton}
{zoomInButton} {zoomInButton}
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW" className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("lightbox|rotate_left")} title={_t("lightbox|rotate_left")}
onClick={this.onRotateCounterClockwiseClick} onClick={this.onRotateCounterClockwiseClick}
/> />
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_rotateCW" className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("lightbox|rotate_right")} title={_t("lightbox|rotate_right")}
onClick={this.onRotateClockwiseClick} onClick={this.onRotateClockwiseClick}
/> />
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_download" className="mx_ImageView_button mx_ImageView_button_download"
title={_t("action|download")} title={_t("action|download")}
onClick={this.onDownloadClick} onClick={this.onDownloadClick}
/> />
{contextMenuButton} {contextMenuButton}
<AccessibleTooltipButton <AccessibleButton
className="mx_ImageView_button mx_ImageView_button_close" className="mx_ImageView_button mx_ImageView_button_close"
title={_t("action|close")} title={_t("action|close")}
onClick={this.props.onFinished} onClick={this.props.onFinished}

View file

@ -30,7 +30,6 @@ import { Signup } from "@matrix-org/analytics-events/types/typescript/Signup";
import PlatformPeg from "../../../PlatformPeg"; import PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import { mediaFromMxc } from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import { PosthogAnalytics } from "../../../PosthogAnalytics"; import { PosthogAnalytics } from "../../../PosthogAnalytics";
@ -131,9 +130,9 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
if (mini) { if (mini) {
// TODO fallback icon // TODO fallback icon
return ( return (
<AccessibleTooltipButton {...props} title={label} className={classes} onClick={onClick}> <AccessibleButton {...props} title={label} className={classes} onClick={onClick}>
{icon} {icon}
</AccessibleTooltipButton> </AccessibleButton>
); );
} }

View file

@ -18,7 +18,7 @@ limitations under the License.
import React from "react"; import React from "react";
import classNames from "classnames"; import classNames from "classnames";
import AccessibleTooltipButton from "./AccessibleTooltipButton"; import AccessibleButton from "./AccessibleButton";
interface IProps { interface IProps {
// Whether or not this toggle is in the 'on' position. // Whether or not this toggle is in the 'on' position.
@ -41,7 +41,7 @@ interface IProps {
} }
// Controlled Toggle Switch element, written with Accessibility in mind // Controlled Toggle Switch element, written with Accessibility in mind
export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.Element => { export default ({ checked, disabled = false, onChange, title, tooltip, ...props }: IProps): JSX.Element => {
const _onClick = (): void => { const _onClick = (): void => {
if (disabled) return; if (disabled) return;
onChange(!checked); onChange(!checked);
@ -54,15 +54,17 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps): JSX.
}); });
return ( return (
<AccessibleTooltipButton <AccessibleButton
{...props} {...props}
className={classes} className={classes}
onClick={_onClick} onClick={_onClick}
role="switch" role="switch"
aria-label={title}
aria-checked={checked} aria-checked={checked}
aria-disabled={disabled} aria-disabled={disabled}
title={tooltip}
> >
<div className="mx_ToggleSwitch_ball" /> <div className="mx_ToggleSwitch_ball" />
</AccessibleTooltipButton> </AccessibleButton>
); );
}; };

View file

@ -48,6 +48,7 @@ exports[`<BeaconListItem /> when a beacon is live and has locations renders beac
<div <div
aria-label="Copy" aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton" class="mx_AccessibleButton mx_CopyableText_copyButton"
data-state="closed"
role="button" role="button"
tabindex="0" tabindex="0"
/> />

View file

@ -82,6 +82,7 @@ exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
<div <div
aria-label="Copy" aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton" class="mx_AccessibleButton mx_CopyableText_copyButton"
data-state="closed"
role="button" role="button"
tabindex="0" tabindex="0"
/> />

View file

@ -19,6 +19,7 @@ exports[`<ShareLatestLocation /> renders share buttons when there is a location
<div <div
aria-label="Copy" aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton" class="mx_AccessibleButton mx_CopyableText_copyButton"
data-state="closed"
role="button" role="button"
tabindex="0" tabindex="0"
/> />

View file

@ -41,6 +41,7 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
<div <div
aria-label="Copy" aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton" class="mx_AccessibleButton mx_CopyableText_copyButton"
data-state="closed"
role="button" role="button"
tabindex="0" tabindex="0"
/> />

View file

@ -0,0 +1,29 @@
/*
*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
import React from "react";
import { render } from "@testing-library/react";
import ImageView from "../../../../src/components/views/elements/ImageView";
describe("<ImageView />", () => {
it("renders correctly", () => {
const { container } = render(<ImageView src="https://example.com/image.png" onFinished={jest.fn()} />);
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ImageView /> renders correctly 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-label="Image view"
class="mx_ImageView"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_ImageView_panel"
>
<div />
<div
class="mx_ImageView_toolbar"
>
<div
aria-describedby="floating-ui-2"
aria-label="Zoom out"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
data-state="open"
role="button"
tabindex="0"
/>
<div
aria-label="Zoom in"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomIn"
data-state="closed"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Left"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCCW"
data-state="closed"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Right"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCW"
data-state="closed"
role="button"
tabindex="0"
/>
<div
aria-label="Download"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_download"
data-state="closed"
role="button"
tabindex="0"
/>
<div
aria-label="Close"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_close"
data-state="closed"
role="button"
tabindex="0"
/>
</div>
</div>
<div
class="mx_ImageView_image_wrapper"
>
<img
class="mx_ImageView_image "
draggable="true"
src="https://example.com/image.png"
style="transform: translateX(0px)
translateY(0px)
scale(0)
rotate(0deg); cursor: zoom-out;"
/>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -45,6 +45,7 @@ exports[`AdvancedRoomSettingsTab should render as expected 1`] = `
<div <div
aria-label="Copy" aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton" class="mx_AccessibleButton mx_CopyableText_copyButton"
data-state="closed"
role="button" role="button"
tabindex="0" tabindex="0"
/> />

View file

@ -15,7 +15,8 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { fireEvent, render, screen } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab"; import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
import SettingsStore from "../../../../../../src/settings/SettingsStore"; import SettingsStore from "../../../../../../src/settings/SettingsStore";
@ -113,12 +114,14 @@ describe("<LabsUserSettingsTab />", () => {
expect(toggle.getAttribute("aria-checked")).toEqual("true"); expect(toggle.getAttribute("aria-checked")).toEqual("true");
// Hover over the toggle to make it show the tooltip // Hover over the toggle to make it show the tooltip
fireEvent.mouseOver(toggle); await userEvent.hover(toggle);
const tooltip = rendered.getByRole("tooltip"); await waitFor(() => {
expect(tooltip).toHaveTextContent( const tooltip = screen.getByRole("tooltip");
"Once enabled, Rust cryptography can only be disabled by logging out and in again", expect(tooltip).toHaveTextContent(
); "Once enabled, Rust cryptography can only be disabled by logging out and in again",
);
});
}); });
}); });
@ -150,12 +153,14 @@ describe("<LabsUserSettingsTab />", () => {
expect(toggle.getAttribute("aria-checked")).toEqual("true"); expect(toggle.getAttribute("aria-checked")).toEqual("true");
// Hover over the toggle to make it show the tooltip // Hover over the toggle to make it show the tooltip
fireEvent.mouseOver(toggle); await userEvent.hover(toggle);
const tooltip = rendered.getByRole("tooltip"); await waitFor(() => {
expect(tooltip).toHaveTextContent( const tooltip = rendered.getByRole("tooltip");
"Rust cryptography cannot be disabled on this deployment of BrandedClient", expect(tooltip).toHaveTextContent(
); "Rust cryptography cannot be disabled on this deployment of BrandedClient",
);
});
}); });
}); });
}); });

View file

@ -289,6 +289,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true" aria-disabled="true"
aria-label="Send read receipts" aria-label="Send read receipts"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on" class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
data-state="closed"
id="mx_SettingsFlag_GQvdMWe954DV" id="mx_SettingsFlag_GQvdMWe954DV"
role="switch" role="switch"
tabindex="0" tabindex="0"