Module: Dommy::Rails::Lint

Defined in:
lib/dommy/rails/lint.rb

Constant Summary collapse

NON_LABELABLE_INPUT_TYPES =

Inputs whose accessible name comes from elsewhere (value attribute) or that are not user-visible, so a <label> is not expected.

%w[hidden submit button reset image].freeze

Class Method Summary collapse

Class Method Details



95
96
97
98
99
100
101
102
# File 'lib/dommy/rails/lint.rb', line 95

def accessible_link_text(link)
  [
    link.text_content,
    link.get_attribute("aria-label"),
    link.get_attribute("title"),
    image_alt_text(link)
  ].compact.join.strip
end

.duplicate_ids(document) ⇒ Object



15
16
17
18
19
# File 'lib/dommy/rails/lint.rb', line 15

def duplicate_ids(document)
  all_elements = document.query_selector_all("*[id]").to_a
  ids = all_elements.map { |el| el.get_attribute("id") }
  ids.select { |id| ids.count(id) > 1 }.uniq
end


53
54
55
56
57
# File 'lib/dommy/rails/lint.rb', line 53

def empty_links(document)
  document.query_selector_all("a[href]").to_a.select do |link|
    accessible_link_text(link).empty?
  end
end

.image_alt_text(link) ⇒ Object



104
105
106
# File 'lib/dommy/rails/lint.rb', line 104

def image_alt_text(link)
  link.query_selector_all("img").to_a.map { |image| image.get_attribute("alt").to_s }.join
end

.interactive_element?(element) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/dommy/rails/lint.rb', line 80

def interactive_element?(element)
  return false unless element.respond_to?(:tag_name)

  case element.tag_name
  when "A"
    element.has_attribute?("href")
  when "BUTTON", "SELECT", "TEXTAREA", "SUMMARY"
    true
  when "INPUT"
    element.get_attribute("type").to_s.downcase != "hidden"
  else
    false
  end
end

.interactive_elements(document) ⇒ Object



74
75
76
77
78
# File 'lib/dommy/rails/lint.rb', line 74

def interactive_elements(document)
  document.query_selector_all("a[href], button, input, select, textarea, summary").to_a.reject do |element|
    element.tag_name == "INPUT" && element.get_attribute("type").to_s.downcase == "hidden"
  end
end

.invalid_aria_references(document) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/dommy/rails/lint.rb', line 21

def invalid_aria_references(document)
  issues = []
  document.query_selector_all("*").each do |el|
    %w[aria-labelledby aria-describedby].each do |attr|
      next unless el.has_attribute?(attr)

      el.get_attribute(attr).to_s.split.each do |id|
        issues << { element: el, attribute: attr, id: id } unless document.get_element_by_id(id)
      end
    end
  end
  issues
end

.missing_form_labels(document) ⇒ Object

Lenient policy: aria-label / aria-labelledby / placeholder are all accepted as label substitutes, even though a placeholder is not a sufficient accessible name under WCAG.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/dommy/rails/lint.rb', line 38

def missing_form_labels(document)
  issues = []
  document.query_selector_all("input, textarea, select").each do |field|
    next if field.tag_name == "INPUT" &&
      NON_LABELABLE_INPUT_TYPES.include?(field.get_attribute("type").to_s.downcase)
    next if field.has_attribute?("aria-label")
    next if field.has_attribute?("aria-labelledby")
    next if field.has_attribute?("placeholder")
    next if Dommy::Internal::ElementMatching.field_labels(field).any?

    issues << { element: field, name: field.get_attribute("name") }
  end
  issues
end

.nested_interactive_elements(document) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/dommy/rails/lint.rb', line 59

def nested_interactive_elements(document)
  issues = []
  interactive_elements(document).each do |element|
    parent = element.parent_node
    while parent
      if interactive_element?(parent)
        issues << { element: element, ancestor: parent }
        break
      end
      parent = parent.respond_to?(:parent_node) ? parent.parent_node : nil
    end
  end
  issues
end