Skip to content

Commit

Permalink
Action menu avatar item (#2165)
Browse files Browse the repository at this point in the history
Co-authored-by: camertron <camertron@users.noreply.github.com>
  • Loading branch information
camertron and camertron authored Jul 31, 2023
1 parent e017481 commit 1b8ff1b
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 108 deletions.
7 changes: 7 additions & 0 deletions .changeset/itchy-pants-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/view-components': minor
---

Add an accessible avatar item to ActionList, NavList, and ActionMenu

<!-- Changed components: Primer::Alpha::ActionList, Primer::Alpha::ActionMenu, Primer::Alpha::NavList -->
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 31 additions & 2 deletions app/components/primer/alpha/action_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,19 @@ def custom_element_name
# def with_divider(**system_arguments, &block)
# end

# Items. Items can be individual items or dividers. See the documentation for `#with_item` and `#with_divider` respectively for more information.
# @!parse
# # Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
# #
# # @param src [String] The source url of the avatar image.
# # @param username [String] The username associated with the avatar.
# # @param full_name [String] Optional. The user's full name.
# # @param full_name_scheme [Symbol] Optional. How to display the user's full name. <%= one_of(Primer::Alpha::ActionList::Item::DESCRIPTION_SCHEME_OPTIONS) %>
# # @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
# def with_avatar_item(src:, username:, full_name: nil, full_name_scheme: Primer::Alpha::ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, avatar_arguments: {}, **system_arguments, &block)
# end

# Items. Items can be individual items, avatar items, or dividers. See the documentation for `#with_item`, `#with_divider`, and `#with_avatar_item` respectively for more information.
#
renders_many :items, types: {
item: {
Expand All @@ -84,8 +96,25 @@ def custom_element_name
as: :item
},

avatar_item: {
renders: lambda { |src:, username:, full_name: nil, full_name_scheme: Primer::Alpha::ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, avatar_arguments: {}, **system_arguments|
build_item(label: username, description_scheme: full_name_scheme, **system_arguments).tap do |item|
item.with_leading_visual_raw_content do
# no alt text necessary for presentational item
render(Primer::Beta::Avatar.new(src: src, **avatar_arguments, role: :presentation, size: 16))
end

item.with_description_content(full_name) if full_name

will_add_item(item)
end
},

as: :avatar_item
},

divider: {
renders: Divider,
renders: ActionList::Divider,
as: :divider
}
}
Expand Down
23 changes: 17 additions & 6 deletions app/components/primer/alpha/action_list/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,31 @@ class Item < Primer::Component
#
# To render an icon, call the `with_leading_visual_icon` method, which accepts the arguments accepted by <%= link_to_component(Primer::Beta::Octicon) %>.
#
# To render an avatar, call the `with_leading_visual_avatar` method, which accepts the arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
#
# To render an SVG, call the `with_leading_visual_svg` method.
#
# To render custom content, call the `with_leading_visual_content` method and pass a block that returns a string.
renders_one :leading_visual, types: {
icon: Primer::Beta::Octicon,
avatar: ->(**kwargs) { Primer::Beta::Avatar.new(**{ **kwargs, size: 16 }) },
icon: lambda { |**system_arguments, &block|
deny_aria_key(
:label,
"Avoid using `aria-label` on leading visual icons, as they are purely decorative.",
**system_arguments
)

Primer::Beta::Octicon.new(**system_arguments, &block)
},
avatar: lambda { |*|
return unless should_raise_error?

raise "Leading visual avatars are no longer supported. Please use the #with_avatar_item slot instead."
},
svg: lambda { |**system_arguments|
Primer::BaseComponent.new(tag: :svg, width: "16", height: "16", **system_arguments)
},
content: lambda { |**system_arguments|
Primer::BaseComponent.new(tag: :span, **system_arguments)
}
},
raw_content: nil
}

# Used internally.
Expand Down Expand Up @@ -142,7 +153,7 @@ class Item < Primer::Component
# @param size [Symbol] Controls block sizing of the item.
# @param scheme [Symbol] Controls color/style based on behavior.
# @param disabled [Boolean] Disabled items are not clickable and visually dim.
# @param description_scheme [Symbol] Display description inline with label, or block on the next line.
# @param description_scheme [Symbol] Display description inline with label, or block on the next line. <%= one_of(Primer::Alpha::ActionList::Item::DESCRIPTION_SCHEME_OPTIONS) %>
# @param active [Boolean] If the parent list's `select_variant` is set to `:single` or `:multiple`, causes this item to render checked.
# @param on_click [String] JavaScript to execute when the item is clicked.
# @param id [String] Used internally.
Expand Down
12 changes: 12 additions & 0 deletions app/components/primer/alpha/action_menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,18 @@ def with_divider(**system_arguments, &block)
@list.with_divider(**system_arguments, &block)
end

# Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
#
# @param src [String] The source url of the avatar image.
# @param username [String] The username associated with the avatar.
# @param full_name [String] Optional. The user's full name.
# @param full_name_scheme [Symbol] Optional. How to display the user's full name.
# @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
def with_avatar_item(**system_arguments, &block)
@list.with_avatar_item(**system_arguments, &block)
end

private

def before_render
Expand Down
95 changes: 62 additions & 33 deletions app/components/primer/alpha/action_menu/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,67 @@ class List < Primer::Alpha::ActionList
# @param data [Hash] When the menu is used as a form input (see the <%= link_to_component(Primer::Alpha::ActionMenu) %> docs), the label is submitted to the server by default. However, if the `data: { value: }` or `"data-value":` attribute is provided, it will be sent to the server instead.
# @param system_arguments [Hash] The same arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
def with_item(data: {}, **system_arguments, &block)
system_arguments = organize_arguments(data: data, **system_arguments)

super(**system_arguments) do |item|
evaluate_block(item, &block)
end
end

# Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
#
# @param src [String] The source url of the avatar image.
# @param username [String] The username associated with the avatar.
# @param full_name [String] Optional. The user's full name.
# @param full_name_scheme [Symbol] Optional. How to display the user's full name. <%= one_of(Primer::Alpha::ActionList::Item::DESCRIPTION_SCHEME_OPTIONS) %>
# @param data [Hash] When the menu is used as a form input (see the <%= link_to_component(Primer::Alpha::ActionMenu) %> docs), the label is submitted to the server by default. However, if the `data: { value: }` or `"data-value":` attribute is provided, it will be sent to the server instead.
# @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
# @param system_arguments [Hash] The same arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Item) %>.
def with_avatar_item(src:, username:, full_name: nil, full_name_scheme: Primer::Alpha::ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, data: {}, avatar_arguments: {}, **system_arguments, &block)
system_arguments = organize_arguments(data: data, **system_arguments)

super(src: src, username: username, full_name: full_name, full_name_scheme: full_name_scheme, avatar_arguments: avatar_arguments, **system_arguments) do |item|
evaluate_block(item, &block)
end
end

# @param menu_id [String] ID of the parent menu.
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>
def initialize(menu_id:, **system_arguments, &block)
@menu_id = menu_id

system_arguments[:aria] = merge_aria(
system_arguments,
{ aria: { labelledby: "#{@menu_id}-button" } }
)

system_arguments[:role] = :menu
system_arguments[:scheme] = :inset
system_arguments[:id] = "#{@menu_id}-list"

super(**system_arguments, &block)
end

private

def evaluate_block(*args, &block)
# Prevent double renders by using the capture method on the component
# that originally received the block.
#
# Handle blocks that originate from C code such as `&:method` by checking
# source_location. Such blocks don't allow access to their receiver.
return unless block&.source_location

block_context = block.binding.receiver

if block_context.class < ActionView::Base
block_context.capture(*args, &block)
else
capture(*args, &block)
end
end

def organize_arguments(data: {}, **system_arguments)
content_arguments = system_arguments.delete(:content_arguments) || {}

# rubocop:disable Style/IfUnlessModifier
Expand Down Expand Up @@ -54,39 +115,7 @@ def with_item(data: {}, **system_arguments, &block)
content_arguments[:disabled] = "" if content_arguments[:tag] == :button
end

super(data: data, **system_arguments, content_arguments: content_arguments) do |item|
# Prevent double renders by using the capture method on the component
# that originally received the block.
#
# Handle blocks that originate from C code such as `&:method` by checking
# source_location. Such blocks don't allow access to their receiver.
if block&.source_location
block_context = block.binding.receiver

if block_context.class < ActionView::Base
block_context.capture(item, &block)
else
capture(item, &block)
end
end
end
end

# @param menu_id [String] ID of the parent menu.
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList) %>
def initialize(menu_id:, **system_arguments, &block)
@menu_id = menu_id

system_arguments[:aria] = merge_aria(
system_arguments,
{ aria: { labelledby: "#{@menu_id}-button" } }
)

system_arguments[:role] = :menu
system_arguments[:scheme] = :inset
system_arguments[:id] = "#{@menu_id}-list"

super(**system_arguments, &block)
{ data: data, **system_arguments, content_arguments: content_arguments }
end
end
end
Expand Down
57 changes: 47 additions & 10 deletions app/components/primer/alpha/nav_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,22 @@ def self.custom_element_name
# @!parse
# # Adds an item to the list.
# #
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::NavList::Item) %>.
# def with_group(**system_arguments, &block)
# # @param component_klass [Class] The component class to use. Defaults to `Primer::Alpha::NavList::Item`.
# # @param system_arguments [Hash] The arguments accepted by the `component_klass` class.
# def with_item(**system_arguments, &block)
# end

# @!parse
# # Adds an avatar item to the list. Avatar items are a convenient way to accessibly add an item with a leading avatar image.
# #
# # @param src [String] The source url of the avatar image.
# # @param username [String] The username associated with the avatar.
# # @param full_name [String] Optional. The user's full name.
# # @param full_name_scheme [Symbol] Optional. How to display the user's full name. <%= one_of(Primer::Alpha::ActionList::Item::DESCRIPTION_SCHEME_OPTIONS) %>
# # @param component_klass [Class] The component class to use. Defaults to `Primer::Alpha::NavList::Item`.
# # @param avatar_arguments [Hash] Optional. The arguments accepted by <%= link_to_component(Primer::Beta::Avatar) %>.
# # @param system_arguments [Hash] The arguments accepted by the `component_klass` class.
# def with_avatar_item(src:, username:, full_name: nil, full_name_scheme: Primer::Alpha::ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, avatar_arguments: {}, **system_arguments, &block)
# end

# @!parse
Expand All @@ -44,7 +58,7 @@ def self.custom_element_name
# @!parse
# # Adds a divider to the list. Dividers visually separate items and groups.
# #
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Divider) %>.
# # @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::NavList::Divider) %>.
# def with_divider(**system_arguments, &block)
# end

Expand All @@ -53,11 +67,8 @@ def self.custom_element_name
renders_many :items, types: {
item: {
renders: lambda { |component_klass: Primer::Alpha::NavList::Item, **system_arguments, &block|
# dummy group just so we have something to pass as the list: argument below
@top_level_group ||= Primer::Alpha::NavList::Group.new(selected_item_id: @selected_item_id)

component_klass.new(
list: @top_level_group,
list: top_level_group,
selected_item_id: @selected_item_id,
**system_arguments,
&block
Expand All @@ -67,6 +78,29 @@ def self.custom_element_name
as: :item
},

avatar_item: {
renders: lambda { |src:, username:, full_name: nil, full_name_scheme: Primer::Alpha::ActionList::Item::DEFAULT_DESCRIPTION_SCHEME, component_klass: Primer::Alpha::NavList::Item, avatar_arguments: {}, **system_arguments|
item = component_klass.new(
list: top_level_group,
selected_item_id: @selected_item_id,
label: username,
description_scheme: full_name_scheme,
**system_arguments
)

item.with_leading_visual_raw_content do
# no alt text necessary
render(Primer::Beta::Avatar.new(src: src, **avatar_arguments, role: :presentation, size: 16))
end

item.with_description_content(full_name) if full_name

item
},

as: :avatar_item
},

divider: {
renders: Primer::Alpha::NavList::Divider,
as: :divider
Expand Down Expand Up @@ -102,9 +136,7 @@ def self.custom_element_name
# <%= render(Primer::Alpha::NavList.new(aria: { label: "Settings" }, selected_item_id: :personal_info)) do |component| %>
# <% component.with_group do |group| %>
# <% group.with_heading(title: "Account Settings") %>
# <% group.with_item(label: "Personal Information", selected_by_ids: :personal_info, href: "/account/info") do |item| %>
# <% item.with_leading_visual_avatar(src: "https://github.com/github.png", alt: "GitHub") %>
# <% end %>
# <% group.with_avatar_item(src: "https://github.com/github.png", username: "person", selected_by_ids: :personal_info, href: "/account/info") %>
# <% group.with_item(label: "Notifications", selected_by_ids: :notifications, href: "/account/notifications") do |item| %>
# <% item.with_leading_visual_icon(icon: :bell) %>
# <% item.with_trailing_visual_counter(count: 15) %>
Expand Down Expand Up @@ -212,6 +244,11 @@ def divider?(item)
def kind(item)
item.respond_to?(:kind) ? item.kind : :item
end

def top_level_group
# dummy group for the list: argument in the item slot above
@top_level_group ||= Primer::Alpha::NavList::Group.new(selected_item_id: @selected_item_id)
end
end
end
end
2 changes: 1 addition & 1 deletion app/components/primer/beta/avatar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Avatar < Primer::Component
# @param shape [Symbol] Shape of the avatar. <%= one_of(Primer::Beta::Avatar::SHAPE_OPTIONS) %>
# @param href [String] The URL to link to. If used, component will be wrapped by an `<a>` tag.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(src:, alt:, size: DEFAULT_SIZE, shape: DEFAULT_SHAPE, href: nil, **system_arguments)
def initialize(src:, alt: nil, size: DEFAULT_SIZE, shape: DEFAULT_SHAPE, href: nil, **system_arguments)
@href = href
@system_arguments = deny_tag_argument(**system_arguments)
@system_arguments[:tag] = :img
Expand Down
Loading

0 comments on commit 1b8ff1b

Please sign in to comment.