Skip to content

Commit

Permalink
Add group support to ActionMenu (#2340)
Browse files Browse the repository at this point in the history
Co-authored-by: camertron <[email protected]>
  • Loading branch information
camertron and camertron authored Nov 21, 2023
1 parent 2c59c33 commit b8d0540
Show file tree
Hide file tree
Showing 39 changed files with 1,381 additions and 823 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-wasps-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': minor
---

Add group support to ActionMenu
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.
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.
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.
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.
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.
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.
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.
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.
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.
9 changes: 5 additions & 4 deletions app/components/primer/alpha/action_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ def custom_element_name

# Heading text rendered above the list of items.
#
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionList::Heading) %>.
renders_one :heading, lambda { |**system_arguments|
Heading.new(**system_arguments)
# @param component_klass [Class] The class to use instead of the default <%= link_to_component(Primer::Alpha::ActionList::Heading) %>.
# @param system_arguments [Hash] The arguments accepted by `component_klass`.
renders_one :heading, lambda { |component_klass: Primer::Alpha::ActionList::Heading, **system_arguments|
component_klass.new(**system_arguments)
}

# @!parse
Expand Down Expand Up @@ -229,7 +230,7 @@ def allows_selection?
end

def acts_as_menu?
@system_arguments[:role] == :menu
@system_arguments[:role] == :menu || @system_arguments[:role] == :group
end

def required_form_arguments_given?
Expand Down
6 changes: 5 additions & 1 deletion app/components/primer/alpha/action_menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def initialize(

@system_arguments[:preload] = true if @src.present? && preload?

select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)
@select_variant = fetch_or_fallback(SELECT_VARIANT_OPTIONS, select_variant, DEFAULT_SELECT_VARIANT)

@system_arguments[:tag] = :"action-menu"
@system_arguments[:"data-select-variant"] = select_variant
Expand Down Expand Up @@ -243,6 +243,10 @@ def with_avatar_item(**system_arguments, &block)
@list.with_avatar_item(**system_arguments, &block)
end

def with_group(**system_arguments, &block)
@list.with_group(**system_arguments, &block)
end

private

def before_render
Expand Down
23 changes: 23 additions & 0 deletions app/components/primer/alpha/action_menu/group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# typed: true
# frozen_string_literal: true

module Primer
module Alpha
class ActionMenu
# This component is part of <%= link_to_component(Primer::Alpha::ActionMenu) %> and should not be
# used as a standalone component.
class Group < Primer::Alpha::ActionList
# Heading text rendered above the list of items.
#
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionMenu::Heading) %>.
def with_heading(**system_arguments, &block)
super(component_klass: Primer::Alpha::ActionMenu::Heading, **system_arguments, &block)
end

def with_divider
raise "ActionMenu groups cannot have dividers"
end
end
end
end
end
17 changes: 17 additions & 0 deletions app/components/primer/alpha/action_menu/heading.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Primer
module Alpha
class ActionMenu
# Heading used to describe groups within an action menu.
class Heading < Primer::Alpha::ActionList::Heading
def initialize(**)
super

# Headings don't make sense in a menu context, so use div instead
@tag = :div
end
end
end
end
end
1 change: 1 addition & 0 deletions app/components/primer/alpha/action_menu/list.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= render(@list) %>
113 changes: 62 additions & 51 deletions app/components/primer/alpha/action_menu/list.rb
Original file line number Diff line number Diff line change
@@ -1,77 +1,88 @@
# typed: true
# frozen_string_literal: true

module Primer
module Alpha
class ActionMenu
# This component is part of <%= link_to_component(Primer::Alpha::ActionMenu) %> and should not be
# used as a standalone component.
class List < Primer::Alpha::ActionList
class List < Primer::Component
DEFAULT_ITEM_TAG = :button
ITEM_TAG_OPTIONS = [:a, :"clipboard-copy", DEFAULT_ITEM_TAG].freeze

# Adds a new item to the list.
#
# @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] These arguments are forwarded to <%= link_to_component(Primer::Alpha::ActionList::Item) %>, or whatever class is passed as the `component_klass` argument.
def with_item(data: {}, **system_arguments, &block)
system_arguments = organize_arguments(data: data, **system_arguments)
attr_reader :items

super(**system_arguments) do |item|
evaluate_block(item, &block)
end
end
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Alpha::ActionMenu::List) %>
def initialize(**system_arguments)
@items = []
@has_group = false

# 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] These arguments are forwarded to <%= link_to_component(Primer::Alpha::ActionList::Item) %>, or whatever class is passed as the `component_klass` argument.
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
@list = Primer::Alpha::ActionMenu::ListWrapper.new(**system_arguments)
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
def with_group(**system_arguments, &block)
@has_group = true

system_arguments[:aria] = merge_aria(
system_arguments,
{ aria: { labelledby: "#{@menu_id}-button" } }
)
@items << {
type: :group,
kwargs: system_arguments,
block: block
}
end

system_arguments[:role] = :menu
system_arguments[:scheme] = :inset
system_arguments[:id] = "#{@menu_id}-list"
def with_item(**system_arguments, &block)
@items << {
type: :item,
kwargs: organize_arguments(**system_arguments),
block: block
}
end

super(**system_arguments, &block)
def with_avatar_item(**system_arguments, &block)
@items << {
type: :avatar_item,
kwargs: organize_arguments(**system_arguments),
block: block
}
end

def with_divider(**system_arguments, &block)
@items << {
type: :divider,
kwargs: system_arguments,
block: 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
def contains_group?
@has_group
end

block_context = block.binding.receiver
def before_render
content

@items.each do |item|
case item[:type]
when :divider, :group
add_item(item, to: @list)
else
if contains_group?
@list.with_group do |group|
add_item(item, to: group)
end
else
add_item(item, to: @list)
end
end
end
end

if block_context.class < ActionView::Base
block_context.capture(*args, &block)
else
capture(*args, &block)
def add_item(item, to:)
parent = to
mtd = :"with_#{item[:type]}"
parent.send(mtd, **item[:kwargs]) do |item_instance|
evaluate_block(item_instance, &item[:block])
end
end

Expand Down
40 changes: 40 additions & 0 deletions app/components/primer/alpha/action_menu/list_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Primer
module Alpha
class ActionMenu
# This component is part of <%= link_to_component(Primer::Alpha::ActionMenu) %> and should not be
# used as a standalone component.
class ListWrapper < Primer::Alpha::ActionList
add_polymorphic_slot_type(
slot_name: :items,
type: :group,
callable: lambda { |**system_arguments|
Primer::Alpha::ActionMenu::Group.new(
**system_arguments,
role: :group,
select_variant: @select_variant
)
}
)

# @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)
@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)
end
end
end
end
end
3 changes: 3 additions & 0 deletions app/components/primer/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class Component < ViewComponent::Base
include ViewComponent::PolymorphicSlots
end

include ExperimentalRenderHelpers
include ExperimentalSlotHelpers

include AttributesHelper
include ClassNameHelper
include FetchOrFallbackHelper
Expand Down
32 changes: 32 additions & 0 deletions app/lib/primer/experimental_render_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Primer
# :nodoc:
module ExperimentalRenderHelpers
def self.included(base)
base.include(InstanceMethods)
end

# :nodoc:
module InstanceMethods
def evaluate_block(*args, **kwargs, &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

return yield(*args, **kwargs) if block&.source_location.nil?

block_context = block.binding.receiver

if block_context.class < ActionView::Base
block_context.capture(*args, &block)
else
capture(*args, &block)
end
end
end
end
end
30 changes: 30 additions & 0 deletions app/lib/primer/experimental_slot_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Primer
# :nodoc:
module ExperimentalSlotHelpers
def self.included(base)
base.extend(ClassMethods)
end

# :nodoc:
module ClassMethods
def add_polymorphic_slot_type(slot_name:, type:, callable:)
slot_def = registered_slots[slot_name]
raise "Unknown slot '#{slot_name}'" unless slot_def

poly_def = define_slot(
type,
collection: slot_def[:collection],
callable: callable
)

registered_slots[slot_name][:renderable_hash][type] = poly_def

define_method(:"with_#{type}") do |**system_arguments, &block|
set_slot(slot_name, poly_def, **system_arguments, &block)
end
end
end
end
end
Loading

0 comments on commit b8d0540

Please sign in to comment.