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

* Provide a way to activate GIFs via the keyboard for a11y

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove dead code

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28585/merge
Michael Telatynski 2024-12-02 13:13:18 +00:00 committed by GitHub
parent 84709df3c9
commit 2c3e01a31c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 34 additions and 24 deletions

View File

@ -51,6 +51,7 @@ interface IState {
naturalHeight: number; naturalHeight: number;
}; };
hover: boolean; hover: boolean;
focus: boolean;
showImage: boolean; showImage: boolean;
placeholder: Placeholder; placeholder: Placeholder;
} }
@ -71,6 +72,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imgError: false, imgError: false,
imgLoaded: false, imgLoaded: false,
hover: false, hover: false,
focus: false,
showImage: SettingsStore.getValue("showImages"), showImage: SettingsStore.getValue("showImages"),
placeholder: Placeholder.NoImage, placeholder: Placeholder.NoImage,
}; };
@ -120,30 +122,29 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
}; };
protected onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => { private get shouldAutoplay(): boolean {
this.setState({ hover: true }); return !(
if (
!this.state.contentUrl || !this.state.contentUrl ||
!this.state.showImage || !this.state.showImage ||
!this.state.isAnimated || !this.state.isAnimated ||
SettingsStore.getValue("autoplayGifs") SettingsStore.getValue("autoplayGifs")
) { );
return; }
}
const imgElement = e.currentTarget; protected onImageEnter = (): void => {
imgElement.src = this.state.contentUrl; this.setState({ hover: true });
}; };
protected onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => { protected onImageLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
};
const url = this.state.thumbUrl ?? this.state.contentUrl; private onFocus = (): void => {
if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { this.setState({ focus: true });
return; };
}
const imgElement = e.currentTarget; private onBlur = (): void => {
imgElement.src = url; this.setState({ focus: false });
}; };
private reconnectedListener = createReconnectedListener((): void => { private reconnectedListener = createReconnectedListener((): void => {
@ -470,14 +471,20 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let showPlaceholder = Boolean(placeholder); let showPlaceholder = Boolean(placeholder);
const hoverOrFocus = this.state.hover || this.state.focus;
if (thumbUrl && !this.state.imgError) { 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 // Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline // which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = ( img = (
<img <img
className="mx_MImageBody_thumbnail" className="mx_MImageBody_thumbnail"
src={thumbUrl} src={url}
ref={this.image} ref={this.image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
@ -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. 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 // 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>; gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
} }
let banner: ReactNode | undefined; let banner: ReactNode | undefined;
if (this.state.showImage && this.state.hover) { if (this.state.showImage && hoverOrFocus) {
banner = this.getBanner(content); banner = this.getBanner(content);
} }
@ -568,7 +575,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode {
if (contentUrl) { if (contentUrl) {
return ( 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} {children}
</a> </a>
); );
@ -657,17 +670,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
interface PlaceholderIProps { interface PlaceholderIProps {
hover?: boolean;
maxWidth?: number; maxWidth?: number;
} }
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> { export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = "mx_HiddenImagePlaceholder";
if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover";
return ( 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"> <div className="mx_HiddenImagePlaceholder_button">
<span className="mx_HiddenImagePlaceholder_eye" /> <span className="mx_HiddenImagePlaceholder_eye" />
<span>{_t("timeline|m.image|show_image")}</span> <span>{_t("timeline|m.image|show_image")}</span>