[Accessibility] Composer: Support attaching media (#1752)

This commit is contained in:
Alexander Zinchuk 2022-03-11 13:51:11 +01:00
parent 1aac51057d
commit bd507a328f
11 changed files with 116 additions and 79 deletions

View File

@ -32,7 +32,6 @@ export { default as MobileSearch } from '../components/middle/MobileSearch';
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
export { default as PollModal } from '../components/middle/composer/PollModal';
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
export { default as AttachMenu } from '../components/middle/composer/AttachMenu';
export { default as BotCommandTooltip } from '../components/middle/composer/BotCommandTooltip';
export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu';
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip';

View File

@ -1,15 +0,0 @@
import React, { FC, memo } from '../../../lib/teact/teact';
import { OwnProps } from './AttachMenu';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const AttachMenuAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const AttachMenu = useModuleLoader(Bundles.Extra, 'AttachMenu', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return AttachMenu ? <AttachMenu {...props} /> : undefined;
};
export default memo(AttachMenuAsync);

View File

@ -1,18 +1,31 @@
.AttachMenu {
position: relative;
.is-pointer-env & {
> .backdrop {
position: absolute;
top: -1rem;
left: auto;
right: 0;
width: 3.5rem;
&--button {
&:focus {
color: var(--color-primary);
}
}
&--menu {
position: relative;
top: -3.5rem;
.media-disabled > button {
white-space: normal;
@media (max-width: 600px) {
top: -2.875rem;
}
.is-pointer-env & {
> .backdrop {
position: absolute;
top: -1rem;
left: auto;
right: 0;
width: 3.5rem;
}
}
.media-disabled > button {
white-space: normal;
}
}
}

View File

@ -1,29 +1,39 @@
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
import React, {
FC, memo, useCallback, useEffect,
} from '../../../lib/teact/teact';
import { CONTENT_TYPES_WITH_PREVIEW } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import './AttachMenu.scss';
export type OwnProps = {
isOpen: boolean;
isButtonVisible: boolean;
canAttachMedia: boolean;
canAttachPolls: boolean;
onFileSelect: (files: File[], isQuick: boolean) => void;
onPollCreate: () => void;
onClose: () => void;
};
const AttachMenu: FC<OwnProps> = ({
isOpen, canAttachMedia, canAttachPolls, onFileSelect, onPollCreate, onClose,
isButtonVisible, canAttachMedia, canAttachPolls, onFileSelect, onPollCreate,
}) => {
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isAttachMenuOpen, closeAttachMenu);
useEffect(() => {
if (isAttachMenuOpen) {
markMouseInside();
}
}, [isAttachMenuOpen, markMouseInside]);
const handleFileSelect = useCallback((e: Event, isQuick: boolean) => {
const { files } = e.target as HTMLInputElement;
@ -46,38 +56,58 @@ const AttachMenu: FC<OwnProps> = ({
const lang = useLang();
if (!isButtonVisible) {
return;
}
return (
<Menu
isOpen={isOpen}
autoClose
positionX="right"
positionY="bottom"
onClose={onClose}
className="AttachMenu fluid"
onCloseAnimationEnd={onClose}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV}
>
{/*
<div className="AttachMenu">
<ResponsiveHoverButton
id="attach-menu-button"
className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
round
color="translucent"
onActivate={openAttachMenu}
ariaLabel="Add an attachment"
ariaControls="attach-menu-controls"
hasPopup
>
<i className="icon-attach" />
</ResponsiveHoverButton>
<Menu
id="attach-menu-controls"
isOpen={isAttachMenuOpen}
autoClose
positionX="right"
positionY="bottom"
onClose={closeAttachMenu}
className="AttachMenu--menu fluid"
onCloseAnimationEnd={closeAttachMenu}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
noCloseOnBackdrop={!IS_TOUCH_ENV}
ariaLabelledBy="attach-menu-button"
>
{/*
** Using ternary operator here causes some attributes from first clause
** transferring to the fragment content in the second clause
*/}
{!canAttachMedia && (
<MenuItem className="media-disabled" disabled>Posting media content is not allowed in this group.</MenuItem>
)}
{canAttachMedia && (
<>
<MenuItem icon="photo" onClick={handleQuickSelect}>
{lang('AttachmentMenu.PhotoOrVideo')}
</MenuItem>
<MenuItem icon="document" onClick={handleDocumentSelect}>{lang('AttachDocument')}</MenuItem>
</>
)}
{canAttachPolls && (
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
)}
</Menu>
{!canAttachMedia && (
<MenuItem className="media-disabled" disabled>Posting media content is not allowed in this group.</MenuItem>
)}
{canAttachMedia && (
<>
<MenuItem icon="photo" onClick={handleQuickSelect}>
{lang('AttachmentMenu.PhotoOrVideo')}
</MenuItem>
<MenuItem icon="document" onClick={handleDocumentSelect}>{lang('AttachDocument')}</MenuItem>
</>
)}
{canAttachPolls && (
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
)}
</Menu>
</div>
);
};

View File

@ -250,6 +250,7 @@
margin-right: -0.5rem;
}
> .AttachMenu > .Button,
> .Button {
flex-shrink: 0;
background: none !important;
@ -263,7 +264,7 @@
color: var(--color-composer-button);
}
+ .Button {
+ .Button, + .AttachMenu {
margin-left: -1rem;
}
@ -271,7 +272,7 @@
width: 2.875rem;
height: 2.875rem;
+ .Button {
+ .Button, + .AttachMenu {
margin-left: -0.6875rem;
}
}

View File

@ -84,8 +84,8 @@ import Button from '../../ui/Button';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
import Spinner from '../../ui/Spinner';
import CalendarModal from '../../common/CalendarModal.async';
import AttachMenu from './AttachMenu';
import Avatar from '../../common/Avatar';
import AttachMenu from './AttachMenu.async';
import SymbolMenu from './SymbolMenu.async';
import InlineBotTooltip from './InlineBotTooltip.async';
import MentionTooltip from './MentionTooltip.async';
@ -289,7 +289,6 @@ const Composer: FC<OwnProps & StateProps> = ({
const [isBotKeyboardOpen, openBotKeyboard, closeBotKeyboard] = useFlag();
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
@ -1014,17 +1013,6 @@ const Composer: FC<OwnProps & StateProps> = ({
<i className="icon-bot-command" />
</ResponsiveHoverButton>
)}
{!activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={isAttachMenuOpen ? 'activated' : ''}
round
color="translucent"
onActivate={openAttachMenu}
ariaLabel="Add an attachment"
>
<i className="icon-attach" />
</ResponsiveHoverButton>
)}
{activeVoiceRecording && currentRecordTime && (
<span className="recording-state">
{formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)}
@ -1044,12 +1032,11 @@ const Composer: FC<OwnProps & StateProps> = ({
addRecentEmoji={addRecentEmoji}
/>
<AttachMenu
isOpen={isAttachMenuOpen}
isButtonVisible={!activeVoiceRecording && !editingMessage}
canAttachMedia={canAttachMedia}
canAttachPolls={canAttachPolls}
onFileSelect={handleFileSelect}
onPollCreate={openPollModal}
onClose={closeAttachMenu}
/>
{botKeyboardMessageId && (
<BotKeyboardMenu

View File

@ -402,6 +402,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
onMouseDown={handleMouseDown}
onContextMenu={IS_ANDROID ? stopEvent : undefined}
onTouchCancel={IS_ANDROID ? processSelection : undefined}
aria-label={placeholder}
/>
<div ref={cloneRef} className={buildClassName(className, 'clone')} dir="auto" />
{!forcedPlaceholder && <span className="placeholder-text" dir="auto">{placeholder}</span>}

View File

@ -20,6 +20,7 @@ export type OwnProps = {
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black' | 'dark'
);
backgroundImage?: string;
id?: string;
className?: string;
round?: boolean;
pill?: boolean;
@ -27,6 +28,8 @@ export type OwnProps = {
isText?: boolean;
isLoading?: boolean;
ariaLabel?: string;
ariaControls?: string;
hasPopup?: boolean;
href?: string;
download?: string;
disabled?: boolean;
@ -49,6 +52,7 @@ const CLICKED_TIMEOUT = 400;
const Button: FC<OwnProps> = ({
ref,
type = 'button',
id,
onClick,
onContextMenu,
onMouseDown,
@ -66,6 +70,8 @@ const Button: FC<OwnProps> = ({
isText,
isLoading,
ariaLabel,
ariaControls,
hasPopup,
href,
download,
disabled,
@ -122,12 +128,16 @@ const Button: FC<OwnProps> = ({
return (
<a
ref={elementRef as RefObject<HTMLAnchorElement>}
id={id}
className={fullClassName}
href={href}
title={ariaLabel}
download={download}
tabIndex={tabIndex}
dir={isRtl ? 'rtl' : undefined}
aria-label={ariaLabel}
aria-controls={ariaControls}
aria-haspopup={hasPopup}
>
{children}
{!disabled && ripple && (
@ -141,6 +151,7 @@ const Button: FC<OwnProps> = ({
// eslint-disable-next-line react/button-has-type
<button
ref={elementRef as RefObject<HTMLButtonElement>}
id={id}
type={type}
className={fullClassName}
onClick={handleClick}
@ -150,6 +161,8 @@ const Button: FC<OwnProps> = ({
onMouseLeave={onMouseLeave && !disabled ? onMouseLeave : undefined}
onFocus={onFocus && !disabled ? onFocus : undefined}
aria-label={ariaLabel}
aria-controls={ariaControls}
aria-haspopup={hasPopup}
title={ariaLabel}
tabIndex={tabIndex}
dir={isRtl ? 'rtl' : undefined}

View File

@ -82,6 +82,7 @@ const InputText: FC<OwnProps> = ({
onKeyDown={onKeyDown}
onBlur={onBlur}
onPaste={onPaste}
aria-label={labelText}
/>
{labelText && (
<label htmlFor={id}>{labelText}</label>

View File

@ -18,9 +18,11 @@ type OwnProps = {
ref?: RefObject<HTMLDivElement>;
containerRef?: RefObject<HTMLElement>;
isOpen: boolean;
id?: string;
className?: string;
style?: string;
bubbleStyle?: string;
ariaLabelledBy?: string;
transformOriginX?: number;
transformOriginY?: number;
positionX?: 'left' | 'right';
@ -44,9 +46,11 @@ const Menu: FC<OwnProps> = ({
ref,
containerRef,
isOpen,
id,
className,
style,
bubbleStyle,
ariaLabelledBy,
children,
transformOriginX,
transformOriginY,
@ -113,16 +117,19 @@ const Menu: FC<OwnProps> = ({
return (
<div
id={id}
className={buildClassName(
'Menu no-selection',
!noCompact && IS_COMPACT_MENU && 'compact',
!IS_BACKDROP_BLUR_SUPPORTED && 'no-blur',
className,
)}
style={style}
aria-labelledby={ariaLabelledBy}
role={ariaLabelledBy ? 'menu' : undefined}
onKeyDown={isOpen ? handleKeyDown : undefined}
onMouseEnter={onMouseEnter}
onMouseLeave={isOpen ? onMouseLeave : undefined}
style={style}
>
{isOpen && (
// This only prevents click events triggering on underlying elements

View File

@ -52,7 +52,7 @@ const ResponsiveHoverButton: FC<OwnProps> = ({ onActivate, ...buttonProps }) =>
{...buttonProps}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
onClick={IS_TOUCH_ENV ? onActivate : undefined}
onClick={onActivate}
/>
);
};