From d54cddcb1d84224fef73745873cd67728578890f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 10 Aug 2026 13:25:55 +0100 Subject: [PATCH] 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 --------- Co-authored-by: Hubert Chathi --- .../structures/NonUrgentToastContainer.tsx | 1 + .../views/audio_messages/Waveform.tsx | 1 + .../components/views/auth/LoginWithQRFlow.tsx | 74 +++++++++++-------- .../views/dialogs/ServerOfflineDialog.tsx | 8 +- .../WidgetCapabilitiesPromptDialog.tsx | 4 +- .../views/elements/PollCreateDialog.tsx | 1 + .../views/messages/MImageReplyBody.tsx | 1 + .../views/room_settings/AliasSettings.tsx | 6 +- .../components/views/rooms/Autocomplete.tsx | 6 +- .../views/rooms/PinnedMessageBanner.tsx | 1 + .../components/views/rooms/RoomPreviewBar.tsx | 1 + .../wysiwyg_composer/components/Editor.tsx | 2 + .../views/settings/devices/DeviceDetails.tsx | 4 +- .../tabs/room/RolesRoomSettingsTab.tsx | 4 +- .../views/spaces/SpaceBasicSettings.tsx | 1 + .../verification/VerificationShowSas.tsx | 9 ++- .../voip/AudioFeedArrayForLegacyCall.tsx | 4 +- apps/web/src/utils/ReactUtils.tsx | 1 + .../accessibility/LandmarkNavigation-test.tsx | 36 +++++---- oxlint.config.ts | 16 ++-- .../VirtualizedList/virtualized-list.test.tsx | 1 + ...rOnString.tsx => applyReplacerOnString.ts} | 4 +- .../src/crypto/SasEmoji/SasEmoji.tsx | 1 + .../RoomListPrimaryFilters.tsx | 1 + .../EventTileView/E2ePadlock/E2ePadlock.tsx | 2 + .../body/MFileBodyView/FileBodyView.tsx | 1 + 26 files changed, 115 insertions(+), 76 deletions(-) rename packages/shared-components/src/core/utils/{applyReplacerOnString.tsx => applyReplacerOnString.ts} (87%) diff --git a/apps/web/src/components/structures/NonUrgentToastContainer.tsx b/apps/web/src/components/structures/NonUrgentToastContainer.tsx index 481c9f0bf9..b7103b378a 100644 --- a/apps/web/src/components/structures/NonUrgentToastContainer.tsx +++ b/apps/web/src/components/structures/NonUrgentToastContainer.tsx @@ -41,6 +41,7 @@ export default class NonUrgentToastContainer extends React.PureComponent { return ( + // oxlint-disable-next-line react/no-array-index-key
{React.createElement(t, {})}
diff --git a/apps/web/src/components/views/audio_messages/Waveform.tsx b/apps/web/src/components/views/audio_messages/Waveform.tsx index 115b310f4c..03ae0d5617 100644 --- a/apps/web/src/components/views/audio_messages/Waveform.tsx +++ b/apps/web/src/components/views/audio_messages/Waveform.tsx @@ -43,6 +43,7 @@ export default class Waveform extends React.PureComponent { }); return ( { ); 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: {_t("settings|sessions|sign_in_with_qr")}, - }), - _t("auth|qr_code_login|choose_desktop_computer", { - desktopComputer: {_t("auth|qr_code_login|desktop_computer")}, - }), - _t("auth|qr_code_login|select_ready_to_scan", { - readyToScan: {_t("auth|qr_code_login|ready_to_scan")}, - }), - _t("auth|qr_code_login|follow_remaining_instructions"), - ]; + 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: {_t("settings|sessions|sign_in_with_qr")}, + })} +
  • +
  • + {_t("auth|qr_code_login|choose_desktop_computer", { + desktopComputer: {_t("auth|qr_code_login|desktop_computer")}, + })} +
  • +
  • + {_t("auth|qr_code_login|select_ready_to_scan", { + readyToScan: {_t("auth|qr_code_login|ready_to_scan")}, + })} +
  • +
  • {_t("auth|qr_code_login|follow_remaining_instructions")}
  • + + ); } else { - steps = [ - _t("auth|qr_code_login|open_element_other_device", { - brand: SdkConfig.get().brand, - }), - _t("auth|qr_code_login|select_qr_code", { - scanQRCode: {_t("auth|qr_code_login|scan_qr_code")}, - }), - _t("auth|qr_code_login|point_the_camera"), - _t("auth|qr_code_login|follow_remaining_instructions"), - ]; + steps = ( + <> +
  • + {_t("auth|qr_code_login|open_element_other_device", { + brand: SdkConfig.get().brand, + })} +
  • +
  • + {_t("auth|qr_code_login|select_qr_code", { + scanQRCode: {_t("auth|qr_code_login|scan_qr_code")}, + })} +
  • +
  • {_t("auth|qr_code_login|point_the_camera")}
  • +
  • {_t("auth|qr_code_login|follow_remaining_instructions")}
  • + + ); } main = ( @@ -308,11 +324,7 @@ export default class LoginWithQRFlow extends React.Component { )} -
      - {steps.map((step, i) => ( -
    1. {step}
    2. - ))} -
    +
      {steps}
    ); break; diff --git a/apps/web/src/components/views/dialogs/ServerOfflineDialog.tsx b/apps/web/src/components/views/dialogs/ServerOfflineDialog.tsx index f403ecb790..9576728f95 100644 --- a/apps/web/src/components/views/dialogs/ServerOfflineDialog.tsx +++ b/apps/web/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -39,7 +39,7 @@ export default class ServerOfflineDialog extends React.PureComponent { }; 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 { ); const entries = c.transactions .filter((t) => t.status === TransactionStatus.Error || t.didPreviouslyFail) - .map((t, j) => { + .map((t) => { let button = ; if (t.status === TransactionStatus.Error) { button = ( @@ -61,14 +61,14 @@ export default class ServerOfflineDialog extends React.PureComponent { ); } return ( -
    +
    {t.auditName} {button}
    ); }); return ( -
    +
    {formatTime(c.firstFailedTime, SettingsStore.getValue("showTwelveHourTimestamps"))}
    diff --git a/apps/web/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/apps/web/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx index 157761e217..c5bfe7bedf 100644 --- a/apps/web/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx +++ b/apps/web/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx @@ -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 ( -
    +
    this.onToggle(cap)} diff --git a/apps/web/src/components/views/elements/PollCreateDialog.tsx b/apps/web/src/components/views/elements/PollCreateDialog.tsx index f72c28f26e..a97230c57f 100644 --- a/apps/web/src/components/views/elements/PollCreateDialog.tsx +++ b/apps/web/src/components/views/elements/PollCreateDialog.tsx @@ -215,6 +215,7 @@ export default class PollCreateDialog extends ScrollableBaseModal

    {_t("poll|options_heading")}

    {this.state.options.map((op, i) => ( + // oxlint-disable-next-line react/no-array-index-key
    {placeholder} diff --git a/apps/web/src/components/views/room_settings/AliasSettings.tsx b/apps/web/src/components/views/room_settings/AliasSettings.tsx index 1633cfeb62..0ce0d500b3 100644 --- a/apps/web/src/components/views/room_settings/AliasSettings.tsx +++ b/apps/web/src/components/views/room_settings/AliasSettings.tsx @@ -347,10 +347,10 @@ export default class AliasSettings extends React.Component { - {this.getAliases().map((alias, i) => { + {this.getAliases().map((alias) => { if (alias === this.state.canonicalAlias) found = true; return ( - ); @@ -414,7 +414,7 @@ export default class AliasSettings extends React.Component { )} {this.getLocalNonAltAliases().map((alias) => { - return { }); return completions.length > 0 ? ( -
    +
    {completionResult.provider.getName()}
    {completionResult.provider.renderCompletions(completions)}
    diff --git a/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx b/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx index 6615743683..7be7be08d8 100644 --- a/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx @@ -218,6 +218,7 @@ function Indicators({ count, currentIndex }: IndicatorsProps): JSX.Element { return (
    {Array.from({ length: numberOfIndicators }).map((_, i) => ( + // oxlint-disable-next-line react/no-array-index-key
    diff --git a/apps/web/src/components/views/rooms/RoomPreviewBar.tsx b/apps/web/src/components/views/rooms/RoomPreviewBar.tsx index 1a0195a68c..f3de47ca94 100644 --- a/apps/web/src/components/views/rooms/RoomPreviewBar.tsx +++ b/apps/web/src/components/views/rooms/RoomPreviewBar.tsx @@ -655,6 +655,7 @@ class RoomPreviewBar extends React.Component { if (!Array.isArray(subTitle)) { subTitle = [subTitle]; } + // oxlint-disable-next-line react/no-array-index-key subTitleElements = subTitle.map((t, i) =>

    {t}

    ); } diff --git a/apps/web/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/apps/web/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index d418a0df19..9a6847c636 100644 --- a/apps/web/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/apps/web/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -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" diff --git a/apps/web/src/components/views/settings/devices/DeviceDetails.tsx b/apps/web/src/components/views/settings/devices/DeviceDetails.tsx index d70c38205e..adc8d9270d 100644 --- a/apps/web/src/components/views/settings/devices/DeviceDetails.tsx +++ b/apps/web/src/components/views/settings/devices/DeviceDetails.tsx @@ -124,10 +124,10 @@ const DeviceDetails: React.FC = ({ {!accountManagementEndpoint && (

    {_t("settings|sessions|details_heading")}

    - {metadata.map(({ heading, values, id }, index) => ( + {metadata.map(({ heading, values, id }) => ( {heading && ( diff --git a/apps/web/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/apps/web/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 4f59829516..993dfead69 100644 --- a/apps/web/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/apps/web/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -387,7 +387,7 @@ export default class RolesRoomSettingsTab extends React.Component { + .map((key) => { const descriptor = powerLevelDescriptors[key]; if (isSpaceRoom && descriptor.hideForSpace) { return null; @@ -395,7 +395,7 @@ export default class RolesRoomSettingsTab extends React.Component +
    + {/* oxlint-disable-next-line jsx-a11y/control-has-associated-label */} avatarUploadRef.current?.click()} diff --git a/apps/web/src/components/views/verification/VerificationShowSas.tsx b/apps/web/src/components/views/verification/VerificationShowSas.tsx index 45e386510d..cc890f72b0 100644 --- a/apps/web/src/components/views/verification/VerificationShowSas.tsx +++ b/apps/web/src/components/views/verification/VerificationShowSas.tsx @@ -65,8 +65,13 @@ export default class VerificationShowSas extends React.Component ? _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) => {num}); - sasDisplay =
    {numberBlocks}
    ; + sasDisplay = ( +
    + {this.props.sas.decimal[0]} + {this.props.sas.decimal[1]} + {this.props.sas.decimal[2]} +
    + ); sasCaption = this.props.isSelf ? _t("encryption|verification|sas_caption_self") : _t("encryption|verification|sas_caption_user"); diff --git a/apps/web/src/components/views/voip/AudioFeedArrayForLegacyCall.tsx b/apps/web/src/components/views/voip/AudioFeedArrayForLegacyCall.tsx index 062ea8693e..c11cd29e7d 100644 --- a/apps/web/src/components/views/voip/AudioFeedArrayForLegacyCall.tsx +++ b/apps/web/src/components/views/voip/AudioFeedArrayForLegacyCall.tsx @@ -44,8 +44,8 @@ export default class AudioFeedArrayForLegacyCall extends React.Component { - return ; + return this.state.feeds.map((feed) => { + return ; }); } } diff --git a/apps/web/src/utils/ReactUtils.tsx b/apps/web/src/utils/ReactUtils.tsx index 40217b1adb..f75d94da7a 100644 --- a/apps/web/src/utils/ReactUtils.tsx +++ b/apps/web/src/utils/ReactUtils.tsx @@ -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 {element} {index === array.length - 1 ? null : joiner} diff --git a/apps/web/test/unit-tests/accessibility/LandmarkNavigation-test.tsx b/apps/web/test/unit-tests/accessibility/LandmarkNavigation-test.tsx index 878981f708..ae536ccd8d 100644 --- a/apps/web/test/unit-tests/accessibility/LandmarkNavigation-test.tsx +++ b/apps/web/test/unit-tests/accessibility/LandmarkNavigation-test.tsx @@ -16,18 +16,18 @@ describe("KeyboardLandmarkUtils", () => { it("Landmarks are cycled through correctly without an opened room", () => { render(
    -
    +
    -
    + +
    -
    + +
    -
    + +
    +
    , ); // ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> HOME <-> ACTIVE_SPACE_BUTTON @@ -69,21 +69,19 @@ describe("KeyboardLandmarkUtils", () => { defaultDispatcher.register(callback); render(
    -
    +
    -
    + +
    -
    + +
    -
    + +
    +
    COMPOSER
    +
    , ); // ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> MESSAGE_COMPOSER <-> ACTIVE_SPACE_BUTTON diff --git a/oxlint.config.ts b/oxlint.config.ts index 1e286f9644..93eb36835f 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -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", diff --git a/packages/shared-components/src/core/VirtualizedList/virtualized-list.test.tsx b/packages/shared-components/src/core/VirtualizedList/virtualized-list.test.tsx index c9b31f272e..d677707adf 100644 --- a/packages/shared-components/src/core/VirtualizedList/virtualized-list.test.tsx +++ b/packages/shared-components/src/core/VirtualizedList/virtualized-list.test.tsx @@ -165,6 +165,7 @@ const groupedVariant: ListTestVariant = {
    onFocus(header, e)} > diff --git a/packages/shared-components/src/core/utils/applyReplacerOnString.tsx b/packages/shared-components/src/core/utils/applyReplacerOnString.ts similarity index 87% rename from packages/shared-components/src/core/utils/applyReplacerOnString.tsx rename to packages/shared-components/src/core/utils/applyReplacerOnString.ts index 334ae1fd99..bf5ab31026 100644 --- a/packages/shared-components/src/core/utils/applyReplacerOnString.tsx +++ b/packages/shared-components/src/core/utils/applyReplacerOnString.ts @@ -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 {(replacer(new Text(item), 0) as JSX.Element) || item}; + return (replacer(new Text(item), 0) as JSX.Element) || item; } return item; }); diff --git a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx index bc9ae90185..4cbcc5e910 100644 --- a/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx +++ b/packages/shared-components/src/crypto/SasEmoji/SasEmoji.tsx @@ -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
    {emoji} diff --git a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx index 679120c0ed..51cc951dd4 100644 --- a/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx +++ b/packages/shared-components/src/room-list/RoomListPrimaryFilters/RoomListPrimaryFilters.tsx @@ -102,6 +102,7 @@ export const RoomListPrimaryFilters = memo(function RoomListPrimaryFilters({ > {visibleFilterIds.map((filterId, index) => ( 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")} > diff --git a/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.tsx b/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.tsx index f13753d6e4..5bf6d242e6 100644 --- a/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.tsx @@ -257,6 +257,7 @@ export function FileBodyView({ vm, refIFrame, refLink, className }: Readonly
    {/*