Enable some more oxlint rules (#34193)

* Enable some more oxlint rules

* Iterate

* Iterate

* Update packages/shared-components/src/room/timeline/event-tile/EventTileView/E2ePadlock/E2ePadlock.tsx

Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>

---------

Co-authored-by: Hubert Chathi <hubert@uhoreg.ca>
This commit is contained in:
Michael Telatynski
2026-08-10 12:25:55 +00:00
committed by GitHub
co-authored by Hubert Chathi
parent 4807ff1568
commit d54cddcb1d
26 changed files with 115 additions and 76 deletions
@@ -41,6 +41,7 @@ export default class NonUrgentToastContainer extends React.PureComponent<EmptyOb
public render(): React.ReactNode {
const toasts = this.state.toasts.map((t, i) => {
return (
// oxlint-disable-next-line react/no-array-index-key
<div className="mx_NonUrgentToastContainer_toast" key={`toast-${i}`}>
{React.createElement(t, {})}
</div>
@@ -43,6 +43,7 @@ export default class Waveform extends React.PureComponent<IProps> {
});
return (
<span
// oxlint-disable-next-line react/no-array-index-key
key={i}
style={
{
@@ -264,34 +264,50 @@ export default class LoginWithQRFlow extends React.Component<Props> {
);
break;
case Phase.ShowingQR: {
let steps: ReactNode[];
let steps: ReactNode;
if (this.props.intent === RendezvousIntent.LOGIN_ON_NEW_DEVICE) {
steps = [
_t("auth|qr_code_login|open_element_mobile_device", {
brand: SdkConfig.get().brand,
}),
_t("auth|qr_code_login|tap_avatar_link_new_device", {
linkNewDevice: <strong>{_t("settings|sessions|sign_in_with_qr")}</strong>,
}),
_t("auth|qr_code_login|choose_desktop_computer", {
desktopComputer: <strong>{_t("auth|qr_code_login|desktop_computer")}</strong>,
}),
_t("auth|qr_code_login|select_ready_to_scan", {
readyToScan: <strong>{_t("auth|qr_code_login|ready_to_scan")}</strong>,
}),
_t("auth|qr_code_login|follow_remaining_instructions"),
];
steps = (
<>
<li>
{_t("auth|qr_code_login|open_element_mobile_device", {
brand: SdkConfig.get().brand,
})}
</li>
<li>
{_t("auth|qr_code_login|tap_avatar_link_new_device", {
linkNewDevice: <strong>{_t("settings|sessions|sign_in_with_qr")}</strong>,
})}
</li>
<li>
{_t("auth|qr_code_login|choose_desktop_computer", {
desktopComputer: <strong>{_t("auth|qr_code_login|desktop_computer")}</strong>,
})}
</li>
<li>
{_t("auth|qr_code_login|select_ready_to_scan", {
readyToScan: <strong>{_t("auth|qr_code_login|ready_to_scan")}</strong>,
})}
</li>
<li>{_t("auth|qr_code_login|follow_remaining_instructions")}</li>
</>
);
} else {
steps = [
_t("auth|qr_code_login|open_element_other_device", {
brand: SdkConfig.get().brand,
}),
_t("auth|qr_code_login|select_qr_code", {
scanQRCode: <strong>{_t("auth|qr_code_login|scan_qr_code")}</strong>,
}),
_t("auth|qr_code_login|point_the_camera"),
_t("auth|qr_code_login|follow_remaining_instructions"),
];
steps = (
<>
<li>
{_t("auth|qr_code_login|open_element_other_device", {
brand: SdkConfig.get().brand,
})}
</li>
<li>
{_t("auth|qr_code_login|select_qr_code", {
scanQRCode: <strong>{_t("auth|qr_code_login|scan_qr_code")}</strong>,
})}
</li>
<li>{_t("auth|qr_code_login|point_the_camera")}</li>
<li>{_t("auth|qr_code_login|follow_remaining_instructions")}</li>
</>
);
}
main = (
@@ -308,11 +324,7 @@ export default class LoginWithQRFlow extends React.Component<Props> {
<Spinner />
)}
</div>
<ol>
{steps.map((step, i) => (
<li key={this.props.intent + i}>{step}</li>
))}
</ol>
<ol>{steps}</ol>
</>
);
break;
@@ -39,7 +39,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
};
private renderTimeline(): ReactNode[] {
return EchoStore.instance.contexts.map((c, i) => {
return EchoStore.instance.contexts.map((c) => {
if (!c.firstFailedTime) return null; // not useful
if (!(c instanceof RoomEchoContext))
throw new Error("Cannot render unknown context: " + c.constructor.name);
@@ -51,7 +51,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
);
const entries = c.transactions
.filter((t) => t.status === TransactionStatus.Error || t.didPreviouslyFail)
.map((t, j) => {
.map((t) => {
let button = <Spinner size={19} />;
if (t.status === TransactionStatus.Error) {
button = (
@@ -61,14 +61,14 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
);
}
return (
<div className="mx_ServerOfflineDialog_content_context_txn" key={`txn-${j}`}>
<div className="mx_ServerOfflineDialog_content_context_txn" key={t.auditName}>
<span className="mx_ServerOfflineDialog_content_context_txn_desc">{t.auditName}</span>
{button}
</div>
);
});
return (
<div className="mx_ServerOfflineDialog_content_context" key={`context-${i}`}>
<div className="mx_ServerOfflineDialog_content_context" key={c.room.roomId}>
<div className="mx_ServerOfflineDialog_content_context_timestamp">
{formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
</div>
@@ -92,11 +92,11 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
// Both are the same type (both timeline or both non-timeline), sort lexicographically
return lexicographicCompare(capA, capB);
});
const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => {
const checkboxRows = orderedCapabilities.map(([cap, isChecked]) => {
const text = CapabilityText.for(cap, this.props.widgetKind);
return (
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap + i}>
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap}>
<StyledCheckbox
checked={isChecked}
onChange={() => this.onToggle(cap)}
@@ -215,6 +215,7 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState
/>
<h2>{_t("poll|options_heading")}</h2>
{this.state.options.map((op, i) => (
// oxlint-disable-next-line react/no-array-index-key
<div key={`option_${i}`} className="mx_PollCreateDialog_option">
<Field
id={`pollcreate_option_${i}`}
@@ -495,6 +495,7 @@ export class ImageBodyBaseInner extends React.Component<ImageBodyBaseProps, ISta
<div
className="mx_MImageBody_thumbnail_container"
style={{ maxHeight, maxWidth, aspectRatio: `${infoWidth}/${infoHeight}` }}
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={tooltipProps ? 0 : undefined}
>
{placeholder}
@@ -347,10 +347,10 @@ export default class AliasSettings extends React.Component<IProps, IState> {
<option value="" key="unset">
{_t("room_settings|alias_not_specified")}
</option>
{this.getAliases().map((alias, i) => {
{this.getAliases().map((alias) => {
if (alias === this.state.canonicalAlias) found = true;
return (
<option value={alias} key={i}>
<option value={alias} key={alias}>
{alias}
</option>
);
@@ -414,7 +414,7 @@ export default class AliasSettings extends React.Component<IProps, IState> {
)}
<datalist id="mx_AliasSettings_altRecommendations">
{this.getLocalNonAltAliases().map((alias) => {
return <option value={alias} key={alias} />;
return <option value={alias} key={alias} aria-label={alias} />;
})}
</datalist>
<EditableAliasesList
@@ -314,7 +314,11 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
});
return completions.length > 0 ? (
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
<div
key={completionResult.provider.getName()}
className="mx_Autocomplete_ProviderSection"
role="presentation"
>
<div className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</div>
{completionResult.provider.renderCompletions(completions)}
</div>
@@ -218,6 +218,7 @@ function Indicators({ count, currentIndex }: IndicatorsProps): JSX.Element {
return (
<div className="mx_PinnedMessageBanner_Indicators">
{Array.from({ length: numberOfIndicators }).map((_, i) => (
// oxlint-disable-next-line react/no-array-index-key
<Indicator key={i} active={i === index} hidden={isLastCycle && lastCycleIndex <= i} />
))}
</div>
@@ -655,6 +655,7 @@ class RoomPreviewBar extends React.Component<IProps, IState> {
if (!Array.isArray(subTitle)) {
subTitle = [subTitle];
}
// oxlint-disable-next-line react/no-array-index-key
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
}
@@ -11,6 +11,7 @@ import React, { type CSSProperties, memo, type RefObject, type ReactNode } from
import { useIsExpanded } from "../hooks/useIsExpanded";
import { useSelection } from "../hooks/useSelection";
import { _t } from "../../../../../i18n";
const HEIGHT_BREAKING_POINT = 24;
@@ -38,6 +39,7 @@ export const Editor = memo(function Editor({ disabled, placeholder, leftComponen
ref={ref}
contentEditable={!disabled}
role="textbox"
aria-label={_t("a11y|message_composer")}
aria-multiline="true"
aria-autocomplete="list"
aria-haspopup="listbox"
@@ -124,10 +124,10 @@ const DeviceDetails: React.FC<Props> = ({
{!accountManagementEndpoint && (
<section className="mx_DeviceDetails_section">
<p className="mx_DeviceDetails_sectionHeading">{_t("settings|sessions|details_heading")}</p>
{metadata.map(({ heading, values, id }, index) => (
{metadata.map(({ heading, values, id }) => (
<table
className="mx_DeviceDetails_metadataTable"
key={index}
key={id}
data-testid={`device-detail-metadata-${id}`}
>
{heading && (
@@ -387,7 +387,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR
}
const powerSelectors = Object.keys(powerLevelDescriptors)
.map((key, index) => {
.map((key) => {
const descriptor = powerLevelDescriptors[key];
if (isSpaceRoom && descriptor.hideForSpace) {
return null;
@@ -395,7 +395,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR
const value = parseIntWithDefault(get(plContent, key), descriptor.defaultValue);
return (
<div key={index} className="">
<div key={key}>
<PowerSelector
label={descriptor.desc}
value={value}
@@ -45,6 +45,7 @@ export const SpaceAvatar: React.FC<Pick<IProps, "avatarUrl" | "avatarDisabled" |
if (avatar) {
avatarSection = (
<React.Fragment>
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */}
<AccessibleButton
className="mx_SpaceBasicSettings_avatar"
onClick={() => avatarUploadRef.current?.click()}
@@ -65,8 +65,13 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
? _t("encryption|verification|confirm_the_emojis")
: _t("encryption|verification|sas_emoji_caption_user");
} else if (this.props.sas.decimal) {
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>{num}</span>);
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">{numberBlocks}</div>;
sasDisplay = (
<div className="mx_VerificationShowSas_decimalSas">
<span>{this.props.sas.decimal[0]}</span>
<span>{this.props.sas.decimal[1]}</span>
<span>{this.props.sas.decimal[2]}</span>
</div>
);
sasCaption = this.props.isSelf
? _t("encryption|verification|sas_caption_self")
: _t("encryption|verification|sas_caption_user");
@@ -44,8 +44,8 @@ export default class AudioFeedArrayForLegacyCall extends React.Component<IProps,
};
public render(): JSX.Element[] {
return this.state.feeds.map((feed, i) => {
return <AudioFeed feed={feed} key={i} />;
return this.state.feeds.map((feed) => {
return <AudioFeed feed={feed} key={feed.deviceId + feed.userId} />;
});
}
}
+1
View File
@@ -18,6 +18,7 @@ export function jsxJoin(array: ReactNode[], joiner?: string | JSX.Element): JSX.
return (
<>
{array.map((element, index) => (
// oxlint-disable-next-line react/no-array-index-key
<React.Fragment key={index}>
{element}
{index === array.length - 1 ? null : joiner}
@@ -16,18 +16,18 @@ describe("KeyboardLandmarkUtils", () => {
it("Landmarks are cycled through correctly without an opened room", () => {
render(
<div>
<div tabIndex={0} className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
<button className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
SPACE_BUTTON
</div>
<div tabIndex={0} id="room-list-search-button" data-testid="mx_RoomListSearch_search">
</button>
<button id="room-list-search-button" data-testid="mx_RoomListSearch_search">
ROOM_SEARCH
</div>
<div tabIndex={0} className="mx_RoomListItemView" data-testid="mx_RoomListItemView">
</button>
<button className="mx_RoomListItemView" data-testid="mx_RoomListItemView">
ROOM_TILE
</div>
<div tabIndex={0} className="mx_HomePage" data-testid="mx_HomePage">
</button>
<button className="mx_HomePage" data-testid="mx_HomePage">
HOME_PAGE
</div>
</button>
</div>,
);
// ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> HOME <-> ACTIVE_SPACE_BUTTON
@@ -69,21 +69,19 @@ describe("KeyboardLandmarkUtils", () => {
defaultDispatcher.register(callback);
render(
<div>
<div tabIndex={0} className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
<button className="mx_SpaceButton_active" data-testid="mx_SpaceButton_active">
SPACE_BUTTON
</div>
<div tabIndex={0} id="room-list-search-button" data-testid="mx_RoomListSearch_search">
</button>
<button id="room-list-search-button" data-testid="mx_RoomListSearch_search">
ROOM_SEARCH
</div>
<div tabIndex={0} className="mx_RoomListItemView_selected" data-testid="mx_RoomListItemView_selected">
</button>
<button className="mx_RoomListItemView_selected" data-testid="mx_RoomListItemView_selected">
ROOM_TILE
</div>
<div tabIndex={0} className="mx_Room" data-testid="mx_Room">
</button>
<button className="mx_Room" data-testid="mx_Room">
ROOM
<div tabIndex={0} className="mx_MessageComposer">
COMPOSER
</div>
</div>
<div className="mx_MessageComposer">COMPOSER</div>
</button>
</div>,
);
// ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> MESSAGE_COMPOSER <-> ACTIVE_SPACE_BUTTON
+10 -6
View File
@@ -205,6 +205,14 @@ export default defineConfig({
// Prevent unnecessary runtime dependencies between files
"typescript/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
"jsx-a11y/control-has-associated-label": [
"error",
{
labelAttributes: ["label", "value"],
depth: 3,
},
],
// Disable some perf rules
"no-await-in-loop": "off",
@@ -212,7 +220,7 @@ export default defineConfig({
"unicorn/switch-case-braces": "off",
"sort-keys": "off",
"typescript/require-array-sort-compare": "off",
"eslint/no-extra-boolean-cast": "off",
"no-extra-boolean-cast": "off",
// These would be nice to enable at some point
"unicorn/prefer-set-has": "off",
@@ -232,21 +240,17 @@ export default defineConfig({
"typescript/no-implied-eval": "off",
"typescript/no-misused-spread": "off",
"promise/valid-params": "off",
"no-extra-boolean-cast": "off",
"react-perf/jsx-no-new-function-as-prop": "off",
"react-perf/jsx-no-new-object-as-prop": "off",
"react-perf/jsx-no-jsx-as-prop": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/no-autofocus": "off",
"react/no-children-prop": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"react-perf/jsx-no-new-array-as-prop": "off",
"react/no-did-update-set-state": "off",
"react/no-did-mount-set-state": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"react/no-array-index-key": "off",
"jsx-a11y/control-has-associated-label": "off",
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/no-noninteractive-element-to-interactive-role": "off",
"jsx-a11y/aria-activedescendant-has-tabindex": "off",
@@ -522,7 +526,6 @@ export default defineConfig({
"typescript/no-empty-object-type": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "off",
"typescript/no-misused-spread": "off",
"vitest/require-mock-type-parameters": "off",
"vitest/no-disabled-tests": "off",
"vitest/no-conditional-expect": "off",
@@ -565,6 +568,7 @@ export default defineConfig({
"no-new": "off",
"react/iframe-missing-sandbox": "off",
"promise/no-promise-in-callback": "off",
// This would be good to enable in the future
"typescript/await-thenable": "off",
"promise/no-callback-in-promise": "off",
@@ -165,6 +165,7 @@ const groupedVariant: ListTestVariant = {
<div
className="mx_group_header"
data-testid={`group-header-${header.id}`}
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={context.tabIndexKey === header.id ? 0 : -1}
onFocus={(e) => onFocus(header, e)}
>
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import { type JSX } from "react";
import { Text, type HTMLReactParserOptions } from "html-react-parser";
type Replacer = HTMLReactParserOptions["replace"];
@@ -28,7 +28,7 @@ export function applyReplacerOnString(
const arr = Array.isArray(input) ? input : [input];
return arr.map((item, index): JSX.Element => {
if (typeof item === "string") {
return <React.Fragment key={index}>{(replacer(new Text(item), 0) as JSX.Element) || item}</React.Fragment>;
return (replacer(new Text(item), 0) as JSX.Element) || item;
}
return item;
});
@@ -31,6 +31,7 @@ export function SasEmoji({ emoji, className }: Props): JSX.Element {
const { language } = useI18n();
const emojiBlocks = emoji.map((emoji, i) => (
// oxlint-disable-next-line react/no-array-index-key
<div className={styles.segment} key={i}>
<div className={styles.emoji} aria-hidden={true}>
{emoji}
@@ -102,6 +102,7 @@ export const RoomListPrimaryFilters = memo(function RoomListPrimaryFilters({
>
{visibleFilterIds.map((filterId, index) => (
<ChatFilter
// oxlint-disable-next-line react/no-array-index-key
key={`${filterId}-${index}`}
role="option"
tabIndex={0}
@@ -69,6 +69,8 @@ export function E2ePadlock({ icon, title, className }: Readonly<E2ePadlockProps>
data-testid="e2e-padlock"
className={classNames(styles.e2ePadlock, iconClasses[icon], className)}
role="img"
// This is only tab indexed to act as a tooltip trigger
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
aria-label={_t("timeline|e2e_state")}
>
@@ -257,6 +257,7 @@ export function FileBodyView({ vm, refIFrame, refLink, className }: Readonly<Fil
* We'll use it to learn how the download button
* would have been styled if it was rendered inline.
* this violates multiple eslint rules so ignore it completely */}
{/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */}
<Button size="md" kind="secondary" Icon={DownloadIcon} as="a" ref={refLink} />
</div>
{/*