mirror of
https://github.com/mastodon/mastodon.git
synced 2025-02-05 17:01:45 +01:00
Add ability to dismiss alt text badge by tapping it in web UI
This commit is contained in:
parent
2df86d6413
commit
9e28b33efc
55
app/javascript/hooks/useSelectableClick.ts
Normal file
55
app/javascript/hooks/useSelectableClick.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
type Position = [number, number];
|
||||||
|
|
||||||
|
export const useSelectableClick = (
|
||||||
|
onClick: React.MouseEventHandler,
|
||||||
|
maxDelta = 5,
|
||||||
|
) => {
|
||||||
|
const clickPositionRef = useRef<Position | null>(null);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
clickPositionRef.current = [e.clientX, e.clientY];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
if (!clickPositionRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [startX, startY] = clickPositionRef.current;
|
||||||
|
const [deltaX, deltaY] = [
|
||||||
|
Math.abs(e.clientX - startX),
|
||||||
|
Math.abs(e.clientY - startY),
|
||||||
|
];
|
||||||
|
|
||||||
|
let element: EventTarget | null = e.target;
|
||||||
|
|
||||||
|
while (element && element instanceof HTMLElement) {
|
||||||
|
if (
|
||||||
|
element.localName === 'button' ||
|
||||||
|
element.localName === 'a' ||
|
||||||
|
element.localName === 'label'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = element.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
deltaX + deltaY < maxDelta &&
|
||||||
|
(e.button === 0 || e.button === 1) &&
|
||||||
|
e.detail >= 1
|
||||||
|
) {
|
||||||
|
onClick(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickPositionRef.current = null;
|
||||||
|
},
|
||||||
|
[maxDelta, onClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [handleMouseDown, handleMouseUp] as const;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@ -8,12 +8,15 @@ import type {
|
|||||||
UsePopperOptions,
|
UsePopperOptions,
|
||||||
} from 'react-overlays/esm/usePopper';
|
} from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
|
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
||||||
|
|
||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
export const AltTextBadge: React.FC<{
|
export const AltTextBadge: React.FC<{
|
||||||
description: string;
|
description: string;
|
||||||
}> = ({ description }) => {
|
}> = ({ description }) => {
|
||||||
|
const accessibilityId = useId();
|
||||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@ -25,12 +28,16 @@ export const AltTextBadge: React.FC<{
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
|
||||||
|
const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
className='media-gallery__alt__label'
|
className='media-gallery__alt__label'
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={accessibilityId}
|
||||||
>
|
>
|
||||||
ALT
|
ALT
|
||||||
</button>
|
</button>
|
||||||
@ -47,9 +54,12 @@ export const AltTextBadge: React.FC<{
|
|||||||
>
|
>
|
||||||
{({ props }) => (
|
{({ props }) => (
|
||||||
<div {...props} className='hover-card-controller'>
|
<div {...props} className='hover-card-controller'>
|
||||||
<div
|
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
className='media-gallery__alt__popover dropdown-animation'
|
className='media-gallery__alt__popover dropdown-animation'
|
||||||
role='tooltip'
|
role='tooltip'
|
||||||
|
id={accessibilityId}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
>
|
>
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useCallback } from 'react';
|
import { useState, useRef, useCallback, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ export const DomainPill: React.FC<{
|
|||||||
username: string;
|
username: string;
|
||||||
isSelf: boolean;
|
isSelf: boolean;
|
||||||
}> = ({ domain, username, isSelf }) => {
|
}> = ({ domain, username, isSelf }) => {
|
||||||
|
const accessibilityId = useId();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const triggerRef = useRef(null);
|
const triggerRef = useRef(null);
|
||||||
@ -34,6 +35,8 @@ export const DomainPill: React.FC<{
|
|||||||
className={classNames('account__domain-pill', { active: open })}
|
className={classNames('account__domain-pill', { active: open })}
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={accessibilityId}
|
||||||
>
|
>
|
||||||
{domain}
|
{domain}
|
||||||
</button>
|
</button>
|
||||||
@ -48,6 +51,8 @@ export const DomainPill: React.FC<{
|
|||||||
{({ props }) => (
|
{({ props }) => (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
|
role='tooltip'
|
||||||
|
id={accessibilityId}
|
||||||
className='account__domain-pill__popout dropdown-animation'
|
className='account__domain-pill__popout dropdown-animation'
|
||||||
>
|
>
|
||||||
<div className='account__domain-pill__popout__header'>
|
<div className='account__domain-pill__popout__header'>
|
||||||
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import { useSelectableClick } from '@/hooks/useSelectableClick';
|
||||||
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ export const InfoButton: React.FC = () => {
|
|||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
}, [open, setOpen]);
|
}, [open, setOpen]);
|
||||||
|
|
||||||
|
const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClick);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@ -46,10 +49,13 @@ export const InfoButton: React.FC = () => {
|
|||||||
target={triggerRef}
|
target={triggerRef}
|
||||||
>
|
>
|
||||||
{({ props }) => (
|
{({ props }) => (
|
||||||
<div
|
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
{...props}
|
{...props}
|
||||||
className='dialog-modal__popout prose dropdown-animation'
|
className='dialog-modal__popout prose dropdown-animation'
|
||||||
|
role='tooltip'
|
||||||
id={accessibilityId}
|
id={accessibilityId}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='info_button.what_is_alt_text'
|
id='info_button.what_is_alt_text'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user