diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts index afe276769f..45463b6e4c 100644 --- a/src/compiler/compile/utils/a11y.ts +++ b/src/compiler/compile/utils/a11y.ts @@ -1,354 +1,341 @@ -import { - ARIARoleDefinitionKey, - roles as roles_map, - elementRoles, - ARIARoleRelationConcept -} from 'aria-query'; +import { roles as roles_map, elementRoles } from 'aria-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; -import Attribute from '../nodes/Attribute'; -import { regex_whitespaces } from '../../utils/patterns'; - +import { regex_whitespaces } from '../../utils/patterns.js'; const aria_roles = roles_map.keys(); const abstract_roles = new Set(aria_roles.filter((role) => roles_map.get(role).abstract)); const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.has(name)); - -const non_interactive_roles = new Set( - non_abstract_roles - .filter((name) => { - const role = roles_map.get(name); - return ( - // 'toolbar' does not descend from widget, but it does support - // aria-activedescendant, thus in practice we treat it as a widget. - // focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable. - // 'generic' is meant to have no semantic meaning. - !['toolbar', 'tabpanel', 'generic'].includes(name) && - !role.superClass.some((classes) => classes.includes('widget')) - ); - }) - .concat( - // The `progressbar` is descended from `widget`, but in practice, its - // value is always `readonly`, so we treat it as a non-interactive role. - 'progressbar' - ) -); - -const interactive_roles = new Set( - non_abstract_roles.filter( - (name) => - !non_interactive_roles.has(name) && - // 'generic' is meant to have no semantic meaning. - name !== 'generic' - ) -); - -export function is_non_interactive_roles(role: ARIARoleDefinitionKey) { - return non_interactive_roles.has(role); +const non_interactive_roles = new Set(non_abstract_roles + .filter((name) => { + const role = roles_map.get(name); + return ( + // 'toolbar' does not descend from widget, but it does support + // aria-activedescendant, thus in practice we treat it as a widget. + // focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable. + // 'generic' is meant to have no semantic meaning. + !['toolbar', 'tabpanel', 'generic'].includes(name) && + !role.superClass.some((classes) => classes.includes('widget'))); +}) + .concat( +// The `progressbar` is descended from `widget`, but in practice, its +// value is always `readonly`, so we treat it as a non-interactive role. +'progressbar')); +const interactive_roles = new Set(non_abstract_roles.filter((name) => !non_interactive_roles.has(name) && + // 'generic' is meant to have no semantic meaning. + name !== 'generic')); + +/** +* @param {ARIARoleDefinitionKey} role + */ +export function is_non_interactive_roles(role) { + return non_interactive_roles.has(role); } -export function is_interactive_roles(role: ARIARoleDefinitionKey) { - return interactive_roles.has(role); +/** +* @param {ARIARoleDefinitionKey} role + */ +export function is_interactive_roles(role) { + return interactive_roles.has(role); } -export function is_abstract_role(role: ARIARoleDefinitionKey) { - return abstract_roles.has(role); +/** +* @param {ARIARoleDefinitionKey} role + */ +export function is_abstract_role(role) { + return abstract_roles.has(role); } - const presentation_roles = new Set(['presentation', 'none']); -export function is_presentation_role(role: ARIARoleDefinitionKey) { - return presentation_roles.has(role); +/** +* @param {ARIARoleDefinitionKey} role + */ +export function is_presentation_role(role) { + return presentation_roles.has(role); } -export function is_hidden_from_screen_reader( - tag_name: string, - attribute_map: Map -) { - if (tag_name === 'input') { - const type = attribute_map.get('type')?.get_static_value(); - - if (type && type === 'hidden') { - return true; - } - } - - const aria_hidden = attribute_map.get('aria-hidden'); - if (!aria_hidden) return false; - if (!aria_hidden.is_static) return true; - const aria_hidden_value = aria_hidden.get_static_value(); - return aria_hidden_value === true || aria_hidden_value === 'true'; +/** + * @param {string} tag_name + * @param {Map} attribute_map + */ +export function is_hidden_from_screen_reader(tag_name, attribute_map) { + if (tag_name === 'input') { + const type = attribute_map.get('type')?.get_static_value(); + if (type && type === 'hidden') { + return true; + } + } + const aria_hidden = attribute_map.get('aria-hidden'); + if (!aria_hidden) + return false; + if (!aria_hidden.is_static) + return true; + const aria_hidden_value = aria_hidden.get_static_value(); + return aria_hidden_value === true || aria_hidden_value === 'true'; } -export function has_disabled_attribute(attribute_map: Map) { - const disabled_attr = attribute_map.get('disabled'); - const disabled_attr_value = disabled_attr && disabled_attr.get_static_value(); - if (disabled_attr_value) { - return true; - } - - const aria_disabled_attr = attribute_map.get('aria-disabled'); - if (aria_disabled_attr) { - const aria_disabled_attr_value = aria_disabled_attr.get_static_value(); - if (aria_disabled_attr_value === true) { - return true; - } - } - - return false; +/** +* @param {Map} attribute_map + */ +export function has_disabled_attribute(attribute_map) { + const disabled_attr = attribute_map.get('disabled'); + const disabled_attr_value = disabled_attr && disabled_attr.get_static_value(); + if (disabled_attr_value) { + return true; + } + const aria_disabled_attr = attribute_map.get('aria-disabled'); + if (aria_disabled_attr) { + const aria_disabled_attr_value = aria_disabled_attr.get_static_value(); + if (aria_disabled_attr_value === true) { + return true; + } + } + return false; } -const non_interactive_element_role_schemas: ARIARoleRelationConcept[] = []; - +/** +* @type {ARIARoleRelationConcept[]} + */ +const non_interactive_element_role_schemas = []; elementRoles.entries().forEach(([schema, roles]) => { - if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.has(role))) { - non_interactive_element_role_schemas.push(schema); - } + if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.has(role))) { + non_interactive_element_role_schemas.push(schema); + } }); -const interactive_element_role_schemas: ARIARoleRelationConcept[] = []; - +/** +* @type {ARIARoleRelationConcept[]} + */ +const interactive_element_role_schemas = []; elementRoles.entries().forEach(([schema, roles]) => { - if ([...roles].every((role) => interactive_roles.has(role))) { - interactive_element_role_schemas.push(schema); - } + if ([...roles].every((role) => interactive_roles.has(role))) { + interactive_element_role_schemas.push(schema); + } }); +const interactive_ax_objects = new Set([...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget')); +const non_interactive_ax_objects = new Set([...AXObjects.keys()].filter((name) => ['windows', 'structure'].includes(AXObjects.get(name).type))); -const interactive_ax_objects = new Set( - [...AXObjects.keys()].filter((name) => AXObjects.get(name).type === 'widget') -); - -const non_interactive_ax_objects = new Set( - [...AXObjects.keys()].filter((name) => - ['windows', 'structure'].includes(AXObjects.get(name).type) - ) -); - -const interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = []; - +/** +* @type {ARIARoleRelationConcept[]} + */ +const interactive_element_ax_object_schemas = []; elementAXObjects.entries().forEach(([schema, ax_object]) => { - if ([...ax_object].every((role) => interactive_ax_objects.has(role))) { - interactive_element_ax_object_schemas.push(schema); - } + if ([...ax_object].every((role) => interactive_ax_objects.has(role))) { + interactive_element_ax_object_schemas.push(schema); + } }); -const non_interactive_element_ax_object_schemas: ARIARoleRelationConcept[] = []; - +/** +* @type {ARIARoleRelationConcept[]} + */ +const non_interactive_element_ax_object_schemas = []; elementAXObjects.entries().forEach(([schema, ax_object]) => { - if ([...ax_object].every((role) => non_interactive_ax_objects.has(role))) { - non_interactive_element_ax_object_schemas.push(schema); - } + if ([...ax_object].every((role) => non_interactive_ax_objects.has(role))) { + non_interactive_element_ax_object_schemas.push(schema); + } }); -function match_schema( - schema: ARIARoleRelationConcept, - tag_name: string, - attribute_map: Map -) { - if (schema.name !== tag_name) return false; - if (!schema.attributes) return true; - return schema.attributes.every((schema_attribute) => { - const attribute = attribute_map.get(schema_attribute.name); - if (!attribute) return false; - if (schema_attribute.value && schema_attribute.value !== attribute.get_static_value()) { - return false; - } - return true; - }); -} - -export enum ElementInteractivity { - Interactive = 'interactive', - NonInteractive = 'non-interactive', - Static = 'static' +/** + * @param {ARIARoleRelationConcept} schema + * @param {string} tag_name + * @param {Map} attribute_map + */ +function match_schema(schema, tag_name, attribute_map) { + if (schema.name !== tag_name) + return false; + if (!schema.attributes) + return true; + return schema.attributes.every((schema_attribute) => { + const attribute = attribute_map.get(schema_attribute.name); + if (!attribute) + return false; + if (schema_attribute.value && schema_attribute.value !== attribute.get_static_value()) { + return false; + } + return true; + }); } - -export function element_interactivity( - tag_name: string, - attribute_map: Map -): ElementInteractivity { - if ( - interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map)) - ) { - return ElementInteractivity.Interactive; - } - - if ( - tag_name !== 'header' && - non_interactive_element_role_schemas.some((schema) => - match_schema(schema, tag_name, attribute_map) - ) - ) { - return ElementInteractivity.NonInteractive; - } - - if ( - interactive_element_ax_object_schemas.some((schema) => - match_schema(schema, tag_name, attribute_map) - ) - ) { - return ElementInteractivity.Interactive; - } - - if ( - non_interactive_element_ax_object_schemas.some((schema) => - match_schema(schema, tag_name, attribute_map) - ) - ) { - return ElementInteractivity.NonInteractive; - } - - return ElementInteractivity.Static; +export var ElementInteractivity; +(function (ElementInteractivity) { + ElementInteractivity["Interactive"] = "interactive"; + ElementInteractivity["NonInteractive"] = "non-interactive"; + ElementInteractivity["Static"] = "static"; +})(ElementInteractivity || (ElementInteractivity = {})); + +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {import("C:/repos/svelte/svelte/a11y.ts-to-jsdoc").ElementInteractivity} + */ +export function element_interactivity(tag_name, attribute_map) { + if (interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map))) { + return ElementInteractivity.Interactive; + } + if (tag_name !== 'header' && + non_interactive_element_role_schemas.some((schema) => match_schema(schema, tag_name, attribute_map))) { + return ElementInteractivity.NonInteractive; + } + if (interactive_element_ax_object_schemas.some((schema) => match_schema(schema, tag_name, attribute_map))) { + return ElementInteractivity.Interactive; + } + if (non_interactive_element_ax_object_schemas.some((schema) => match_schema(schema, tag_name, attribute_map))) { + return ElementInteractivity.NonInteractive; + } + return ElementInteractivity.Static; } -export function is_interactive_element( - tag_name: string, - attribute_map: Map -): boolean { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive; +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +export function is_interactive_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive; } -export function is_non_interactive_element( - tag_name: string, - attribute_map: Map -): boolean { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive; +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +export function is_non_interactive_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive; } -export function is_static_element( - tag_name: string, - attribute_map: Map -): boolean { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static; +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +export function is_static_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static; } -export function is_semantic_role_element( - role: ARIARoleDefinitionKey, - tag_name: string, - attribute_map: Map -) { - for (const [schema, ax_object] of elementAXObjects.entries()) { - if ( - schema.name === tag_name && - (!schema.attributes || - schema.attributes.every( - (attr) => - attribute_map.has(attr.name) && - attribute_map.get(attr.name).get_static_value() === attr.value - )) - ) { - for (const name of ax_object) { - const roles = AXObjectRoles.get(name); - if (roles) { - for (const { name } of roles) { - if (name === role) { - return true; - } - } - } - } - } - } - return false; +/** + * @param {ARIARoleDefinitionKey} role + * @param {string} tag_name + * @param {Map} attribute_map + */ +export function is_semantic_role_element(role, tag_name, attribute_map) { + for (const [schema, ax_object] of elementAXObjects.entries()) { + if (schema.name === tag_name && + (!schema.attributes || + schema.attributes.every((attr) => attribute_map.has(attr.name) && + attribute_map.get(attr.name).get_static_value() === attr.value))) { + for (const name of ax_object) { + const roles = AXObjectRoles.get(name); + if (roles) { + for (const { name } of roles) { + if (name === role) { + return true; + } + } + } + } + } + } + return false; } - // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute const address_type_tokens = new Set(['shipping', 'billing']); const autofill_field_name_tokens = new Set([ - '', - 'on', - 'off', - 'name', - 'honorific-prefix', - 'given-name', - 'additional-name', - 'family-name', - 'honorific-suffix', - 'nickname', - 'username', - 'new-password', - 'current-password', - 'one-time-code', - 'organization-title', - 'organization', - 'street-address', - 'address-line1', - 'address-line2', - 'address-line3', - 'address-level4', - 'address-level3', - 'address-level2', - 'address-level1', - 'country', - 'country-name', - 'postal-code', - 'cc-name', - 'cc-given-name', - 'cc-additional-name', - 'cc-family-name', - 'cc-number', - 'cc-exp', - 'cc-exp-month', - 'cc-exp-year', - 'cc-csc', - 'cc-type', - 'transaction-currency', - 'transaction-amount', - 'language', - 'bday', - 'bday-day', - 'bday-month', - 'bday-year', - 'sex', - 'url', - 'photo' + '', + 'on', + 'off', + 'name', + 'honorific-prefix', + 'given-name', + 'additional-name', + 'family-name', + 'honorific-suffix', + 'nickname', + 'username', + 'new-password', + 'current-password', + 'one-time-code', + 'organization-title', + 'organization', + 'street-address', + 'address-line1', + 'address-line2', + 'address-line3', + 'address-level4', + 'address-level3', + 'address-level2', + 'address-level1', + 'country', + 'country-name', + 'postal-code', + 'cc-name', + 'cc-given-name', + 'cc-additional-name', + 'cc-family-name', + 'cc-number', + 'cc-exp', + 'cc-exp-month', + 'cc-exp-year', + 'cc-csc', + 'cc-type', + 'transaction-currency', + 'transaction-amount', + 'language', + 'bday', + 'bday-day', + 'bday-month', + 'bday-year', + 'sex', + 'url', + 'photo' ]); const contact_type_tokens = new Set(['home', 'work', 'mobile', 'fax', 'pager']); const autofill_contact_field_name_tokens = new Set([ - 'tel', - 'tel-country-code', - 'tel-national', - 'tel-area-code', - 'tel-local', - 'tel-local-prefix', - 'tel-local-suffix', - 'tel-extension', - 'email', - 'impp' + 'tel', + 'tel-country-code', + 'tel-national', + 'tel-area-code', + 'tel-local', + 'tel-local-prefix', + 'tel-local-suffix', + 'tel-extension', + 'email', + 'impp' ]); -export function is_valid_autocomplete(autocomplete: null | true | string) { - if (autocomplete === true) { - return false; - } else if (!autocomplete) { - return true; // dynamic value - } - - const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces); - - if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) { - tokens.shift(); - } - - if (address_type_tokens.has(tokens[0])) { - tokens.shift(); - } +/** +* @param {null | true | string} autocomplete + */ +export function is_valid_autocomplete(autocomplete) { + if (autocomplete === true) { + return false; + } + else if (!autocomplete) { + return true; // dynamic value + } + const tokens = autocomplete.trim().toLowerCase().split(regex_whitespaces); + if (typeof tokens[0] === 'string' && tokens[0].startsWith('section-')) { + tokens.shift(); + } + if (address_type_tokens.has(tokens[0])) { + tokens.shift(); + } + if (autofill_field_name_tokens.has(tokens[0])) { + tokens.shift(); + } + else { + if (contact_type_tokens.has(tokens[0])) { + tokens.shift(); + } + if (autofill_contact_field_name_tokens.has(tokens[0])) { + tokens.shift(); + } + else { + return false; + } + } + if (tokens[0] === 'webauthn') { + tokens.shift(); + } + return tokens.length === 0; +} - if (autofill_field_name_tokens.has(tokens[0])) { - tokens.shift(); - } else { - if (contact_type_tokens.has(tokens[0])) { - tokens.shift(); - } - if (autofill_contact_field_name_tokens.has(tokens[0])) { - tokens.shift(); - } else { - return false; - } - } - if (tokens[0] === 'webauthn') { - tokens.shift(); - } - return tokens.length === 0; -}