Skip to content

Commit

Permalink
Standalone form control (#1821)
Browse files Browse the repository at this point in the history
Co-authored-by: camertron <camertron@users.noreply.github.com>
Co-authored-by: Jon Rohan <rohan@github.com>
  • Loading branch information
3 people committed Feb 14, 2023
1 parent e62af5e commit c12ae8c
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/polite-turkeys-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': patch
---

Add a standalone FormControl component
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.
26 changes: 26 additions & 0 deletions app/components/primer/alpha/form_control.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<%= render(Primer::BaseComponent.new(tag: :div, **@system_arguments)) do %>
<%= render(Primer::BaseComponent.new(tag: :label, **@label_arguments)) do %>
<%= @label %>
<% if required? %>
<span aria-hidden="true">*</span>
<% end %>
<% end %>
<% if @input_block %>
<%= view_context.capture { @input_block.call(@input_arguments) } %>
<% end %>
<% if @validation_message %>
<%= render(Primer::BaseComponent.new(tag: :div, **@validation_arguments)) do %>
<%= render(Primer::Beta::Octicon.new(icon: :"alert-fill", size: :xsmall, aria: { hidden: true })) %>
<span><%= @validation_message %></span>
<% end %>
<% end %>
<% if @init_caption || caption? %>
<span class="FormControl-caption" id="<%= @caption_id %>">
<% if caption? %>
<%= caption %>
<% else %>
<%= @init_caption %>
<% end %>
</span>
<% end %>
<% end %>
105 changes: 105 additions & 0 deletions app/components/primer/alpha/form_control.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true

module Primer
module Alpha
# Wraps an input (or arbitrary content) with a label above and a caption and validation message beneath.
# NOTE: This `FormControl` component is designed for wrapping inputs that aren't supported by the Primer
# forms framework.
class FormControl < Primer::Component
# Describes the field and what sorts of input it expects. Displayed below the input.
# Note that this slot takes precedence over the `caption:` argument in the constructor.
renders_one :caption

# @example Default
# <%= render(Primer::Alpha::FormControl.new(label: "Best character")) do |component| %>
# <% component.with_input do |input_arguments| %>
# <%= render(Primer::Alpha::SegmentedControl.new("aria-label": "Best character", **input_arguments)) do |seg| %>
# <% seg.with_item(label: "Han Solo") %>
# <% seg.with_item(label: "Luke Skywalker") %>
# <% seg.with_item(label: "Leia Organa") %>
# <% end %>
# <% end %>
# <% end %>
#
# @param label [String] Label text displayed above the input.
# @param caption [String] Describes the field and what sort of input it expects. Displayed below the input. Note that the `caption` slot is also available and takes precedence over this argument when provided.
# @param validation_message [String] A string displayed in red between the caption and the input indicating the input's contents are invalid.
# @param required [Boolean] Default `false`. When set to `true`, causes an asterisk (*) to appear next to the field's label indicating it is a required field. Note that this option explicitly does _not_ add a `required` HTML attribute. Doing so would enable native browser validations, which are inaccessible and inconsistent with the Primer design system.
# @param visually_hide_label [Boolean] When set to `true`, hides the label. Although the label will be hidden visually, it will still be visible to screen readers.
# @param full_width [Boolean] When set to `true`, the form control will take up all the horizontal space allowed by its container.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(label:, caption: nil, validation_message: nil, required: false, visually_hide_label: false, full_width: false, **system_arguments)
@label = label
@init_caption = caption
@validation_message = validation_message
@required = required
@visually_hide_label = visually_hide_label
@full_width = full_width
@system_arguments = system_arguments

@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"FormControl",
"FormControl--fullWidth" => full_width?
)

@label_arguments = {
classes: class_names(
"FormControl-label",
visually_hide_label? ? "sr-only" : nil
)
}

base_id = self.class.generate_id
@validation_id = "validation-#{base_id}"
@caption_id = "caption-#{base_id}"

@validation_arguments = {
classes: "FormControl-inlineValidation",
id: @validation_id
}
end

# @!parse
# # The input content. Yields a set of <%= link_to_system_arguments_docs %> that should be added to the input.
# #
# renders_one(:input)

def with_input(&block)
@input_block = block
end

def required?
@required
end

def visually_hide_label?
@visually_hide_label
end

def full_width?
@full_width
end

private

def before_render
# make sure to evaluate the component's content block so slots are defined
content

@input_arguments = {
aria: {}
}

ids = [].tap do |memo|
memo << @validation_id if @validation_message
memo << @caption_id if @init_caption || caption?
end

return if ids.empty?

@input_arguments[:aria][:describedby] = ids.join(" ")
end
end
end
end
106 changes: 106 additions & 0 deletions previews/primer/alpha/form_control_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

module Primer
module Alpha
# @label FormControl
class FormControlPreview < ViewComponent::Preview
# @label Playground
#
# @param label text
# @param caption text
# @param validation_message text
# @param required toggle
# @param visually_hide_label toggle
# @param full_width toggle
def playground(
label: "Best character",
caption: "May the force be with you",
validation_message: "Something went wrong",
required: false,
visually_hide_label: false,
full_width: false
)
render_with_template(
locals: {
system_arguments: {
label: label,
caption: caption,
validation_message: validation_message,
required: required,
visually_hide_label: visually_hide_label,
full_width: full_width
}
}
)
end

# @label Default
def default
render_with_template(
template: "primer/alpha/form_control_preview/playground",
locals: {
system_arguments: {
label: "Best character"
}
}
)
end

# @!group Options
#
# @label With caption
def with_caption
render_with_template(
template: "primer/alpha/form_control_preview/playground",
locals: {
system_arguments: {
label: "Best character",
caption: "May the force be with you"
}
}
)
end

# @label With validation message
def with_validation_message
render_with_template(
template: "primer/alpha/form_control_preview/playground",
locals: {
system_arguments: {
label: "Best character",
validation_message: "Something went wrong"
}
}
)
end

# @label Required
def required
render_with_template(
template: "primer/alpha/form_control_preview/playground",
locals: {
system_arguments: {
label: "Best character",
required: true
}
}
)
end

# @label With visually hidden label
def with_visually_hidden_label
render_with_template(
template: "primer/alpha/form_control_preview/playground",
locals: {
system_arguments: {
label: "Best character",
visually_hide_label: true
}
}
)
end
#
# @!endgroup
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= render(Primer::Alpha::FormControl.new(**system_arguments)) do |component| %>
<% component.with_input do |input_arguments| %>
<%= render(Primer::Alpha::SegmentedControl.new("aria-label": "Best character", **input_arguments)) do |seg| %>
<% seg.with_item(label: "Han Solo", selected: true) %>
<% seg.with_item(label: "Luke Skywalker") %>
<% seg.with_item(label: "Leia Organa") %>
<% end %>
<% end %>
<% end %>
47 changes: 47 additions & 0 deletions test/components/alpha/form_control_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "components/test_helper"

module Primer
module Alpha
class FormControlTest < Minitest::Test
include Primer::ComponentTestHelpers

def test_basic_structure
render_preview(:playground)

assert_selector(".FormControl-label", text: "Best character")
assert_selector("segmented-control")
assert_selector(".FormControl-inlineValidation", text: "Something went wrong") do
assert_selector(".octicon-alert-fill")
end
assert_selector(".FormControl-caption", text: "May the force be with you")
end

def test_described_by_ids
render_preview(:playground)

caption_id = page.find_css(".FormControl-caption")[0].attributes["id"].value
validation_id = page.find_css(".FormControl-inlineValidation")[0].attributes["id"].value
described_by_ids = page.find_css("segmented-control ul")[0].attributes["aria-describedby"].value.split

assert_includes(described_by_ids, caption_id)
assert_includes(described_by_ids, validation_id)
end

def test_required
render_preview(:required)

assert_selector(".FormControl-label", text: "Best character") do
assert_selector("[aria-hidden=true]", text: "*")
end
end

def test_visually_hidden_label
render_preview(:with_visually_hidden_label)

assert_selector(".FormControl-label.sr-only", text: "Best character")
end
end
end
end
3 changes: 2 additions & 1 deletion test/components/component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ class PrimerComponentTest < Minitest::Test
[Primer::Alpha::UnderlineNav, { label: "aria label" }, proc { |component| component.with_tab(selected: true) { "Foo" } }],
[Primer::Alpha::Tooltip, { type: :label, for_id: "some-button", text: "Foo" }],
[Primer::Alpha::NavList, { aria: { label: "Nav list" } }],
[Primer::Alpha::Banner, {}]
[Primer::Alpha::Banner, {}],
[Primer::Alpha::FormControl, { label: "Foo" }]
].freeze

def test_registered_components
Expand Down

0 comments on commit c12ae8c

Please sign in to comment.