chore: Add eslint-plugin-jsx-a11y plugin (#32140)

This commit is contained in:
Douglas Fabris 2024-04-14 19:11:50 -03:00 committed by GitHub
parent 30e34b3aab
commit b63b33b3fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 810 additions and 39 deletions

View File

@ -1,14 +1,13 @@
import { Box, Icon, TextInput, Button, Margins } from '@rocket.chat/fuselage';
import { useAutoFocus, useMergedRefs } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactNode, ChangeEvent, FormEvent, ReactElement } from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import type { ReactNode, ChangeEvent, FormEvent } from 'react';
import React, { forwardRef, memo, useCallback, useEffect, useState } from 'react';
type FilterByTextCommonProps = {
children?: ReactNode | undefined;
placeholder?: string;
inputRef?: () => void;
onChange: (filter: { text: string }) => void;
autoFocus?: boolean;
};
type FilterByTextPropsWithButton = FilterByTextCommonProps & {
@ -22,10 +21,14 @@ type FilterByTextProps = FilterByTextCommonProps | FilterByTextPropsWithButton;
const isFilterByTextPropsWithButton = (props: any): props is FilterByTextPropsWithButton =>
'displayButton' in props && props.displayButton === true;
const FilterByText = ({ placeholder, onChange: setFilter, inputRef, children, autoFocus, ...props }: FilterByTextProps): ReactElement => {
const FilterByText = forwardRef<HTMLElement, FilterByTextProps>(function FilterByText(
{ placeholder, onChange: setFilter, children, ...props },
ref,
) {
const t = useTranslation();
const [text, setText] = useState('');
const autoFocusRef = useAutoFocus();
const mergedRefs = useMergedRefs(ref, autoFocusRef);
const handleInputChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setText(event.currentTarget.value);
@ -44,11 +47,10 @@ const FilterByText = ({ placeholder, onChange: setFilter, inputRef, children, au
<Box mi={4} display='flex' flexGrow={1}>
<TextInput
placeholder={placeholder ?? t('Search')}
ref={inputRef}
ref={mergedRefs}
addon={<Icon name='magnifier' size='x20' />}
onChange={handleInputChange}
value={text}
autoFocus={autoFocus}
flexGrow={2}
minWidth='x220'
/>
@ -62,6 +64,6 @@ const FilterByText = ({ placeholder, onChange: setFilter, inputRef, children, au
)}
</Box>
);
};
});
export default memo<FilterByTextProps>(FilterByText);
export default memo(FilterByText);

View File

@ -128,7 +128,7 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[];
return createPortal(
<FocusScope contain autoFocus>
<Box className={swiperStyle}>
<div className='swiper-container' onClick={onClose}>
<div role='presentation' className='swiper-container' onClick={onClose}>
<ButtonGroup className='rcx-swiper-controls' onClick={preventPropagation}>
{zoomScale !== 1 && <IconButton small icon='arrow-collapse' title='Resize' rcx-swiper-zoom-out onClick={handleResize} />}
<IconButton small icon='h-bar' title='Zoom out' rcx-swiper-zoom-out onClick={handleZoomOut} disabled={zoomScale === 1} />
@ -155,7 +155,10 @@ export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[];
{images?.map(({ _id, url }) => (
<SwiperSlide key={_id}>
<div className='swiper-zoom-container'>
<img src={url} loading='lazy' onClick={preventPropagation} />
<span tabIndex={0} role='link' onClick={preventPropagation} onKeyDown={preventPropagation}>
<img src={url} loading='lazy' alt='' />
</span>
<div className='rcx-lazy-preloader'>
<Throbber inheritColor />
</div>

View File

@ -19,9 +19,15 @@ const IgnoredContent = ({ onShowMessageIgnored }: IgnoredContentProps): ReactEle
return (
<MessageBody data-qa-type='message-body' dir='auto'>
<Box display='flex' alignItems='center' fontSize='c2' color='hint'>
<p role='button' onClick={showMessageIgnored} style={{ cursor: 'pointer' }}>
<span
tabIndex={0}
role='button'
onClick={showMessageIgnored}
onKeyDown={(e) => e.code === 'Enter' && showMessageIgnored(e)}
style={{ cursor: 'pointer' }}
>
<Icon name='chevron-left' /> {t('Message_Ignored')}
</p>
</span>
</Box>
</MessageBody>
);

View File

@ -83,6 +83,7 @@ const AttachmentImage: FC<AttachmentImageProps> = ({ id, previewUrl, dataSrc, lo
className='gallery-item'
data-src={dataSrc || src}
src={src}
alt=''
width={dimensions.width}
height={dimensions.height}
/>

View File

@ -8,6 +8,7 @@ const UrlVideoPreview = ({ url, originalType }: Omit<UrlPreviewMetadata, 'type'>
<video controls style={style}>
<source src={url} type={originalType} />
Your browser doesn't support the video element.
<track kind='captions' />
</video>
);

View File

@ -1,8 +1,8 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import type { MouseEvent } from 'react';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { UIEvent } from 'react';
export const usePreventPropagation = (fn?: (e: MouseEvent) => void): ((e: MouseEvent) => void) => {
const preventClickPropagation = useMutableCallback((e): void => {
export const usePreventPropagation = (fn?: (e: UIEvent) => void): ((e: UIEvent) => void) => {
const preventClickPropagation = useEffectEvent((e): void => {
e.stopPropagation();
fn?.(e);
});

View File

@ -526,7 +526,13 @@ export const CallProvider: FC = ({ children }) => {
return (
<CallContext.Provider value={contextValue}>
{children}
{contextValue.enabled && createPortal(<audio ref={remoteAudioMediaRef} />, document.body)}
{contextValue.enabled &&
createPortal(
<audio ref={remoteAudioMediaRef}>
<track kind='captions' />
</audio>,
document.body,
)}
</CallContext.Provider>
);
};

View File

@ -15,7 +15,7 @@ const ModerationFilter = ({ setText, setDateRange }: ModerationFilterProps) => {
const handleChange = useCallback(({ text }): void => setText(text), [setText]);
return (
<FilterByText autoFocus placeholder={t('Search')} onChange={handleChange}>
<FilterByText placeholder={t('Search')} onChange={handleChange}>
<DateRangePicker onChange={setDateRange} />
</FilterByText>
);

View File

@ -144,7 +144,7 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
return (
<>
<FilterByText autoFocus placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
<FilterByText placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>

View File

@ -188,6 +188,7 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => {
<EmojiPickerContainer role='dialog' aria-label={t('Emoji_picker')} onKeyDown={handleKeyDown}>
<EmojiPickerHeader>
<TextInput
// FIXME: remove autoFocus prop when rewriting the emojiPicker dropdown
autoFocus
ref={mergedTextInputRef}
value={searchTerm}

View File

@ -51,13 +51,7 @@ const EnterE2EPasswordModal = ({
<FieldGroup mbs={24} w='full'>
<Field>
<FieldRow>
<PasswordInput
autoFocus
error={passwordError}
value={password}
onChange={handleChange}
placeholder={t('New_Password_Placeholder')}
/>
<PasswordInput error={passwordError} value={password} onChange={handleChange} placeholder={t('New_Password_Placeholder')} />
</FieldRow>
<FieldError>{passwordError}</FieldError>
</Field>

View File

@ -1,4 +1,5 @@
import { Box, Modal } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useEffect } from 'react';
const iframeMsgListener = (confirm, cancel) => (e) => {
@ -13,6 +14,8 @@ const iframeMsgListener = (confirm, cancel) => (e) => {
};
const IframeModal = ({ url, confirm, cancel, wrapperHeight = 'x360', ...props }) => {
const t = useTranslation();
useEffect(() => {
const listener = iframeMsgListener(confirm, cancel);
@ -26,7 +29,7 @@ const IframeModal = ({ url, confirm, cancel, wrapperHeight = 'x360', ...props })
return (
<Modal height={wrapperHeight} {...props}>
<Box padding='x12' w='full' h='full' flexGrow={1} bg='white' borderRadius='x8'>
<iframe style={{ border: 'none', height: '100%', width: '100%' }} src={url} />
<iframe title={t('Marketplace_apps')} style={{ border: 'none', height: '100%', width: '100%' }} src={url} />
</Box>
</Modal>
);

View File

@ -280,7 +280,9 @@ const CallPage: FC<CallPageProps> = ({
transform: 'scaleX(-1)',
display: isRemoteCameraOn ? 'block' : 'none',
}}
></video>
>
<track kind='captions' />
</video>
<Box
position='absolute'
zIndex={1}

View File

@ -39,7 +39,7 @@ function ExternalFrameContainer() {
return (
<div className='flex-nav'>
<iframe className='external-frame' src={externalFrameUrl} />
<iframe title='external-frame' className='external-frame' src={externalFrameUrl} />
</div>
);
}

View File

@ -218,6 +218,7 @@ const RoomBody = (): ReactElement => {
{!isLayoutEmbedded && room.announcement && <Announcement announcement={room.announcement} announcementDetails={undefined} />}
<Box key={room._id} className={['main-content-flex', listStyle]}>
<section
role='presentation'
className={`messages-container flex-tab-main-content ${admin ? 'admin' : ''}`}
id={`chat-window-${room._id}`}
onClick={hideFlexTab && handleCloseFlexTab}

View File

@ -123,12 +123,14 @@ const ComposerBoxPopupPreview = forwardRef(function ComposerBoxPopupPreview(
{item.type === 'image' && <img src={item.value} alt={item._id} />}
{item.type === 'audio' && (
<audio controls>
<track kind='captions' />
<source src={item.value} />
Your browser does not support the audio element.
</audio>
)}
{item.type === 'video' && (
<video controls className='inline-video'>
<track kind='captions' />
<source src={item.value} />
Your browser does not support the video element.
</video>

View File

@ -1,4 +1,4 @@
import { useSession } from '@rocket.chat/ui-contexts';
import { useSession, useTranslation } from '@rocket.chat/ui-contexts';
import type { LoginRoutes } from '@rocket.chat/web-ui-registration';
import RegistrationRoute from '@rocket.chat/web-ui-registration';
import type { ReactElement, ReactNode } from 'react';
@ -8,11 +8,12 @@ import LoggedOutBanner from '../../../../ee/client/components/deviceManagement/L
import { useIframeLogin } from './useIframeLogin';
const LoginPage = ({ defaultRoute, children }: { defaultRoute?: LoginRoutes; children?: ReactNode }): ReactElement => {
const t = useTranslation();
const showForcedLogoutBanner = useSession('force_logout');
const iframeLoginUrl = useIframeLogin();
if (iframeLoginUrl) {
return <iframe src={iframeLoginUrl} style={{ height: '100%', width: '100%' }} />;
return <iframe title={t('Login')} src={iframeLoginUrl} style={{ height: '100%', width: '100%' }} />;
}
return (

View File

@ -1,4 +1,5 @@
import { Avatar } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React from 'react';
@ -18,6 +19,8 @@ interface IGameCenterContainerProps {
}
const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps): ReactElement => {
const t = useTranslation();
return (
<>
<ContextualbarHeader>
@ -28,7 +31,7 @@ const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterConta
{handleClose && <ContextualbarClose onClick={handleClose} />}
</ContextualbarHeader>
<ContextualbarContent pb={16}>
<iframe style={{ position: 'absolute', width: '95%', height: '80%' }} src={game.url}></iframe>
<iframe title={t('Apps_Game_Center')} style={{ position: 'absolute', width: '95%', height: '80%' }} src={game.url}></iframe>
</ContextualbarContent>
</>
);

View File

@ -7,7 +7,7 @@ import { Files } from './Files';
jest.mock('@react-pdf/renderer', () => ({
StyleSheet: { create: () => ({ style: '' }) },
Image: () => <img src='' />,
Image: () => <img src='' alt='' />,
Text: ({ children }: { children: ReactNode }) => <div>{children}</div>,
View: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}));

View File

@ -14,6 +14,7 @@
"eslint-plugin-anti-trojan-source": "~1.1.1",
"eslint-plugin-import": "~2.26.0",
"eslint-plugin-jest": "~27.2.3",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "~4.2.1",
"prettier": "~2.8.8"
},

View File

@ -1,6 +1,7 @@
/** @type {import('eslint').ESLint.ConfigData} */
const config = {
plugins: ['react', 'react-hooks'],
plugins: ['react', 'react-hooks', 'jsx-a11y'],
extends: ['plugin:jsx-a11y/recommended'],
rules: {
'react-hooks/exhaustive-deps': 'error',
'react-hooks/rules-of-hooks': 'error',
@ -12,6 +13,7 @@ const config = {
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/no-multi-comp': 'error',
'jsx-a11y/no-autofocus': [2, { ignoreNonDOM: true }],
},
settings: {
react: {

748
yarn.lock

File diff suppressed because it is too large Load Diff