Module: Dommy::Internal::AriaState

Defined in:
lib/dommy/internal/aria_state.rb

Overview

Computes the ARIA state/property set an accessibility tree exposes for an element, given its already-computed role. Returns a compact hash holding only the states meaningful for that role, in a fixed key order so an ARIA snapshot serializes deterministically:

checked, disabled, expanded, level, pressed, readonly, required, selected

Each value is true, false, "mixed", or (for level) an Integer. A state native to the host language (an 's checked/disabled, an

Constant Summary collapse

LEVEL_ROLES =

Roles that expose aria-level / a heading level in the snapshot.

%w[heading listitem row treeitem].freeze
CHECKABLE =
%w[checkbox radio menuitemcheckbox menuitemradio switch treeitem].freeze
SELECTABLE =
%w[option tab row gridcell treeitem columnheader rowheader].freeze
READONLY_ROLES =
%w[textbox searchbox spinbutton combobox gridcell columnheader rowheader].freeze
REQUIRED_ROLES =
%w[textbox searchbox spinbutton combobox listbox radiogroup checkbox].freeze
DISABLEABLE_TAGS =

Form controls whose disabled content attribute is meaningful but which do not (all) expose a reflected disabled IDL property in Dommy.

%w[button fieldset input optgroup option select textarea].freeze

Class Method Summary collapse

Class Method Details

.add(states, key, value) ⇒ Object



43
44
45
# File 'lib/dommy/internal/aria_state.rb', line 43

def add(states, key, value)
  states[key] = value unless value.nil?
end

.aria_true?(element, attribute) ⇒ Boolean

Returns:

  • (Boolean)


140
# File 'lib/dommy/internal/aria_state.rb', line 140

def aria_true?(element, attribute) = element.get_attribute(attribute).to_s.casecmp?("true")

.checked_state(element) ⇒ Object

checkbox/radio: an indeterminate native control is "mixed"; otherwise the native checkedness, else the tri-state aria-checked.



49
50
51
52
53
54
# File 'lib/dommy/internal/aria_state.rb', line 49

def checked_state(element)
  return "mixed" if element.respond_to?(:indeterminate) && element.indeterminate
  return element.checked if native_checkbox_radio?(element)

  tristate(element, "aria-checked")
end

.compute(element, role) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/dommy/internal/aria_state.rb', line 26

def compute(element, role)
  states = {}
  add(states, :checked, checked_state(element)) if CHECKABLE.include?(role)
  add(states, :disabled, disabled_state(element))
  add(states, :expanded, expanded_state(element))
  # The level (hN tag or aria-level) is reported for the roles that
  # support it, even when an explicit role overrides the heading role
  # (<h2 role=listitem> is `listitem [level=2]`, but <h2 role=tablist> has
  # no level).
  add(states, :level, LEVEL_ROLES.include?(role) ? AriaRole.heading_level(element) : nil)
  add(states, :pressed, tristate(element, "aria-pressed")) if role == "button"
  add(states, :readonly, readonly_state(element)) if READONLY_ROLES.include?(role)
  add(states, :required, required_state(element)) if REQUIRED_ROLES.include?(role)
  add(states, :selected, selected_state(element)) if SELECTABLE.include?(role)
  states
end

.disabled_state(element) ⇒ Object

disabled / readonly / required are binary (no "[disabled=false]"): return true or nil so the caller omits the key when absent.



112
113
114
# File 'lib/dommy/internal/aria_state.rb', line 112

def disabled_state(element)
  true if native_disabled?(element) || aria_true?(element, "aria-disabled")
end

Returns:

  • (Boolean)


91
92
93
# File 'lib/dommy/internal/aria_state.rb', line 91

def dropdown?(select)
  !multiple_select?(select) && select.get_attribute("size").to_s.to_i <= 1
end

.effective_single_selection(select) ⇒ Object

The single selected option: the last bearing selected, or — only for a drop-down (not multiple and no size>1) — the first option when none is.



83
84
85
86
87
88
89
# File 'lib/dommy/internal/aria_state.rb', line 83

def effective_single_selection(select)
  options = select.query_selector_all("option").to_a
  marked = options.reverse_each.find { |option| option.has_attribute?("selected") }
  return marked if marked

  dropdown?(select) ? options.first : nil
end

.expanded_state(element) ⇒ Object

Expanded comes only from aria-expanded. A native

is NOT reported as expanded (Playwright / Chromium put no expanded state on the details group).



102
103
104
# File 'lib/dommy/internal/aria_state.rb', line 102

def expanded_state(element)
  tristate(element, "aria-expanded")
end

.multiple_select?(select) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/dommy/internal/aria_state.rb', line 77

def multiple_select?(select)
  select.respond_to?(:multiple) && select.multiple
end

.native_checkbox_radio?(element) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
145
# File 'lib/dommy/internal/aria_state.rb', line 142

def native_checkbox_radio?(element)
  element.respond_to?(:checked) && tag?(element, "input") &&
    %w[checkbox radio].include?(element.get_attribute("type").to_s.downcase)
end

.native_disabled?(element) ⇒ Boolean

Native disabledness: the reflected property when present (input/option/ optgroup), else the content attribute on a disable-able control.

Returns:

  • (Boolean)


118
119
120
121
122
# File 'lib/dommy/internal/aria_state.rb', line 118

def native_disabled?(element)
  return element.disabled if element.respond_to?(:disabled)

  DISABLEABLE_TAGS.include?(element.local_name.to_s.downcase) && element.has_attribute?("disabled")
end

.native_option?(element) ⇒ Boolean

Returns:

  • (Boolean)


147
# File 'lib/dommy/internal/aria_state.rb', line 147

def native_option?(element) = element.respond_to?(:selected) && tag?(element, "option")

.option_selected?(option) ⇒ Boolean

An