Files
3x-ui/frontend/.oxlintrc.json
T
Alireza Ahmadi 1bdb517b88 fix(a11y): silence live stats and make Select tags removable for screen readers
Ant Design's Spin wraps its content in aria-live="polite", so every
WebSocket tick on the dashboard, inbounds, clients and nodes pages was
announced, and only the changed number was read, with no label.
Pass aria-live="off" on every Spin that wraps content; aria-busy still
signals loading. Each vital tile is now a group named by its metric,
so "47.2 %" is read as CPU.

rc-select renders each tag's remove "x" as an aria-hidden span with no
tabIndex, so screen-reader users could only Backspace the last value
away, never remove one SNI or inbound from the middle.
selectTagRender swaps in a real button named "Remove <value>" and
moves focus to the next tag after removal. It is applied to every
tags/multiple Select. maxTagCount="responsive" is dropped because it
hid selected inbounds behind "+N", where no screen reader could reach
them.

Two oxlint rules (antd-a11y/spin-content-not-live and
antd-a11y/select-tag-remove) keep new code from regressing either fix.
2026-11-01 16:49:19 +01:00

103 lines
2.5 KiB
JSON

{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
"node_modules/**"
],
"plugins": [
"typescript",
"react",
"jsx-a11y"
],
"jsPlugins": [
"./tools/oxlint/input-number-guard.mjs",
"./tools/oxlint/control-label-guard.mjs",
"./tools/oxlint/icon-a11y-guard.mjs",
"./tools/oxlint/antd-a11y-guard.mjs"
],
"categories": {
"correctness": "error"
},
"env": {
"browser": true,
"es2022": true
},
"rules": {
"typescript/no-explicit-any": "error",
"typescript/no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"typescript/ban-ts-comment": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-namespace": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-unused-expressions": "warn",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/triple-slash-reference": "error",
"no-empty": [
"error",
{
"allowEmptyCatch": true
}
],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"jsx-a11y/no-autofocus": "off",
"input-number/no-synthetic-clear": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"control-label/has-name": "off",
"antd-icon/explicit-a11y": "off",
"antd-a11y/spin-content-not-live": "off",
"antd-a11y/select-tag-remove": "off"
},
"overrides": [
{
"files": [
"src/pages/settings/**/*.tsx",
"src/pages/xray/**/*.tsx"
],
"rules": {
"input-number/no-synthetic-clear": "error"
}
},
{
"files": [
"src/pages/xray/**/*Modal.tsx"
],
"rules": {
"input-number/no-synthetic-clear": "off"
}
},
{
"files": [
"src/**/*.tsx"
],
"rules": {
"control-label/has-name": "error",
"antd-icon/explicit-a11y": "error",
"antd-a11y/spin-content-not-live": "error",
"antd-a11y/select-tag-remove": "error"
}
},
{
"files": [
"src/**/*.stories.tsx",
"src/test/**"
],
"rules": {
"control-label/has-name": "off",
"antd-icon/explicit-a11y": "off",
"antd-a11y/spin-content-not-live": "off",
"antd-a11y/select-tag-remove": "off"
}
}
]
}