Skip to content

Commit

Permalink
Merge branch 'main' into fix_menus_inside_dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
camertron authored Oct 18, 2023
2 parents 529f2ad + 725bbd9 commit b9c1fc0
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 34 deletions.
7 changes: 7 additions & 0 deletions .changeset/pink-wolves-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/view-components': minor
---

Allow ActionMenu items to submit multiple values on form submission; fix keyboard handling for submit items

<!-- Changed components: Primer::Alpha::ActionMenu -->
3 changes: 2 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ gem "bootsnap", ">= 1.4.2", require: false
gem "lookbook", "~> 2.1.1" unless rails_version.to_f < 7
gem "view_component", path: ENV["VIEW_COMPONENT_PATH"] if ENV["VIEW_COMPONENT_PATH"]

gem "sourcemap"
gem "sourcemap", "~> 0.1"
gem "kramdown", "~> 2.4"

group :test do
gem "webmock"
Expand Down
5 changes: 4 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ GEM
htmlentities (4.3.4)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
kramdown (2.4.0)
rexml
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
Expand Down Expand Up @@ -249,6 +251,7 @@ DEPENDENCIES
cuprite (~> 0.14.3)
erb_lint (~> 0.4.0)
erblint-github (= 0.4.0)
kramdown (~> 2.4)
listen (~> 3.0)
lookbook (~> 2.1.1)
matrix (~> 0.4.2)
Expand All @@ -266,7 +269,7 @@ DEPENDENCIES
rubocop-performance (~> 1.7)
simplecov (~> 0.21.2)
simplecov-console (~> 0.9.1)
sourcemap
sourcemap (~> 0.1)
sprockets
sprockets-rails
thor
Expand Down
6 changes: 4 additions & 2 deletions app/components/primer/alpha/action_list/form_wrapper.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<% if form_required? %>
<%= form_with(url: @action, method: @http_method, **@form_arguments) do %>
<% if render_input? %>
<%= render(Primer::BaseComponent.new(tag: :input, **@input_arguments)) %>
<% if render_inputs? %>
<% @inputs.each do |input_arguments| %>
<%= render(Primer::BaseComponent.new(tag: :input, **input_arguments)) %>
<% end %>
<% end %>
<%= content %>
<% end %>
Expand Down
29 changes: 20 additions & 9 deletions app/components/primer/alpha/action_list/form_wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,25 @@ def initialize(list:, action: nil, **form_arguments)

name = @form_arguments.delete(:name)
value = @form_arguments.delete(:value) || name
inputs = @form_arguments.delete(:inputs) || []

@input_arguments = {
type: :hidden,
name: name,
value: value,
data: { list_item_input: true },
**(@form_arguments.delete(:input_arguments) || {})
}
# For the older version of this component that only allowed you to
# specify a single input
if inputs.empty?
inputs << {
name: name,
value: value,
**(@form_arguments.delete(:input_arguments) || {})
}
end

@inputs = inputs.map do |input_data|
input_data = input_data.dup
input_data[:type] ||= :hidden
input_data[:data] ||= {}
input_data[:data][:list_item_input] = true
input_data
end
end

def get?
Expand All @@ -42,8 +53,8 @@ def form_required?
@action && !get?
end

def render_input?
@input_arguments[:name].present?
def render_inputs?
@inputs.present?
end

private
Expand Down
123 changes: 120 additions & 3 deletions app/components/primer/alpha/action_menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,131 @@

module Primer
module Alpha
# ActionMenu is used for actions, navigation, to display secondary options, or single/multi select lists. They appear when users interact with buttons, actions, or other controls.
# ActionMenu is used for actions, navigation, to display secondary options, or single/multi select lists. They appear when
# users interact with buttons, actions, or other controls.
#
# The only allowed elements for the `Item` components are: `:a`, `:button`, and `:clipboard-copy`. The default is `:button`.
#
# ### Select variants
#
# While `ActionMenu`s default to a list of buttons that can link to other pages, copy text to the clipboard, etc, they also support
# `single` and `multiple` select variants. The single select variant allows a single item to be "selected" (i.e. marked "active")
# when clicked, which will cause a check mark to appear to the left of the item text. When the `multiple` select variant is chosen,
# multiple items may be selected and check marks will appear next to each selected item.
#
# Use the `select_variant:` option to control which variant the `ActionMenu` uses. For more information, see the documentation on
# supported arguments below.
#
# ### Dynamic labels
#
# When using the `single` select variant, an optional label indicating the selected item can be displayed inside the menu button.
# Dynamic labels can also be prefixed with custom text.
#
# Pass `dynamic_label: true` to enable dynamic label behavior, and pass `dynamic_label_prefix: "<string>"` to set a custom prefix.
# For more information, see the documentation on supported arguments below.
#
# ### `ActionMenu`s as form inputs
#
# When using either the `single` or `multiple` select variants, `ActionMenu`s can be used as form inputs. They behave very
# similarly to how HTML `<select>` boxes behave, and play nicely with Rails' built-in form mechanisms. Pass arguments via the
# `form_arguments:` argument, including the Rails form builder object and the name of the field:
#
# ```erb
# <% form_with(url: update_merge_strategy_path) do |f| %>
# <%= render(Primer::Alpha::ActionMenu.new(form_arguments: { builder: f, name: "merge_strategy" })) do |menu| %>
# <% menu.with_item(label: "Fast forward", data: { value: "fast_forward" }) %>
# <% menu.with_item(label: "Recursive", data: { value: "recursive" }) %>
# <% menu.with_item(label: "Ours", data: { value: "ours" }) %>
# <% menu.with_item(label: "Theirs", data: { value: "theirs" }) %>
# <% end %>
# <% end %>
# ```
#
# The value of the `data: { value: ... }` argument is sent to the server on submit, keyed using the name provided above
# (eg. `"merge_strategy"`). If no value is provided for an item, the value of that item is the item's label. Here's the
# corresponding `MergeStrategyController` that might be written to handle the form above:
#
# ```ruby
# class MergeStrategyController < ApplicationController
# def update
# puts "You chose #{merge_strategy_params[:merge_strategy]}"
# end
#
# private
#
# def merge_strategy_params
# params.permit(:merge_strategy)
# end
# end
# ```
#
# ### `ActionMenu` items that submit forms
#
# Whereas `ActionMenu` items normally permit navigation via `<a>` tags which make HTTP `get` requests, `ActionMenu` items
# also permit navigation via `POST` requests. To enable this behavior, include the `href:` argument as normal, but also pass
# the `form_arguments:` argument to the appropriate item:
#
# ```erb
# <%= render(Primer::Alpha::ActionMenu.new) do |menu| %>
# <% menu.with_item(
# label: "Repository",
# href: update_repo_grouping_path,
# form_arguments: {
# method: :post,
# name: "group_by",
# value: "repository"
# }
# ) %>
# <% end %>
# ```
#
# Make sure to specify `method: :post`, as the default is `:get`. When clicked, the list item will submit a POST request to
# the URL passed in the `href:` argument, including a parameter named `"group_by"` with a value of `"repository"`. If no value
# is given, the name, eg. `"group_by"`, will be used as the value.
#
# It is possible to include multiple fields on submit. Instead of passing the `name:` and `value:` arguments, pass an array via
# the `inputs:` argument:
#
# ```erb
# <%= render(Primer::Alpha::ActionMenu.new) do |menu| %>
# <% menu.with_show_button { "Group By" } %>
# <% menu.with_item(
# label: "Repository",
# href: update_repo_grouping_path,
# form_arguments: {
# method: :post,
# inputs: [{
# name: "group_by",
# value: "repository"
# }, {
# name: "some_other_field",
# value: "some value",
# }],
# }
# ) %>
# <% end %>
# ```
#
# ### Form arguments
#
# The following table summarizes the arguments allowed in the `form_arguments:` hash mentioned above.
#
# |Name |Type |Default|Description|
# |:----------------|:-------------|:------|:----------|
# |`method` |`Symbol` |`:get` |The HTTP request method to use to submit the form. One of `:get`, `:post`, `:patch`, `:put`, `:delete`, or `:head`|
# |`name` |`String` |`nil` |The name of the field that will be sent to the server on submit.|
# |`value` |`String` |`nil` |The value of the field that will be sent to the server on submit.|
# |`input_arguments`|`Hash` |`{}` |Additional key/value pairs to emit as HTML attributes on the `<input type="hidden">` element.|
# |`inputs` |`Array<Hash>` |`[]` |An array of hashes representing HTML `<input type="hidden">` elements. Must contain at least `name:` and `value:` keys. If additional key/value pairs are provided, they are emitted as HTML attributes on the `<input>` element. This argument supercedes the `name:`, `value:`, and `:input_arguments` arguments listed above.|
#
# The elements of the `inputs:` array will be emitted as HTML `<input type="hidden">` elements.
#
# @accessibility
# The action for the menu item needs to be on the element with `role="menuitem"`. Semantics are removed for everything nested inside of it. When a menu item is selected, the menu will close immediately.
# The action for the menu item needs to be on the element with `role="menuitem"`. Semantics are removed for everything
# nested inside of it. When a menu item is selected, the menu will close immediately.
#
# Additional information around the keyboard functionality and implementation can be found on the [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices-1.2/#menu).
# Additional information around the keyboard functionality and implementation can be found on the
# [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices-1.2/#menu).
class ActionMenu < Primer::Component
status :alpha

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,12 +278,6 @@ export class ActionMenuElement extends HTMLElement {
}

this.#updateInput()

if (event instanceof KeyboardEvent && event.target instanceof HTMLButtonElement) {
// prevent buttons from being clicked twice
event.preventDefault()
return
}
}

#activateItem(event: Event, item: Element) {
Expand Down
15 changes: 14 additions & 1 deletion demo/app/controllers/action_menu_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ def form_action
respond_to do |format|
format.html do
@value = form_action_selected_value
@other_params = form_action_other_params
end

format.json do
render json: { value: form_action_selected_value }
render json: { value: form_action_selected_value, other_params: form_action_other_params }
end
end
end
Expand All @@ -31,4 +32,16 @@ def form_action
def form_action_selected_value
params.permit(:foo)[:foo] || params.permit(foo: [])[:foo]
end

def form_action_other_params
params.permit!.to_hash.tap do |all|
case all
when Hash
all.delete("foo")
all.delete("authenticity_token")
when Array
all.delete(form_action_selected_value)
end
end
end
end
3 changes: 2 additions & 1 deletion demo/app/views/action_menu/form_action.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
You selected <%= @value.inspect %>
You selected <%= @value.inspect %><br/>
Other params sent to server: <%= @other_params.inspect %>
Loading

0 comments on commit b9c1fc0

Please sign in to comment.