Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to activate GIFs via the keyboard for a11y #28611

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Changes from all commits
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
58 changes: 34 additions & 24 deletions src/components/views/messages/MImageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface IState {
naturalHeight: number;
};
hover: boolean;
focus: boolean;
showImage: boolean;
placeholder: Placeholder;
}
Expand All @@ -71,6 +72,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imgError: false,
imgLoaded: false,
hover: false,
focus: false,
showImage: SettingsStore.getValue("showImages"),
placeholder: Placeholder.NoImage,
};
Expand Down Expand Up @@ -120,30 +122,29 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}
};

protected onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => {
this.setState({ hover: true });

if (
private get shouldAutoplay(): boolean {
return !(
!this.state.contentUrl ||
!this.state.showImage ||
!this.state.isAnimated ||
SettingsStore.getValue("autoplayGifs")
) {
return;
}
const imgElement = e.currentTarget;
imgElement.src = this.state.contentUrl;
);
}

protected onImageEnter = (): void => {
this.setState({ hover: true });
};

protected onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => {
protected onImageLeave = (): void => {
this.setState({ hover: false });
};

const url = this.state.thumbUrl ?? this.state.contentUrl;
if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) {
return;
}
const imgElement = e.currentTarget;
imgElement.src = url;
private onFocus = (): void => {
this.setState({ focus: true });
};

private onBlur = (): void => {
this.setState({ focus: false });
};

private reconnectedListener = createReconnectedListener((): void => {
Expand Down Expand Up @@ -470,14 +471,20 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {

let showPlaceholder = Boolean(placeholder);

const hoverOrFocus = this.state.hover || this.state.focus;
if (thumbUrl && !this.state.imgError) {
let url = thumbUrl;
if (hoverOrFocus && this.shouldAutoplay) {
url = this.state.contentUrl!;
}

// Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size
img = (
<img
className="mx_MImageBody_thumbnail"
src={thumbUrl}
src={url}
ref={this.image}
alt={content.body}
onError={this.onImageError}
Expand All @@ -493,13 +500,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
}

if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) {
if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !hoverOrFocus) {
// XXX: Arguably we may want a different label when the animated image is WEBP and not GIF
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
}

let banner: ReactNode | undefined;
if (this.state.showImage && this.state.hover) {
if (this.state.showImage && hoverOrFocus) {
banner = this.getBanner(content);
}

Expand Down Expand Up @@ -568,7 +575,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode {
if (contentUrl) {
return (
<a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}>
<a
href={contentUrl}
target={this.props.forExport ? "_blank" : undefined}
onClick={this.onClick}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
{children}
</a>
);
Expand Down Expand Up @@ -657,17 +670,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
}

interface PlaceholderIProps {
hover?: boolean;
maxWidth?: number;
}

export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
public render(): React.ReactNode {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = "mx_HiddenImagePlaceholder";
if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover";
return (
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className="mx_HiddenImagePlaceholder_button">
<span className="mx_HiddenImagePlaceholder_eye" />
<span>{_t("timeline|m.image|show_image")}</span>
Expand Down
Loading