Skip to content

Commit

Permalink
ActionMenu upstream from Experimental (#1830)
Browse files Browse the repository at this point in the history
Co-authored-by: Jon Rohan <yes@jonrohan.codes>
Co-authored-by: Keith Cirkel <keithamus@users.noreply.github.com>
Co-authored-by: Cameron Dutro <camertron@gmail.com>
Co-authored-by: Jon Rohan <rohan@github.com>
  • Loading branch information
5 people authored Apr 5, 2023
1 parent 7142fad commit d7e4f5d
Show file tree
Hide file tree
Showing 46 changed files with 2,106 additions and 222 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-fans-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': patch
---

ActionMenu upstream from Experimental
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.
39 changes: 19 additions & 20 deletions app/components/primer/alpha/action_list.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@

@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkIn 200ms cubic-bezier(0.11, 0, 0.5, 0) forwards;
@keyframes checkmarkIn {
from {
clip-path: inset(16px 0 0 0);
}

to {
clip-path: inset(0 0 0 0);
}
}
}
}

Expand Down Expand Up @@ -206,6 +215,15 @@

@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkOut 200ms cubic-bezier(0.11, 0, 0.5, 0) forwards;
@keyframes checkmarkOut {
from {
clip-path: inset(0 0 0 0);
}

to {
clip-path: inset(16px 0 0 0);
}
}
}
}

Expand All @@ -224,26 +242,6 @@
}
}

@keyframes checkmarkIn {
from {
clip-path: inset(16px 0 0 0);
}

to {
clip-path: inset(0 0 0 0);
}
}

@keyframes checkmarkOut {
from {
clip-path: inset(0 0 0 0);
}

to {
clip-path: inset(16px 0 0 0);
}
}

/* Autocomplete [aria-selected] items */

&[aria-selected='true'] {
Expand Down Expand Up @@ -300,6 +298,7 @@

/* disabled */

&.ActionListItem--disabled,
&[aria-disabled='true'] {
& .ActionListContent {
& .ActionListItem-label,
Expand Down
58 changes: 53 additions & 5 deletions app/components/primer/alpha/action_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,30 @@ class ActionList < Primer::Component

DEFAULT_ROLE = :list

MENU_ROLE = :menu
DEFAULT_MENU_ITEM_ROLE = :menuitem

DEFAULT_SCHEME = :full
SCHEME_MAPPINGS = {
DEFAULT_SCHEME => nil,
:inset => "ActionListWrap--inset"
}.freeze
SCHEME_OPTIONS = SCHEME_MAPPINGS.keys.freeze

DEFAULT_SELECT_VARIANT = :none
SELECT_VARIANT_OPTIONS = [
:single,
:multiple,
:multiple_checkbox,
DEFAULT_SELECT_VARIANT
].freeze

SELECT_VARIANT_ROLE_MAP = {
single: :menuitemradio,
multiple: :menuitemcheckbox,
multiple_checkbox: :menuitemcheckbox
}.freeze

# :nocov:
# @private
def self.custom_element_name
Expand Down Expand Up @@ -57,33 +74,42 @@ def with_divider(**system_arguments, &block)
set_slot(:items, { renderable: Divider, collection: true }, **system_arguments, &block)
end

attr_reader :select_variant, :role

# @param id [String] HTML ID value.
# @param role [Boolean] ARIA role describing the function of the list. listbox and menu are a common values.
# @param item_classes [String] Additional CSS classes to attach to items.
# @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 system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(
role: DEFAULT_ROLE,
id: self.class.generate_id,
role: nil,
item_classes: nil,
scheme: DEFAULT_SCHEME,
show_dividers: false,
select_variant: DEFAULT_SELECT_VARIANT,
**system_arguments
)
@id = self.class.generate_id

@system_arguments = system_arguments
@id = id
@system_arguments[:id] = @id
@system_arguments[:tag] = :ul
@system_arguments[:role] = role
@item_classes = item_classes
@scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)
@show_dividers = show_dividers
@select_variant = select_variant
@system_arguments[:classes] = class_names(
SCHEME_MAPPINGS[@scheme],
system_arguments[:classes],
"ActionListWrap",
"ActionListWrap--divided" => @show_dividers
)

@role = role || allows_selection? ? MENU_ROLE : DEFAULT_ROLE
@system_arguments[:role] = @role

@list_wrapper_arguments = {}
end

Expand All @@ -93,7 +119,7 @@ def before_render
aria_labelledby = aria(:labelledby, @system_arguments)

if heading.present?
@system_arguments[:"aria-labelledby"] = @id
@system_arguments[:"aria-labelledby"] = heading.id unless aria_labelledby
raise ArgumentError, "An aria-label should not be provided if a heading is present" if aria_label.present?
elsif aria_label.blank? && aria_labelledby.blank?
raise ArgumentError, "An aria-label, aria-labelledby, or heading must be provided"
Expand All @@ -102,6 +128,12 @@ def before_render

# @private
def build_item(**system_arguments)
# rubocop:disable Style/IfUnlessModifier
if single_select? && system_arguments[:active] && items.count(&:active?).positive?
raise ArgumentError, "only a single item may be active when select_variant is set to :single"
end
# rubocop:enable Style/IfUnlessModifier

system_arguments[:classes] = class_names(
@item_classes,
system_arguments[:classes]
Expand All @@ -110,6 +142,22 @@ def build_item(**system_arguments)
ActionList::Item.new(list: self, **system_arguments)
end

def single_select?
select_variant == :single
end

def multi_select?
select_variant == :multiple || select_variant == :multiple_checkbox
end

def allows_selection?
single_select? || multi_select?
end

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

# @private
def will_add_item(_item); end
end
Expand Down
2 changes: 1 addition & 1 deletion app/components/primer/alpha/action_list/heading.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= render(Primer::BaseComponent.new(tag: :div, **@system_arguments)) do %>
<%= render(Primer::BaseComponent.new(tag: @tag, classes: "ActionList-sectionDivider-title", id: @list_id)) do %>
<%= render(Primer::BaseComponent.new(tag: @tag, classes: "ActionList-sectionDivider-title", id: @id)) do %>
<%= @title %>
<% end %>
<%= render Primer::ConditionalWrapper.new(condition: @subtitle.present?, tag: :span, classes: "ActionListItem-description") do %>
Expand Down
8 changes: 5 additions & 3 deletions app/components/primer/alpha/action_list/heading.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ class Heading < Primer::Component
HEADING_MAX = 6
HEADING_LEVELS = (HEADING_MIN..HEADING_MAX).to_a.freeze

# @param list_id [String] The unique identifier of the sub list the heading belongs to. Used internally.
attr_reader :id

# @param id [String] The group's identifier (ID attribute in HTML).
# @param title [String] Sub list title.
# @param heading_level [Integer] Heading level. Level 2 results in an `<h2>` tag, level 3 an `<h3>` tag, etc.
# @param subtitle [String] Optional sub list description.
# @param scheme [Symbol] Display a background color if scheme is `filled`.
# @param tag [Integer] Semantic tag for the heading.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(list_id:, title:, heading_level: 3, scheme: DEFAULT_SCHEME, subtitle: nil, **system_arguments)
def initialize(title:, id: self.class.generate_id, heading_level: 3, scheme: DEFAULT_SCHEME, subtitle: nil, **system_arguments)
raise "Heading level must be between #{HEADING_MIN} and #{HEADING_MAX}" unless HEADING_LEVELS.include?(heading_level)

@id = id
@heading_level = heading_level
@tag = :"h#{heading_level}"
@system_arguments = deny_tag_argument(**system_arguments)
@list_id = list_id
@title = title
@subtitle = subtitle
@scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme, DEFAULT_SCHEME)
Expand Down
9 changes: 9 additions & 0 deletions app/components/primer/alpha/action_list/item.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
<%= private_leading_action_icon %>
</span>
<% end %>
<% if list.select_variant == :single || list.select_variant == :multiple %>
<span class="ActionListItem-visual ActionListItem-action--leading">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" class="ActionListItem-singleSelectCheckmark"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>
</span>
<% elsif list.select_variant == :multiple_checkbox %>
<span class="ActionListItem-visual ActionListItem-action--leading">
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" class="ActionListItem-multiSelectIcon"><rect x="2" y="2" width="12" height="12" rx="4" class="ActionListItem-multiSelectIconRect"></rect><path fill-rule="evenodd" d="M4.03231 8.69862C3.84775 8.20646 4.49385 7.77554 4.95539 7.77554C5.41693 7.77554 6.80154 9.85246 6.80154 9.85246C6.80154 9.85246 10.2631 4.314 10.4938 4.08323C10.7246 3.85246 11.8785 4.08323 11.4169 5.00631C11.0081 5.82388 7.26308 11.4678 7.26308 11.4678C7.26308 11.4678 6.80154 12.1602 6.34 11.4678C5.87846 10.7755 4.21687 9.19077 4.03231 8.69862Z" class="ActionListItem-multiSelectCheckmark"></path></svg>
</span>
<% end %>
<% if leading_visual %>
<span class="ActionListItem-visual ActionListItem-visual--leading">
<%= leading_visual %>
Expand Down
39 changes: 30 additions & 9 deletions app/components/primer/alpha/action_list/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class Item < Primer::Component
#
# 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,
icon: lambda { |**system_arguments|
Primer::Beta::Octicon.new(classes: "ActionListItem-visual ActionListItem-visual--leading", **system_arguments)
},
avatar: ->(**kwargs) { Primer::Beta::Avatar.new(**{ **kwargs, size: 16 }) },
svg: lambda { |**system_arguments|
Primer::BaseComponent.new(tag: :svg, width: "16", height: "16", **system_arguments)
Expand Down Expand Up @@ -87,6 +89,7 @@ class Item < Primer::Component
# @param system_arguments [Hash] The arguments accepted by <%= link_to_component(Primer::Beta::IconButton) %>.
renders_one :trailing_action, lambda { |**system_arguments|
Primer::Beta::IconButton.new(
scheme: :invisible,
classes: class_names(
system_arguments[:classes],
"ActionListItem-trailingAction"
Expand Down Expand Up @@ -141,7 +144,7 @@ class Item < Primer::Component
# @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 active [Boolean] Sets an active state on navigational items.
# @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.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
Expand Down Expand Up @@ -184,13 +187,11 @@ def initialize(
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
SCHEME_MAPPINGS[@scheme],
"ActionListItem"
"ActionListItem",
"ActionListItem--disabled" => @disabled
)

@system_arguments[:role] = role if role

@system_arguments[:aria] ||= {}
@system_arguments[:aria][:disabled] = "true" if @disabled
@system_arguments[:role] = :none

@system_arguments[:data] ||= {}
@system_arguments[:data][:targets] = "#{list_class.custom_element_name}.items"
Expand Down Expand Up @@ -222,6 +223,20 @@ def initialize(
end
end

if @disabled
@content_arguments[:aria] ||= merge_aria(
@content_arguments,
{ aria: { disabled: "true" } }
)
end

@content_arguments[:role] = role ||
if @list.allows_selection?
ActionList::SELECT_VARIANT_ROLE_MAP[@list.select_variant]
elsif @list.acts_as_menu?
ActionList::DEFAULT_MENU_ITEM_ROLE
end

@description_wrapper_arguments = {
classes: class_names(
"ActionListItem-descriptionWrap",
Expand All @@ -233,10 +248,16 @@ def initialize(
private

def before_render
if @list.allows_selection?
@system_arguments[:aria] = merge_aria(
@system_arguments,
{ aria: { checked: active? } }
)
end

@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"ActionListItem--withActions" => trailing_action.present?,
"ActionListItem--navActive" => active?
"ActionListItem--withActions" => trailing_action.present?
)

return unless leading_visual
Expand Down
26 changes: 26 additions & 0 deletions app/components/primer/alpha/action_menu.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<%= render Primer::BaseComponent.new(**@system_arguments) do %>
<focus-group direction="vertical" mnemonics retain>
<%= render(@overlay) do |overlay| %>
<% if @src.present? %>
<include-fragment src="<%= @src %>" loading="<%= preload? ? "eager" : "lazy" %>" data-target="action-menu.includeFragment">
<%= render(Primer::Alpha::ActionMenu::List.new(id: "#{@menu_id}-list", menu_id: @menu_id)) do |list| %>
<% list.with_item(
aria: { disabled: true },
content_arguments: {
display: :flex,
align_items: :center,
justify_content: :center,
text_align: :center,
autofocus: true
}
) do %>
<%= render Primer::Beta::Spinner.new(aria: { label: "Loading content..." }) %>
<% end %>
<% end %>
</include-fragment>
<% else %>
<%= render(@list) %>
<% end %>
<% end %>
</focus-group>
<% end %>
Loading

0 comments on commit d7e4f5d

Please sign in to comment.