Serialize aria- namespaced list attributes

Summary
===

Prior to this commit, calls passing `aria: { labelledby: [...] }`
serialized the `aria-labelledby` Array value as JSON.

This commit introduces special case logic to serialize `aria-` prefixed
`TrueClass`, `FalseClass`, `Hash`, and `Array` values more
appropriately.

An element's [`aria-labelledby` attribute][aria-labelledby] and
[`aria-describedby` attribute][aria-describedby] can accept a
space-delimited list of identifier values (much like the [`class`
attribute][class] accepts a space delimited [`DOMTokenList`
value][DOMTokenList]).

Similarly, there are [no boolean `aria-` attributes][aria-attributes]
(only `true`, `false`, or undefined), so this commit serializes `true`
to `"true"` and `false` to `"false"`.

Testing
---

This change moves an assertion _outside_ of a loop over `["aria",
:aria]`. Prior to this change, the second assertion within the loop
wasn't utilizing the iterated value as a Hash key. That is to say:
`aria:` (where an `aria` local variable is declared) is not equivalent
an equivalent syntax to `aria =>`.

Since the migration to `**options` in response to Ruby 2.7 deprecations,
invoking `tag.a("aria" => {...})` incorrectly coerces the `"aria" =>
{...}` has to be the `TagBuilder#a` method `content = nil` ordered
argument, instead of its `options` keyword arguments. This commit does
not modify that behavior, but it _does_ move the assertion outside the
block so that it isn't run unnecessarily.

[aria-labelledby]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-labelledby_attribute
[aria-describedby]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute
[aria-attributes]: https://www.w3.org/TR/wai-aria-1.1/#propcharacteristic_value
[class]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class
[DOMTokenList]: https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList
[class_names]: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names
This commit is contained in:
Sean Doyle 2020-10-28 21:41:21 -04:00
parent 43daedcb7d
commit 8b19d66fc6
3 changed files with 38 additions and 9 deletions

View File

@ -1,3 +1,20 @@
* ARIA Array and Hash attributes are treated as space separated `DOMTokenList`
values. This is useful when declaring lists of label text identifiers in
`aria-labelledby` or `aria-describedby`.
tag.input type: 'checkbox', name: 'published', aria: {
invalid: @post.errors[:published].any?,
labelledby: ['published_context', 'published_label'],
describedby: { published_errors: @post.errors[:published].any? }
}
#=> <input
type="checkbox" name="published" aria-invalid="true"
aria-labelledby="published_context published_label"
aria-describedby="published_errors"
>
*Sean Doyle*
* Remove deprecated `escape_whitelist` from `ActionView::Template::Handlers::ERB`. * Remove deprecated `escape_whitelist` from `ActionView::Template::Handlers::ERB`.
*Rafael Mendonça França* *Rafael Mendonça França*

View File

@ -26,11 +26,13 @@ module ActionView
BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
BOOLEAN_ATTRIBUTES.freeze BOOLEAN_ATTRIBUTES.freeze
TAG_PREFIXES = ["aria", "data", :aria, :data].to_set.freeze ARIA_PREFIXES = ["aria", :aria].to_set.freeze
DATA_PREFIXES = ["data", :data].to_set.freeze
TAG_TYPES = {} TAG_TYPES = {}
TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean) TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
TAG_TYPES.merge! TAG_PREFIXES.index_with(:prefix) TAG_TYPES.merge! DATA_PREFIXES.index_with(:data)
TAG_TYPES.merge! ARIA_PREFIXES.index_with(:aria)
TAG_TYPES.freeze TAG_TYPES.freeze
PRE_CONTENT_STRINGS = Hash.new { "" } PRE_CONTENT_STRINGS = Hash.new { "" }
@ -72,9 +74,18 @@ module ActionView
sep = " " sep = " "
options.each_pair do |key, value| options.each_pair do |key, value|
type = TAG_TYPES[key] type = TAG_TYPES[key]
if type == :prefix && value.is_a?(Hash) if type == :data && value.is_a?(Hash)
value.each_pair do |k, v| value.each_pair do |k, v|
next if v.nil? next if v.nil?
output << sep
output << prefix_tag_option(key, k, v, escape)
end
elsif type == :aria && value.is_a?(Hash)
value.each_pair do |k, v|
next if v.nil?
v = (v.is_a?(Array) || v.is_a?(Hash)) ? safe_join(TagHelper.build_tag_values(v), " ") : v.to_s
output << sep output << sep
output << prefix_tag_option(key, k, v, escape) output << prefix_tag_option(key, k, v, escape)
end end
@ -165,8 +176,8 @@ module ActionView
# tag.input type: 'text', disabled: true # tag.input type: 'text', disabled: true
# # => <input type="text" disabled="disabled"> # # => <input type="text" disabled="disabled">
# #
# HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key # HTML5 <tt>data-*</tt> and <tt>aria-*</tt> attributes can be set with a
# pointing to a hash of sub-attributes. # single +data+ or +aria+ key pointing to a hash of sub-attributes.
# #
# To play nicely with JavaScript conventions, sub-attributes are dasherized. # To play nicely with JavaScript conventions, sub-attributes are dasherized.
# #

View File

@ -441,11 +441,12 @@ class TagHelperTest < ActionView::TestCase
def test_aria_attributes def test_aria_attributes
["aria", :aria].each { |aria| ["aria", :aria].each { |aria|
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />', assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-truthy="true" aria-falsey="false" aria-array="1 2 3" aria-hash="a b" aria-tokens="a b" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
tag("a", aria => { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' }) tag("a", aria => { nil: nil, a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, truthy: true, falsey: false, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { a: true, b: "truthy", falsey: false, nil: nil }, tokens: ["a", { b: true, c: false }], string_with_quotes: 'double"quote"party"' })
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { key: "value" }, string_with_quotes: 'double"quote"party"' })
} }
assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-truthy="true" aria-falsey="false" aria-array="1 2 3" aria-hash="a b" aria-tokens="a b" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
tag.a(aria: { nil: nil, a_float: 3.14, a_big_decimal: BigDecimal("-123.456"), a_number: 1, truthy: true, falsey: false, string: "hello", symbol: :foo, array: [1, 2, 3], hash: { a: true, b: "truthy", falsey: false, nil: nil }, tokens: ["a", { b: true, c: false }], string_with_quotes: 'double"quote"party"' })
end end
def test_link_to_data_nil_equal def test_link_to_data_nil_equal