diff --git a/.changeset/new-students-change.md b/.changeset/new-students-change.md new file mode 100644 index 0000000000..e983d38e24 --- /dev/null +++ b/.changeset/new-students-change.md @@ -0,0 +1,5 @@ +--- +"@primer/view-components": patch +--- + +Add trailing visuals to the text field diff --git a/app/components/primer/alpha/text_field.pcss b/app/components/primer/alpha/text_field.pcss index cb46fdb8ae..94707ab81b 100644 --- a/app/components/primer/alpha/text_field.pcss +++ b/app/components/primer/alpha/text_field.pcss @@ -121,6 +121,8 @@ ** .FormControl ** ├─ .FormControl-label ** │ ├─ .FormControl-input-wrap +** │ │ ├─ .FormControl-input-trailingVisualWrap +** │ │ │ ├─ .FormControl-input-trailingVisual ** │ │ ├─ .FormControl-input-leadingVisualWrap ** │ │ │ ├─ .FormControl-input-leadingVisual ** │ │ ├─ .FormControl-input @@ -253,6 +255,28 @@ } } + & .FormControl-input-trailingVisualWrap { + position: absolute; + top: var(--base-size-8); + right: var(--base-size-8); + display: flex; + height: var(--base-size-16); + align-items: center; + gap: var(--base-size-4); + color: var(--fgColor-muted); + pointer-events: none; + &:has( .FormControl-input-trailingVisualText) { + width: auto; + max-width: 25%; + padding-left: var(--base-size-8); + + & .FormControl-input-trailingVisualText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } /* TODO: replace with new Button component */ & .FormControl-input-trailingAction { position: absolute; @@ -333,10 +357,29 @@ } } + /* if trailingVisual is present */ + + /* + ┌──────────────────┬──32px──┐ + ╎ ┌──────────────┐ ┌────┐ ╎ + ╎ 24px 24px ╎ + ╎ └──────────────┘ └────┘ ╎ + └──────────────────┴────────┘ + */ + + &.FormControl-input-wrap--trailingVisual { + & .FormControl-input { + padding-inline-end: calc(var(--control-medium-paddingInline-condensed) + var(--base-size-16) + var(--control-medium-gap)); + } + &:has(.FormControl-input-trailingVisualText) .FormControl-input { + padding-inline-end: 25% + } + } + /* ┌──────────────────┬──32px──┐ ╎ ┌──────────────┐ ┌────┐ ╎ - ╎ 24px 24px ╎ + ╎ 24px 24px ╎ ╎ └──────────────┘ └────┘ ╎ └──────────────────┴────────┘ */ @@ -377,6 +420,10 @@ top: calc(var(--control-medium-paddingInline-condensed) - var(--base-size-2)); /* 6px */ left: calc(var(--control-medium-paddingInline-condensed) - var(--base-size-2)); /* 6px */ } + & .FormControl-input-trailingVisualWrap { + top: calc(var(--control-medium-paddingInline-condensed) - var(--base-size-2)); /* 6px */ + right: calc(var(--control-medium-paddingInline-condensed) - var(--base-size-2)); /* 6px */ + } /* ┌──────────────────┬──28px──┐ @@ -427,6 +474,10 @@ top: var(--control-medium-paddingInline-normal); left: var(--control-medium-paddingInline-normal); } + & .FormControl-input-trailingVisualWrap { + top: var(--control-medium-paddingInline-normal); + right: var(--control-medium-paddingInline-normal); + } /* ┌──36px──┬───12px padding──────┐ @@ -444,6 +495,21 @@ } } + &.FormControl-input-wrap--trailingVisual { + & .FormControl-input { + padding-inline-end: calc(var(--control-large-paddingInline-normal) + var(--base-size-16) + var(--control-large-gap)); + } + &:has(.FormControl-input-trailingVisualText) .FormControl-input { + padding-inline-end: 25% + } + } + + &.FormControl-input-wrap--trailingText { + & .FormControl-input.FormControl-large { + padding-inline-end: 25%; + } + } + /* ┌──────────────────┬──36px──┐ ╎ ┌──────────────┐ ┌────┐ ╎ diff --git a/app/lib/primer/forms/dsl/text_field_input.rb b/app/lib/primer/forms/dsl/text_field_input.rb index 698189fcc8..9aa4dd23f2 100644 --- a/app/lib/primer/forms/dsl/text_field_input.rb +++ b/app/lib/primer/forms/dsl/text_field_input.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - module Primer module Forms module Dsl @@ -7,7 +5,7 @@ module Dsl class TextFieldInput < Input attr_reader( *%i[ - name label show_clear_button leading_visual leading_spinner clear_button_id + name label show_clear_button leading_visual leading_spinner trailing_visual clear_button_id visually_hide_label inset monospace field_wrap_classes auto_check_src ] ) @@ -20,6 +18,7 @@ def initialize(name:, label:, **system_arguments) @show_clear_button = system_arguments.delete(:show_clear_button) @leading_visual = system_arguments.delete(:leading_visual) + @trailing_visual = build_trailing_visual(system_arguments.delete(:trailing_visual)) @leading_spinner = !!system_arguments.delete(:leading_spinner) @clear_button_id = system_arguments.delete(:clear_button_id) @inset = system_arguments.delete(:inset) @@ -48,6 +47,25 @@ def initialize(name:, label:, **system_arguments) alias inset? inset alias monospace? monospace + def trailing_visual? + !!@trailing_visual + end + + def leading_visual? + !!@leading_visual + end + + def build_trailing_visual(trailing_visual) + return nil unless trailing_visual + + icon = trailing_visual[:icon] + text = trailing_visual[:text] + counter = trailing_visual[:counter] + label = trailing_visual[:label] + + { icon: icon, text: text, counter: counter, label: label }.compact + end + def to_component TextField.new(input: self) end @@ -60,10 +78,6 @@ def focusable? true end - def leading_visual? - !!@leading_visual - end - def validation_arguments if auto_check_src.present? super.merge( diff --git a/app/lib/primer/forms/text_field.html.erb b/app/lib/primer/forms/text_field.html.erb index 7b06434e76..207ad1973e 100644 --- a/app/lib/primer/forms/text_field.html.erb +++ b/app/lib/primer/forms/text_field.html.erb @@ -17,5 +17,8 @@ <%= render(Primer::Beta::Octicon.new(icon: :"x-circle-fill")) %> <% end %> + <% if @input.trailing_visual %> + <%= trailing_visual_render %> + <% end %> <% end %> <% end %> diff --git a/app/lib/primer/forms/text_field.rb b/app/lib/primer/forms/text_field.rb index 1840f92434..3391c400e7 100644 --- a/app/lib/primer/forms/text_field.rb +++ b/app/lib/primer/forms/text_field.rb @@ -1,8 +1,5 @@ -# frozen_string_literal: true - module Primer module Forms - # :nodoc: class TextField < BaseComponent delegate :builder, :form, to: :@input @@ -24,9 +21,9 @@ def initialize(input:) "FormControl-input-wrap", INPUT_WRAP_SIZE[input.size], "FormControl-input-wrap--trailingAction": @input.show_clear_button?, + "FormControl-input-wrap--trailingVisual": @input.trailing_visual?, "FormControl-input-wrap--leadingVisual": @input.leading_visual? ), - hidden: @input.hidden? } end @@ -41,6 +38,30 @@ def auto_check_authenticity_token ) end end + + def trailing_visual_render + visual = @input.trailing_visual + return unless visual + + content = ActiveSupport::SafeBuffer.new # Use SafeBuffer for safe HTML concatenation + + # Render icon if specified + content << render(Primer::Beta::Octicon.new(icon: visual[:icon], classes: "FormControl-input-trailingVisualIcon")) if visual[:icon] + + # Render label if specified + content << render(Primer::Beta::Label.new(classes: "FormControl-input-trailingVisualLabel")) { visual[:label] } if visual[:label] + + # Render counter if specified + content << render(Primer::Beta::Counter.new(count: visual[:counter], classes: "FormControl-input-trailingVisualCounter")) if visual[:counter] + + # Render text if specified + content << content_tag(:span, visual[:text], class: "FormControl-input-trailingVisualText") if visual[:text] + + # Wrap in the trailing visual container + content_tag(:span, content, class: "FormControl-input-trailingVisualWrap") + end + + end end end diff --git a/previews/primer/alpha/text_field_preview.rb b/previews/primer/alpha/text_field_preview.rb index e077ff0d09..a8390f7c65 100644 --- a/previews/primer/alpha/text_field_preview.rb +++ b/previews/primer/alpha/text_field_preview.rb @@ -172,6 +172,30 @@ def monospace render(Primer::Alpha::TextField.new(monospace: true, name: "my-text-field", label: "My text field")) end + # @label With trailing icon + # @snapshot + def with_trailing_icon + render(Primer::Alpha::TextField.new(trailing_visual: { icon: :search }, name: "my-text-field", label: "My text field")) + end + + # @label With trailing text + # @snapshot + def with_trailing_text + render(Primer::Alpha::TextField.new( trailing_visual: { text: "minute" }, name: "my-text-field", label: "My text field")) + end + + # @label With trailing counter + # @snapshot + def with_trailing_counter + render(Primer::Alpha::TextField.new( trailing_visual: { counter: 5 }, name: "my-text-field", label: "My text field")) + end + + # @label With trailing label + # @snapshot + def with_trailing_label + render(Primer::Alpha::TextField.new( trailing_visual: { label: "Hello" }, name: "my-text-field", label: "My text field")) + end + # @label With leading visual # @snapshot def with_leading_visual diff --git a/test/components/alpha/text_field_test.rb b/test/components/alpha/text_field_test.rb index e906c1b3cd..e2ed7531e1 100644 --- a/test/components/alpha/text_field_test.rb +++ b/test/components/alpha/text_field_test.rb @@ -102,4 +102,36 @@ def test_enforces_leading_visual_when_spinner_requested assert_includes error.message, "must also specify a leading visual" end + + def test_renders_a_trailing_visual_icon + render_inline(Primer::Alpha::TextField.new(**@default_params, trailing_visual: { icon: :search })) + + assert_selector ".FormControl-input-trailingVisualWrap" do + assert_selector "svg.octicon.octicon-search.FormControl-input-trailingVisualIcon" + end + end + + def test_renders_a_trailing_visual_text + render_inline(Primer::Alpha::TextField.new(**@default_params, trailing_visual: { text: 'minute' })) + + assert_selector ".FormControl-input-trailingVisualWrap" do + assert_selector ".FormControl-input-trailingVisualText", text: "minute" + end + end + + def test_renders_a_trailing_visual_label + render_inline(Primer::Alpha::TextField.new(**@default_params, trailing_visual: { label: 'Hello' })) + + assert_selector ".FormControl-input-trailingVisualWrap" do + assert_selector ".FormControl-input-trailingVisualLabel.Label", text: "Hello" + end + end + + def test_renders_a_trailing_visual_Counter + render_inline(Primer::Alpha::TextField.new(**@default_params, trailing_visual: { counter: '5' })) + + assert_selector ".FormControl-input-trailingVisualWrap" do + assert_selector ".FormControl-input-trailingVisualCounter.Counter", text: "5" + end + end end