mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-27 22:40:49 +00:00
chore: Add eslint-plugin-jsx-a11y plugin (#32140)
This commit is contained in:
parent
30e34b3aab
commit
b63b33b3fb
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>,
|
||||
}));
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
4
packages/eslint-config/react.js
vendored
4
packages/eslint-config/react.js
vendored
@ -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: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user