diff --git a/.changeset/short-ligers-smile.md b/.changeset/short-ligers-smile.md new file mode 100644 index 0000000000..c96f3f344d --- /dev/null +++ b/.changeset/short-ligers-smile.md @@ -0,0 +1,5 @@ +--- +'@primer/view-components': patch +--- + +Update ActionList such that if role='listbox' is passed in the items render as options diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/default.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/default.png new file mode 100644 index 0000000000..901a20a762 Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/default.png differ diff --git a/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/focused.png b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/focused.png new file mode 100644 index 0000000000..21b4d0861d Binary files /dev/null and b/.playwright/screenshots/snapshots.test.ts-snapshots/primer/alpha/action_list/listbox/focused.png differ diff --git a/app/components/primer/alpha/action_list.rb b/app/components/primer/alpha/action_list.rb index 7d4ed32943..becd31a75c 100644 --- a/app/components/primer/alpha/action_list.rb +++ b/app/components/primer/alpha/action_list.rb @@ -16,7 +16,7 @@ class ActionList < Primer::Component MENU_ROLE = :menu DEFAULT_MENU_ITEM_ROLE = :menuitem - + LIST_BOX_ITEM_ROLE = :option DEFAULT_SCHEME = :full SCHEME_MAPPINGS = { DEFAULT_SCHEME => nil, @@ -24,6 +24,11 @@ class ActionList < Primer::Component }.freeze SCHEME_OPTIONS = SCHEME_MAPPINGS.keys.freeze + DEFAULT_ARIA_SELECTION_VARIANT = :checked + ARIA_SELECTION_VARIANT_OPTIONS = [ + :selected, + DEFAULT_ARIA_SELECTION_VARIANT, + ].freeze DEFAULT_SELECT_VARIANT = :none SELECT_VARIANT_OPTIONS = [ :single, @@ -115,7 +120,7 @@ def custom_element_name } } - attr_reader :id, :select_variant, :role + attr_reader :id, :select_variant, :role, :aria_selection_variant # @param id [String] HTML ID value. # @param role [Boolean] ARIA role describing the function of the list. listbox and menu are a common values. @@ -123,6 +128,7 @@ def custom_element_name # @param scheme [Symbol] <%= one_of(Primer::Alpha::ActionList::SCHEME_OPTIONS) %> `inset` children are offset (vertically and horizontally) from list edges. `full` (default) children are flush (vertically and horizontally) with list edges. # @param show_dividers [Boolean] Display a divider above each item in the list when it does not follow a header or divider. # @param select_variant [Symbol] How items may be selected in the list. <%= one_of(Primer::Alpha::ActionList::SELECT_VARIANT_OPTIONS) %> + # @param aria_selection_variant [Symbol] Specifies which aria selection to use. <%= one_of(Primer::Alpha::ActionList::ARIA_SELECTION_VARIANT_OPTIONS) %? # @param form_arguments [Hash] Allows an `ActionList` to act as a select list in multi- and single-select modes. Pass the `builder:` and `name:` options to this hash. `builder:` should be an instance of `ActionView::Helpers::FormBuilder`, which are created by the standard Rails `#form_with` and `#form_for` helpers. The `name:` option is the desired name of the field that will be included in the params sent to the server on form submission. *NOTE*: Consider using an <%= link_to_component(Primer::Alpha::ActionMenu) %> instead of using this feature directly. # @param system_arguments [Hash] <%= link_to_system_arguments_docs %> def initialize( @@ -131,6 +137,7 @@ def initialize( item_classes: nil, scheme: DEFAULT_SCHEME, show_dividers: false, + aria_selection_variant: DEFAULT_ARIA_SELECTION_VARIANT, select_variant: DEFAULT_SELECT_VARIANT, form_arguments: {}, **system_arguments @@ -143,6 +150,7 @@ def initialize( @scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME) @show_dividers = show_dividers @select_variant = select_variant + @aria_selection_variant = aria_selection_variant @system_arguments[:classes] = class_names( SCHEME_MAPPINGS[@scheme], system_arguments[:classes], @@ -229,6 +237,10 @@ def allows_selection? single_select? || multi_select? end + def acts_as_listbox? + @system_arguments[:role] == "listbox" + end + def acts_as_menu? @system_arguments[:role] == :menu || @system_arguments[:role] == :group end diff --git a/app/components/primer/alpha/action_list/item.rb b/app/components/primer/alpha/action_list/item.rb index fd10fdc12a..74e9b8095c 100644 --- a/app/components/primer/alpha/action_list/item.rb +++ b/app/components/primer/alpha/action_list/item.rb @@ -271,13 +271,15 @@ def initialize( end @content_arguments[:role] = role || - if @list.allows_selection? + if @list.acts_as_listbox? + ActionList::LIST_BOX_ITEM_ROLE + elsif @list.allows_selection? ActionList::SELECT_VARIANT_ROLE_MAP[@list.select_variant] elsif @list.acts_as_menu? ActionList::DEFAULT_MENU_ITEM_ROLE end - @system_arguments[:role] = @list.acts_as_menu? ? :none : nil + @system_arguments[:role] = @list.acts_as_menu? || @list.acts_as_listbox? ? :none : nil @description_wrapper_arguments = { classes: class_names( @@ -293,7 +295,7 @@ def before_render if @list.allows_selection? @content_arguments[:aria] = merge_aria( @content_arguments, - { aria: { checked: active? } } + { aria: @list.aria_selection_variant == :selected ? {selected: active?} : { checked: active? } } ) end diff --git a/previews/primer/alpha/action_list_preview.rb b/previews/primer/alpha/action_list_preview.rb index a26a0a9b5c..2e3c073d0e 100644 --- a/previews/primer/alpha/action_list_preview.rb +++ b/previews/primer/alpha/action_list_preview.rb @@ -519,6 +519,25 @@ def long_label_show_tooltip_with_truncate_label(truncate_label: :none) end end end + + # @label Listbox + def listbox( + role: "listbox", + aria_selection_variant: :selected, + scheme: Primer::Alpha::ActionList::DEFAULT_SCHEME, + show_dividers: false + ) + render(Primer::Alpha::ActionList.new( + role: role, + scheme: scheme, + show_dividers: show_dividers + )) do |component| + component.with_heading(title: "Action List") + component.with_item(label: "Item one", href: "/", active: true) + component.with_item(label: "Item two", href: "/") + component.with_item(label: "Item three", href: "/") + end + end end end end diff --git a/static/constants.json b/static/constants.json index 1f313705e5..af7c6e8271 100644 --- a/static/constants.json +++ b/static/constants.json @@ -19,7 +19,9 @@ "Primer::Alpha::ActionBar::Item": { }, "Primer::Alpha::ActionList": { + "LIST_BOX_ITEM_ROLE": "option", "DEFAULT_MENU_ITEM_ROLE": "menuitem", + "DEFAULT_ARIA_SELECTION_VARIANT": "checked", "DEFAULT_ROLE": "list", "DEFAULT_SCHEME": "full", "DEFAULT_SELECT_VARIANT": "none", @@ -42,6 +44,10 @@ "multiple_checkbox", "none" ], + "ARIA_SELECTION_VARIANT_OPTIONS": [ + "selected", + "checked" + ], "SELECT_VARIANT_ROLE_MAP": { "single": "menuitemradio", "multiple": "menuitemcheckbox", diff --git a/test/components/alpha/action_list_test.rb b/test/components/alpha/action_list_test.rb index 99d3b7fa3f..2f1fe85aed 100644 --- a/test/components/alpha/action_list_test.rb +++ b/test/components/alpha/action_list_test.rb @@ -182,6 +182,16 @@ def test_uses_correct_menu_and_item_roles_when_multi_select end end + def test_uses_correct_item_role_when_role_set_to_option + render_inline(Primer::Alpha::ActionList.new(aria: { label: "List" }, role: "listbox")) do |component| + component.with_item(label: "Item 1") + end + + assert_selector("ul.ActionListWrap[role=listbox]") do |list| + list.assert_selector("li.ActionListItem button[role=option]") + end + end + def test_uses_correct_item_role_when_role_set_to_menu render_inline(Primer::Alpha::ActionList.new(aria: { label: "List" }, role: :menu)) do |component| component.with_item(label: "Item 1")