[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 AttachmentModal } from '../components/middle/composer/AttachmentModal';
export { default as PollModal } from '../components/middle/composer/PollModal'; export { default as PollModal } from '../components/middle/composer/PollModal';
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; 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 BotCommandTooltip } from '../components/middle/composer/BotCommandTooltip';
export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu'; export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu';
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip'; 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 { .AttachMenu {
position: relative; &--button {
&:focus {
.is-pointer-env & { color: var(--color-primary);
> .backdrop {
position: absolute;
top: -1rem;
left: auto;
right: 0;
width: 3.5rem;
} }
} }
&--menu {
position: relative;
top: -3.5rem;
.media-disabled > button { @media (max-width: 600px) {
white-space: normal; 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 { CONTENT_TYPES_WITH_PREVIEW } from '../../../config';
import { IS_TOUCH_ENV } from '../../../util/environment'; import { IS_TOUCH_ENV } from '../../../util/environment';
import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
import './AttachMenu.scss'; import './AttachMenu.scss';
export type OwnProps = { export type OwnProps = {
isOpen: boolean; isButtonVisible: boolean;
canAttachMedia: boolean; canAttachMedia: boolean;
canAttachPolls: boolean; canAttachPolls: boolean;
onFileSelect: (files: File[], isQuick: boolean) => void; onFileSelect: (files: File[], isQuick: boolean) => void;
onPollCreate: () => void; onPollCreate: () => void;
onClose: () => void;
}; };
const AttachMenu: FC<OwnProps> = ({ 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 handleFileSelect = useCallback((e: Event, isQuick: boolean) => {
const { files } = e.target as HTMLInputElement; const { files } = e.target as HTMLInputElement;
@ -46,38 +56,58 @@ const AttachMenu: FC<OwnProps> = ({
const lang = useLang(); const lang = useLang();
if (!isButtonVisible) {
return;
}
return ( return (
<Menu <div className="AttachMenu">
isOpen={isOpen} <ResponsiveHoverButton
autoClose id="attach-menu-button"
positionX="right" className={isAttachMenuOpen ? 'AttachMenu--button activated' : 'AttachMenu--button'}
positionY="bottom" round
onClose={onClose} color="translucent"
className="AttachMenu fluid" onActivate={openAttachMenu}
onCloseAnimationEnd={onClose} ariaLabel="Add an attachment"
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} ariaControls="attach-menu-controls"
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} hasPopup
noCloseOnBackdrop={!IS_TOUCH_ENV} >
> <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 ** Using ternary operator here causes some attributes from first clause
** transferring to the fragment content in the second clause ** transferring to the fragment content in the second clause
*/} */}
{!canAttachMedia && ( {!canAttachMedia && (
<MenuItem className="media-disabled" disabled>Posting media content is not allowed in this group.</MenuItem> <MenuItem className="media-disabled" disabled>Posting media content is not allowed in this group.</MenuItem>
)} )}
{canAttachMedia && ( {canAttachMedia && (
<> <>
<MenuItem icon="photo" onClick={handleQuickSelect}> <MenuItem icon="photo" onClick={handleQuickSelect}>
{lang('AttachmentMenu.PhotoOrVideo')} {lang('AttachmentMenu.PhotoOrVideo')}
</MenuItem> </MenuItem>
<MenuItem icon="document" onClick={handleDocumentSelect}>{lang('AttachDocument')}</MenuItem> <MenuItem icon="document" onClick={handleDocumentSelect}>{lang('AttachDocument')}</MenuItem>
</> </>
)} )}
{canAttachPolls && ( {canAttachPolls && (
<MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem> <MenuItem icon="poll" onClick={onPollCreate}>{lang('Poll')}</MenuItem>
)} )}
</Menu> </Menu>
</div>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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