Skip to content

Commit

Permalink
Add an autocomplete form input (#2609)
Browse files Browse the repository at this point in the history
Co-authored-by: camertron <[email protected]>
  • Loading branch information
camertron and camertron authored Mar 6, 2024
1 parent c7c206a commit 14d8dc5
Show file tree
Hide file tree
Showing 19 changed files with 237 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-books-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/view-components": minor
---

Add an AutoComplete form input
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: 9 additions & 0 deletions app/components/primer/alpha/dialog.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ dialog.Overlay:not([open]) {
opacity: 0;
}

/* hacks until firefox fully supports popovers */
:where([popover]:not(.\:popover-open)) {
display: none !important;
}

:popover-open {
display: inherit !important;
}

.Overlay {
display: flex;
inset: 0;
Expand Down
18 changes: 9 additions & 9 deletions app/components/primer/beta/auto_complete/auto_complete.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
<%= @label_text %>
</label>
<% if leading_visual || @show_clear_button %>
<div class="<%= @field_wrap_classes %> <% if leading_visual %>FormControl-input-wrap--leadingVisual<% end %>">
<span class="FormControl-input-leadingVisualWrap">
<%= leading_visual %>
</span>
<%= input %>
<% if @show_clear_button %>
<button id="<%= @input_id %>-clear" class="FormControl-input-trailingAction" aria-label="Clear"><%= primer_octicon "x-circle-fill" %></button>
<% end %>
</div>
<div class="<%= @field_wrap_classes %> <% if leading_visual %>FormControl-input-wrap--leadingVisual<% end %>">
<span class="FormControl-input-leadingVisualWrap">
<%= leading_visual %>
</span>
<%= input %>
<% if @show_clear_button %>
<button id="<%= @input_id %>-clear" class="FormControl-input-trailingAction" aria-label="Clear"><%= primer_octicon "x-circle-fill" %></button>
<% end %>
</div>
<% else %>
<%= input %>
<% end %>
Expand Down
18 changes: 18 additions & 0 deletions app/forms/auto_complete_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require_relative "../../previews/primer/url_helpers"

# :nodoc:
class AutoCompleteForm < ApplicationForm
form do |auto_complete_form|
auto_complete_form.auto_complete(
name: :fruit,
label: "Fruit",
caption: "Please enter your favorite fruit",
src: Primer::UrlHelpers.autocomplete_index_path,
validation_message: "Something went wrong"
)

auto_complete_form.submit(label: "Submit", name: :submit)
end
end
6 changes: 6 additions & 0 deletions lib/primer/forms/auto_complete.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%= render(Primer::Beta::AutoComplete.new(**auto_complete_arguments)) do |autocomplete| %>
<% @input.block.call(autocomplete) if @input.block %>
<% autocomplete.with_input(**input_arguments) %>
<% end %>
<%= render(ValidationMessage.new(input: @input)) %>
<%= render(Caption.new(input: @input)) %>
56 changes: 56 additions & 0 deletions lib/primer/forms/auto_complete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Primer
module Forms
# :nodoc:
class AutoComplete < BaseComponent
ARGUMENT_TYPES = %i(keyreq key).freeze

delegate :builder, :form, to: :@input

def initialize(input:)
@input = input
@input.merge_input_arguments!(text_field_attributes.deep_symbolize_keys)
end

def self.auto_complete_argument_names
@auto_complete_argument_names ||=
Primer::Beta::AutoComplete.instance_method(:initialize)
.parameters
.filter_map { |(type, param_name)| next param_name if ARGUMENT_TYPES.include?(type) }
end

private

def all_input_arguments
@all_input_arguments ||= @input.input_arguments.deep_dup.tap do |args|
# rails uses :class but PVC wants :classes
args[:classes] = class_names(
args[:classes],
args.delete(:class)
)
end
end

def auto_complete_arguments
all_args = all_input_arguments
all_args
.slice(*self.class.auto_complete_argument_names)
.merge(
input_name: all_args[:name],
input_id: all_args[:id],
label_text: @input.label,
list_id: "#{all_args[:id]}-list"
)
end

def input_arguments
all_input_arguments.except(*self.class.auto_complete_argument_names, :id, :name)
end

def text_field_attributes
builder.text_field_attributes(@input.name).except("size", "value")
end
end
end
end
19 changes: 19 additions & 0 deletions lib/primer/forms/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ def error_wrapping(html_tag)

module Primer
module Forms
module Tags
# :nodoc:
class TextField < ::ActionView::Helpers::Tags::TextField
def attributes
render
end

private

def tag(_name, options)
options
end
end
end

# :nodoc:
class Builder < ActionView::Helpers::FormBuilder
alias primer_fields_for fields_for
Expand Down Expand Up @@ -57,6 +72,10 @@ def text_field(*args, **options, &block)
super(*args, classify(options).merge(generate_error_markup: false), &block)
end

def text_field_attributes(method, options = {})
Tags::TextField.new(@object_name, method, @template, options).attributes
end

def text_area(*args, **options, &block)
super(*args, classify(options).merge(generate_error_markup: false), &block)
end
Expand Down
33 changes: 33 additions & 0 deletions lib/primer/forms/dsl/auto_complete_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Primer
module Forms
module Dsl
# :nodoc:
class AutoCompleteInput < Input
attr_reader :name, :label, :block

def initialize(name:, label:, **system_arguments, &block)
@name = name
@label = label
@block = block

super(**system_arguments)
end

def to_component
AutoComplete.new(input: self)
end

def type
:autocomplete
end

# The AutoComplete Primer component does not allow auto-focusing
def focusable?
false
end
end
end
end
end
9 changes: 9 additions & 0 deletions lib/primer/forms/dsl/input_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ def text_field(**options, &block)
add_input TextFieldInput.new(builder: builder, form: form, **options, &block)
end

# Adds an autocomplete text field to this form.
#
# @param options [Hash] The options accepted by the autocomplete input (see forms docs).
# @param block [Proc] A block that will be yielded a reference to the input object so it can be customized.
def auto_complete(**options, &block)
options = decorate_options(**options)
add_input AutoCompleteInput.new(builder: builder, form: form, **options, &block)
end

# Adds a text area to this form.
#
# @param options [Hash] The options accepted by the text area input (see forms docs).
Expand Down
4 changes: 2 additions & 2 deletions lib/primer/forms/primer_text_field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class PrimerTextFieldElement extends HTMLElement {
this.#abortController?.abort()
const {signal} = (this.#abortController = new AbortController())

this.inputElement.addEventListener(
this.addEventListener(
'auto-check-success',
async (event: any) => {
const message = await event.detail.response.text()
Expand All @@ -30,7 +30,7 @@ class PrimerTextFieldElement extends HTMLElement {
{signal},
)

this.inputElement.addEventListener(
this.addEventListener(
'auto-check-error',
async (event: any) => {
const errorMessage = await event.detail.response.text()
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
},
"dependencies": {
"@github/auto-check-element": "^5.2.0",
"@github/auto-complete-element": "^3.6.0",
"@github/auto-complete-element": "^3.6.2",
"@github/catalyst": "^1.6.0",
"@github/clipboard-copy-element": "^1.3.0",
"@github/details-menu-element": "^1.0.12",
Expand Down
2 changes: 2 additions & 0 deletions previews/primer/forms_preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,7 @@ def name_with_question_mark_form; end
def immediate_validation_form; end

def example_toggle_switch_form; end

def auto_complete_form; end
end
end
3 changes: 3 additions & 0 deletions previews/primer/forms_preview/auto_complete_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= primer_form_with(url: generic_form_submission_path) do |f| %>
<%= render(AutoCompleteForm.new(f)) %>
<% end %>
39 changes: 39 additions & 0 deletions test/lib/primer/forms/auto_complete_input_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require "lib/test_helper"
require_relative "models/deep_thought"

class Primer::Forms::AutoCompleteInputTest < Minitest::Test
include Primer::ComponentTestHelpers

def test_hidden_auto_complete
render_in_view_context do
primer_form_with(url: "/foo") do |f|
render_inline_form(f) do |auto_complete_form|
auto_complete_form.auto_complete(name: :foo, label: "Foo", src: "/items", hidden: true)
end
end
end

assert_selector "input[type=text]#foo", visible: :hidden
end

def test_only_primer_error_markup
model = DeepThought.new(41)
model.valid? # populate validation error messages

render_in_view_context do
primer_form_with(model: model, url: "/foo") do |f|
render_inline_form(f) do |auto_complete_form|
auto_complete_form.auto_complete(name: :ultimate_answer, label: "Ultimate answer", src: "/items")
end
end
end

# primer error markup
assert_selector ".FormControl-inlineValidation", text: "Ultimate answer must be greater than 41"

# no rails error markup
refute_selector ".field_with_errors", visible: :all
end
end
14 changes: 14 additions & 0 deletions test/lib/primer/forms/integration_forms_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ def test_action_menu_form_input
assert_equal result.dig("other_params", "city"), "lopez_island"
end

def test_autocomplete_form_input
visit_preview(:auto_complete_form)

# type "app" into the autocomplete field
fruit_field = find("#fruit")
fruit_field.fill_in(with: "app")

# click on the resulting "Apples" list item
find(".ActionListItem-label", text: "Apples").click

# assert autocomplete field now contains the text from the list item, "Apples"
assert fruit_field.value == "Apples"
end

private

def wait_for_toggle_switch_spinner
Expand Down
6 changes: 5 additions & 1 deletion test/lib/primer/forms/text_field_input_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_hidden_text_field
assert_selector "input[type=text]#foo", visible: :hidden
end

def test_no_error_markup
def test_only_primer_error_markup
model = DeepThought.new(41)
model.valid? # populate validation error messages

Expand All @@ -28,6 +28,10 @@ def test_no_error_markup
end
end

# primer error markup
assert_selector ".FormControl-inlineValidation", text: "Ultimate answer must be greater than 41"

# no rails error markup
refute_selector ".field_with_errors", visible: :all
end

Expand Down

0 comments on commit 14d8dc5

Please sign in to comment.