mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-16 01:55:01 +03:00
fix(frontend): make the panel usable with screen readers (#6907)
* fix(frontend): make the panel usable with screen readers * fix(frontend): keep mobile tab names and the inbound expiry hint Review of #6907 found two gaps in the screen-reader pass: - On phone-width screens catTabLabel renders only the icon and names the tab by putting aria-label on it, but callers now pass that icon with aria-hidden for the desktop label, so every Settings, Xray and Host category tab lost its name under TalkBack. catTabLabel now clears aria-hidden on the icon it uses as the name. - DateTimePicker forwarded id and aria-label but not aria-describedby, so the inbound expiry hint ("leave blank to never expire") was never attached. It now forwards it on both the Gregorian and Jalali input.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
// jsx-a11y cannot see through Ant Design components, so a Switch or Select with no
|
||||
// tie to its visible label reaches screen readers unnamed (s-ui#1293). Guard it here.
|
||||
const CONTROLS = new Set([
|
||||
'Input',
|
||||
'Input.Password',
|
||||
'Input.TextArea',
|
||||
'Input.Search',
|
||||
'InputNumber',
|
||||
'Select',
|
||||
'AutoComplete',
|
||||
'Switch',
|
||||
'DatePicker',
|
||||
'DateTimePicker',
|
||||
'DatePicker.RangePicker',
|
||||
'TimePicker',
|
||||
'Cascader',
|
||||
'TreeSelect',
|
||||
]);
|
||||
|
||||
const UNNAMED =
|
||||
'Form control has no accessible name; give it aria-label, or an id wired to a visible label.';
|
||||
const NESTED =
|
||||
'Form.Item name hands its id only to its direct child; wire htmlFor/id or aria-label for this nested control.';
|
||||
const UNTIED =
|
||||
'Form.Item label without name is not tied to this control; pass htmlFor on the Form.Item and the same id on the control.';
|
||||
|
||||
function tagName(node) {
|
||||
if (!node) return '';
|
||||
if (node.type === 'JSXIdentifier') return node.name;
|
||||
if (node.type === 'JSXMemberExpression') return `${tagName(node.object)}.${node.property.name}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
function attrs(element) {
|
||||
const map = new Map();
|
||||
let spread = false;
|
||||
for (const attr of element.openingElement.attributes ?? []) {
|
||||
if (attr.type === 'JSXAttribute') map.set(attr.name.name, attr);
|
||||
else spread = true;
|
||||
}
|
||||
return { map, spread };
|
||||
}
|
||||
|
||||
// `direct` marks the innermost element: the only one whose cloneElement reaches the control.
|
||||
function* labellingAncestors(node) {
|
||||
let direct = true;
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (cur.type !== 'JSXElement') continue;
|
||||
yield { element: cur, direct };
|
||||
direct = false;
|
||||
}
|
||||
}
|
||||
|
||||
function check(node) {
|
||||
const { map, spread } = attrs(node);
|
||||
if (map.has('aria-label') || map.has('aria-labelledby') || spread) return null;
|
||||
const hasId = map.has('id');
|
||||
for (const { element, direct } of labellingAncestors(node)) {
|
||||
const name = tagName(element.openingElement.name);
|
||||
const parentAttrs = attrs(element).map;
|
||||
if (name === 'label') return null;
|
||||
if (name === 'FormField' && parentAttrs.has('label')) return null;
|
||||
if (name === 'SettingListItem' && parentAttrs.has('title')) return direct ? null : UNNAMED;
|
||||
if (name === 'Form.Item' && parentAttrs.has('label')) {
|
||||
if (parentAttrs.has('name')) return direct ? null : NESTED;
|
||||
if (parentAttrs.has('htmlFor')) return hasId ? null : UNTIED;
|
||||
return UNTIED;
|
||||
}
|
||||
}
|
||||
return hasId ? null : UNNAMED;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: { name: 'control-label' },
|
||||
rules: {
|
||||
'has-name': {
|
||||
create(context) {
|
||||
return {
|
||||
JSXElement(node) {
|
||||
if (!CONTROLS.has(tagName(node.openingElement.name))) return;
|
||||
const message = check(node);
|
||||
if (message) context.report({ node: node.openingElement, message });
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user