From 21ed6c1dea180198efdeca834eac24c0a80e2150 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 15:00:26 -0400 Subject: [PATCH] put the exported function up top --- .../2-analyze/visitors/shared/a11y/index.js | 674 +++++++++--------- 1 file changed, 337 insertions(+), 337 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js index baadd0b69b..5db31314d4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js @@ -50,359 +50,79 @@ import { is_content_editable_binding } from '../../../../../../utils.js'; import * as w from '../../../../../warnings.js'; /** - * @param {ARIARoleDefinitionKey} role - */ -function is_presentation_role(role) { - return presentation_roles.includes(role); -} - -/** - * @param {string} tag_name - * @param {Map} attribute_map - */ -function is_hidden_from_screen_reader(tag_name, attribute_map) { - if (tag_name === 'input') { - const type = get_static_value(attribute_map.get('type')); - if (type === 'hidden') { - return true; - } - } - - const aria_hidden = attribute_map.get('aria-hidden'); - if (!aria_hidden) return false; - const aria_hidden_value = get_static_value(aria_hidden); - if (aria_hidden_value === null) return true; - return aria_hidden_value === true || aria_hidden_value === 'true'; -} - -/** - * @param {Map} attribute_map + * @param {AST.RegularElement | AST.SvelteElement} node + * @param {Context} context */ -function has_disabled_attribute(attribute_map) { - const disabled_attr_value = get_static_value(attribute_map.get('disabled')); - if (disabled_attr_value) { - return true; - } - - const aria_disabled_attr = attribute_map.get('aria-disabled'); - if (aria_disabled_attr) { - const aria_disabled_attr_value = get_static_value(aria_disabled_attr); - if (aria_disabled_attr_value === 'true') { - return true; - } - } - return false; -} +export function check_element(node, context) { + /** @type {Map} */ + const attribute_map = new Map(); -/** - * @param {string} tag_name - * @param {Map} attribute_map - * @returns {ElementInteractivity[keyof ElementInteractivity]} - */ -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; -} + /** @type {Set} */ + const handlers = new Set(); -/** - * @param {string} tag_name - * @param {Map} attribute_map - * @returns {boolean} - */ -function is_interactive_element(tag_name, attribute_map) { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive; -} + /** @type {AST.Attribute[]} */ + const attributes = []; -/** - * @param {string} tag_name - * @param {Map} attribute_map - * @returns {boolean} - */ -function is_non_interactive_element(tag_name, attribute_map) { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive; -} + const is_dynamic_element = node.type === 'SvelteElement'; -/** - * @param {string} tag_name - * @param {Map} attribute_map - * @returns {boolean} - */ -function is_static_element(tag_name, attribute_map) { - return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static; -} + let has_spread = false; + let has_contenteditable_attr = false; + let has_contenteditable_binding = false; -/** - * @param {ARIARoleDefinitionKey} role - * @param {string} tag_name - * @param {Map} attribute_map - */ -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( - /** @param {any} attr */ - (attr) => - attribute_map.has(attr.name) && - get_static_value(attribute_map.get(attr.name)) === 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; - } + for (const attribute of node.attributes) { + switch (attribute.type) { + case 'Attribute': { + if (is_event_attribute(attribute)) { + handlers.add(attribute.name.slice(2)); + } else { + attributes.push(attribute); + attribute_map.set(attribute.name, attribute); + if (attribute.name === 'contenteditable') { + has_contenteditable_attr = true; } } + break; + } + case 'SpreadAttribute': { + has_spread = true; + break; + } + case 'BindDirective': { + if (is_content_editable_binding(attribute.name)) { + has_contenteditable_binding = true; + } + break; + } + case 'OnDirective': { + handlers.add(attribute.name); + break; } } } - return false; -} -/** - * @param {null | true | string} autocomplete - */ -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.includes(tokens[0])) { - tokens.shift(); - } - if (autofill_field_name_tokens.includes(tokens[0])) { - tokens.shift(); - } else { - if (contact_type_tokens.includes(tokens[0])) { - tokens.shift(); - } - if (autofill_contact_field_name_tokens.includes(tokens[0])) { - tokens.shift(); - } else { - return false; - } - } - if (tokens[0] === 'webauthn') { - tokens.shift(); - } - return tokens.length === 0; -} + for (const attribute of node.attributes) { + if (attribute.type !== 'Attribute') continue; -/** @param {Map} attribute_map */ -function input_implicit_role(attribute_map) { - const type_attribute = attribute_map.get('type'); - if (!type_attribute) return; - const type = get_static_text_value(type_attribute); - if (!type) return; - const list_attribute_exists = attribute_map.has('list'); - if (list_attribute_exists && combobox_if_list.includes(type)) { - return 'combobox'; - } - return input_type_to_implicit_role.get(type); -} + const name = attribute.name.toLowerCase(); + // aria-props + if (name.startsWith('aria-')) { + if (invisible_elements.includes(node.name)) { + // aria-unsupported-elements + w.a11y_aria_attributes(attribute, node.name); + } -/** @param {Map} attribute_map */ -function menuitem_implicit_role(attribute_map) { - const type_attribute = attribute_map.get('type'); - if (!type_attribute) return; - const type = get_static_text_value(type_attribute); - if (!type) return; - return menuitem_type_to_implicit_role.get(type); -} + const type = name.slice(5); + if (!aria_attributes.includes(type)) { + const match = fuzzymatch(type, aria_attributes); + w.a11y_unknown_aria_attribute(attribute, type, match); + } -/** - * @param {string} name - * @param {Map} attribute_map - */ -function get_implicit_role(name, attribute_map) { - if (name === 'menuitem') { - return menuitem_implicit_role(attribute_map); - } else if (name === 'input') { - return input_implicit_role(attribute_map); - } else { - return a11y_implicit_semantics.get(name); - } -} + if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) { + w.a11y_hidden(attribute, node.name); + } -/** - * @param {ARIARoleDefinitionKey} role - */ -function is_non_interactive_roles(role) { - return non_interactive_roles.includes(role); -} - -/** - * @param {ARIARoleDefinitionKey} role - */ -function is_interactive_roles(role) { - return interactive_roles.includes(role); -} - -/** - * @param {ARIARoleDefinitionKey} role - */ -function is_abstract_role(role) { - return abstract_roles.includes(role); -} - -/** - * @param {AST.Attribute | undefined} attribute - */ -function get_static_text_value(attribute) { - const value = get_static_value(attribute); - if (value === true) return null; - return value; -} - -/** - * @param {AST.Attribute | undefined} attribute - */ -function get_static_value(attribute) { - if (!attribute) return null; - if (attribute.value === true) return true; - if (is_text_attribute(attribute)) return attribute.value[0].data; - return null; -} - -/** - * @param {AST.RegularElement | AST.SvelteElement} element - */ -function has_content(element) { - for (const node of element.fragment.nodes) { - if (node.type === 'Text') { - if (node.data.trim() === '') { - continue; - } - } - - if (node.type === 'RegularElement' || node.type === 'SvelteElement') { - if ( - node.name === 'img' && - node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt') - ) { - return true; - } - - if (!has_content(node)) { - continue; - } - } - - // assume everything else has content — this will result in false positives - // (e.g. an empty `{#if ...}{/if}`) but that's probably fine - return true; - } -} - -/** - * @param {AST.RegularElement | AST.SvelteElement} node - * @param {Context} context - */ -export function check_element(node, context) { - /** @type {Map} */ - const attribute_map = new Map(); - - /** @type {Set} */ - const handlers = new Set(); - - /** @type {AST.Attribute[]} */ - const attributes = []; - - const is_dynamic_element = node.type === 'SvelteElement'; - - let has_spread = false; - let has_contenteditable_attr = false; - let has_contenteditable_binding = false; - - for (const attribute of node.attributes) { - switch (attribute.type) { - case 'Attribute': { - if (is_event_attribute(attribute)) { - handlers.add(attribute.name.slice(2)); - } else { - attributes.push(attribute); - attribute_map.set(attribute.name, attribute); - if (attribute.name === 'contenteditable') { - has_contenteditable_attr = true; - } - } - break; - } - case 'SpreadAttribute': { - has_spread = true; - break; - } - case 'BindDirective': { - if (is_content_editable_binding(attribute.name)) { - has_contenteditable_binding = true; - } - break; - } - case 'OnDirective': { - handlers.add(attribute.name); - break; - } - } - } - - for (const attribute of node.attributes) { - if (attribute.type !== 'Attribute') continue; - - const name = attribute.name.toLowerCase(); - // aria-props - if (name.startsWith('aria-')) { - if (invisible_elements.includes(node.name)) { - // aria-unsupported-elements - w.a11y_aria_attributes(attribute, node.name); - } - - const type = name.slice(5); - if (!aria_attributes.includes(type)) { - const match = fuzzymatch(type, aria_attributes); - w.a11y_unknown_aria_attribute(attribute, type, match); - } - - if (name === 'aria-hidden' && regex_heading_tags.test(node.name)) { - w.a11y_hidden(attribute, node.name); - } - - // aria-proptypes - let value = get_static_value(attribute); + // aria-proptypes + let value = get_static_value(attribute); const schema = aria.get(/** @type {ARIAProperty} */ (name)); if (schema !== undefined) { @@ -828,6 +548,286 @@ export function check_element(node, context) { } } +/** + * @param {ARIARoleDefinitionKey} role + */ +function is_presentation_role(role) { + return presentation_roles.includes(role); +} + +/** + * @param {string} tag_name + * @param {Map} attribute_map + */ +function is_hidden_from_screen_reader(tag_name, attribute_map) { + if (tag_name === 'input') { + const type = get_static_value(attribute_map.get('type')); + if (type === 'hidden') { + return true; + } + } + + const aria_hidden = attribute_map.get('aria-hidden'); + if (!aria_hidden) return false; + const aria_hidden_value = get_static_value(aria_hidden); + if (aria_hidden_value === null) return true; + return aria_hidden_value === true || aria_hidden_value === 'true'; +} + +/** + * @param {Map} attribute_map + */ +function has_disabled_attribute(attribute_map) { + const disabled_attr_value = get_static_value(attribute_map.get('disabled')); + if (disabled_attr_value) { + return true; + } + + const aria_disabled_attr = attribute_map.get('aria-disabled'); + if (aria_disabled_attr) { + const aria_disabled_attr_value = get_static_value(aria_disabled_attr); + if (aria_disabled_attr_value === 'true') { + return true; + } + } + return false; +} + +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {ElementInteractivity[keyof ElementInteractivity]} + */ +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; +} + +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +function is_interactive_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Interactive; +} + +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +function is_non_interactive_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.NonInteractive; +} + +/** + * @param {string} tag_name + * @param {Map} attribute_map + * @returns {boolean} + */ +function is_static_element(tag_name, attribute_map) { + return element_interactivity(tag_name, attribute_map) === ElementInteractivity.Static; +} + +/** + * @param {ARIARoleDefinitionKey} role + * @param {string} tag_name + * @param {Map} attribute_map + */ +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( + /** @param {any} attr */ + (attr) => + attribute_map.has(attr.name) && + get_static_value(attribute_map.get(attr.name)) === 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 {null | true | string} autocomplete + */ +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.includes(tokens[0])) { + tokens.shift(); + } + if (autofill_field_name_tokens.includes(tokens[0])) { + tokens.shift(); + } else { + if (contact_type_tokens.includes(tokens[0])) { + tokens.shift(); + } + if (autofill_contact_field_name_tokens.includes(tokens[0])) { + tokens.shift(); + } else { + return false; + } + } + if (tokens[0] === 'webauthn') { + tokens.shift(); + } + return tokens.length === 0; +} + +/** @param {Map} attribute_map */ +function input_implicit_role(attribute_map) { + const type_attribute = attribute_map.get('type'); + if (!type_attribute) return; + const type = get_static_text_value(type_attribute); + if (!type) return; + const list_attribute_exists = attribute_map.has('list'); + if (list_attribute_exists && combobox_if_list.includes(type)) { + return 'combobox'; + } + return input_type_to_implicit_role.get(type); +} + +/** @param {Map} attribute_map */ +function menuitem_implicit_role(attribute_map) { + const type_attribute = attribute_map.get('type'); + if (!type_attribute) return; + const type = get_static_text_value(type_attribute); + if (!type) return; + return menuitem_type_to_implicit_role.get(type); +} + +/** + * @param {string} name + * @param {Map} attribute_map + */ +function get_implicit_role(name, attribute_map) { + if (name === 'menuitem') { + return menuitem_implicit_role(attribute_map); + } else if (name === 'input') { + return input_implicit_role(attribute_map); + } else { + return a11y_implicit_semantics.get(name); + } +} + +/** + * @param {ARIARoleDefinitionKey} role + */ +function is_non_interactive_roles(role) { + return non_interactive_roles.includes(role); +} + +/** + * @param {ARIARoleDefinitionKey} role + */ +function is_interactive_roles(role) { + return interactive_roles.includes(role); +} + +/** + * @param {ARIARoleDefinitionKey} role + */ +function is_abstract_role(role) { + return abstract_roles.includes(role); +} + +/** + * @param {AST.Attribute | undefined} attribute + */ +function get_static_text_value(attribute) { + const value = get_static_value(attribute); + if (value === true) return null; + return value; +} + +/** + * @param {AST.Attribute | undefined} attribute + */ +function get_static_value(attribute) { + if (!attribute) return null; + if (attribute.value === true) return true; + if (is_text_attribute(attribute)) return attribute.value[0].data; + return null; +} + +/** + * @param {AST.RegularElement | AST.SvelteElement} element + */ +function has_content(element) { + for (const node of element.fragment.nodes) { + if (node.type === 'Text') { + if (node.data.trim() === '') { + continue; + } + } + + if (node.type === 'RegularElement' || node.type === 'SvelteElement') { + if ( + node.name === 'img' && + node.attributes.some((node) => node.type === 'Attribute' && node.name === 'alt') + ) { + return true; + } + + if (!has_content(node)) { + continue; + } + } + + // assume everything else has content — this will result in false positives + // (e.g. an empty `{#if ...}{/if}`) but that's probably fine + return true; + } +} + /** * @param {ARIARoleRelationConcept} schema * @param {string} tag_name