diff --git a/Makefile b/Makefile
index e521072f34d..c7eeaae248e 100644
--- a/Makefile
+++ b/Makefile
@@ -115,7 +115,13 @@ lint_yarn_workspaces: ## Lints Yarn workspace packages
scripts/validate-workspaces.js
lint_asset_bundle_size: ## Lints JavaScript and CSS compiled bundle size
- find app/assets/builds/application.css -size -270000c | grep .
+ @# This enforces an asset size budget to ensure that download sizes are reasonable and to protect
+ @# against accidentally importing large pieces of third-party libraries. If you're here debugging
+ @# a failing build, check to ensure that you've not added more JavaScript or CSS than necessary,
+ @# and you have no options to split that from the common bundles. If you need to increase this
+ @# budget and accept the fact that this will force end-users to endure longer load times, you
+ @# should set the new budget to within a few thousand bytes of the production-compiled size.
+ find app/assets/builds/application.css -size -235000c | grep .
find public/packs/js/application-*.digested.js -size -5000c | grep .
lint_migrations:
diff --git a/app/assets/stylesheets/_uswds.scss b/app/assets/stylesheets/_uswds.scss
index 0595c4695e1..18a3348ec68 100644
--- a/app/assets/stylesheets/_uswds.scss
+++ b/app/assets/stylesheets/_uswds.scss
@@ -9,7 +9,6 @@
@forward 'usa-collection';
@forward 'usa-form';
@forward 'usa-header';
-@forward 'usa-icon-list';
@forward 'usa-icon';
@forward 'usa-layout-grid';
@forward 'usa-link';
diff --git a/app/components/icon_list_component.html.erb b/app/components/icon_list_component.html.erb
new file mode 100644
index 00000000000..48c260bf7d5
--- /dev/null
+++ b/app/components/icon_list_component.html.erb
@@ -0,0 +1,5 @@
+<%= content_tag(:ul, **tag_options, class: css_class) do %>
+ <% items.each do |item| %>
+ <%= item %>
+ <% end %>
+<% end %>
diff --git a/app/components/icon_list_component.rb b/app/components/icon_list_component.rb
new file mode 100644
index 00000000000..58a37885fd0
--- /dev/null
+++ b/app/components/icon_list_component.rb
@@ -0,0 +1,35 @@
+class IconListComponent < BaseComponent
+ renders_many :items, ->(**kwargs, &block) do
+ IconListItemComponent.new(icon:, color:, **kwargs, &block)
+ end
+
+ attr_reader :icon, :size, :color, :tag_options
+
+ def initialize(icon: nil, size: 'md', color: nil, **tag_options)
+ @icon = icon
+ @size = size
+ @color = color
+ @tag_options = tag_options
+ end
+
+ def css_class
+ classes = ['usa-icon-list', *tag_options[:class]]
+ classes << ["usa-icon-list--size-#{size}"] if size
+ classes
+ end
+
+ class IconListItemComponent < BaseComponent
+ attr_reader :icon, :color
+
+ def initialize(icon:, color:)
+ @icon = icon
+ @color = color
+ end
+
+ def icon_css_class
+ classes = ['usa-icon-list__icon']
+ classes << "text-#{color}" if color
+ classes
+ end
+ end
+end
diff --git a/app/components/icon_list_component.scss b/app/components/icon_list_component.scss
new file mode 100644
index 00000000000..018f0f4b5f2
--- /dev/null
+++ b/app/components/icon_list_component.scss
@@ -0,0 +1,3 @@
+@use 'uswds-core' as *;
+
+@forward 'usa-icon-list';
diff --git a/app/components/icon_list_item_component.html.erb b/app/components/icon_list_item_component.html.erb
new file mode 100644
index 00000000000..05bab773005
--- /dev/null
+++ b/app/components/icon_list_item_component.html.erb
@@ -0,0 +1,6 @@
+
+ <%= content_tag(:div, class: icon_css_class) do %>
+ <%= render IconComponent.new(icon: icon) %>
+ <% end %>
+ <%= content %>
+
diff --git a/app/components/process_list_component.rb b/app/components/process_list_component.rb
index 6ba17d09c7d..1ee0c5b2b7a 100644
--- a/app/components/process_list_component.rb
+++ b/app/components/process_list_component.rb
@@ -1,7 +1,7 @@
class ProcessListComponent < BaseComponent
- renders_many :items, ->(**kwargs, &block) {
+ renders_many :items, ->(**kwargs, &block) do
ProcessListItemComponent.new(heading_level:, **kwargs, &block)
- }
+ end
attr_reader :heading_level, :big, :connected, :tag_options
diff --git a/app/views/partials/multi_factor_authentication/_selected_mfa_option.html.erb b/app/views/partials/multi_factor_authentication/_selected_mfa_option.html.erb
deleted file mode 100644
index e3fd5365b63..00000000000
--- a/app/views/partials/multi_factor_authentication/_selected_mfa_option.html.erb
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- <%= render IconComponent.new(icon: :check_circle, width: 24, height: 24) %>
-
-
- <%= option.label %> <%= option.mfa_added_label %>
-
-
diff --git a/app/views/users/two_factor_authentication_setup/index.html.erb b/app/views/users/two_factor_authentication_setup/index.html.erb
index b9cb887f760..6c76f7de089 100644
--- a/app/views/users/two_factor_authentication_setup/index.html.erb
+++ b/app/views/users/two_factor_authentication_setup/index.html.erb
@@ -19,13 +19,15 @@
<%= t('headings.account.two_factor') %>
-
+ <%= render IconListComponent.new(icon: :check_circle, color: :success) do |c| %>
<% @presenter.all_user_selected_options.each do |option| %>
<% if option.mfa_configuration_count > 0 %>
- <%= render partial: 'partials/multi_factor_authentication/selected_mfa_option', locals: { option: option } %>
+ <% c.with_item do %>
+ <%= option.label %> <%= option.mfa_added_label %>
+ <% end %>
<% end %>
<% end %>
-
+ <% end %>
<% end %>
<%= simple_form_for @two_factor_options_form,
diff --git a/spec/components/icon_list_component_spec.rb b/spec/components/icon_list_component_spec.rb
new file mode 100644
index 00000000000..b414b631c23
--- /dev/null
+++ b/spec/components/icon_list_component_spec.rb
@@ -0,0 +1,69 @@
+require 'rails_helper'
+
+RSpec.describe IconListComponent, type: :component do
+ subject(:rendered) { render_inline IconListComponent.new }
+
+ it 'renders with expected attributes for default state' do
+ expect(rendered).to have_css('.usa-icon-list.usa-icon-list--size-md')
+ end
+
+ context 'with explicitly nil size' do
+ subject(:rendered) { render_inline IconListComponent.new(size: nil) }
+
+ it 'renders without size modifier css class' do
+ expect(rendered).not_to have_css('[class*=usa-icon-list--size-]')
+ end
+ end
+
+ context 'with additional tag options' do
+ it 'applies tag options to wrapper element' do
+ rendered = render_inline IconListComponent.new(class: 'custom-class', data: { foo: 'bar' })
+
+ expect(rendered).to have_css('.usa-icon-list.custom-class[data-foo="bar"]')
+ end
+ end
+
+ context 'with slotted items' do
+ subject(:rendered) do
+ render_inline IconListComponent.new(icon: :cancel) do |c|
+ c.with_item { 'First' }
+ c.with_item { 'Second' }
+ end
+ end
+
+ it 'renders items with default color' do
+ expect(rendered).to have_css('.usa-icon-list__icon:not([class*="text-"])', count: 2)
+ expect(rendered).to have_css('.usa-icon use[href$=".svg#cancel"]', count: 2)
+ end
+
+ context 'with icon or color attributes specified on parent component' do
+ subject(:rendered) do
+ render_inline IconListComponent.new(icon: :cancel, color: :error) do |c|
+ c.with_item { 'First' }
+ c.with_item { 'Second' }
+ end
+ end
+
+ it 'passes those attributes to slotted items' do
+ expect(rendered).to have_css('.usa-icon-list__icon.text-error', count: 2)
+ expect(rendered).to have_css('.usa-icon use[href$=".svg#cancel"]', count: 2)
+ end
+ end
+
+ context 'with icon and color attributes specified on items' do
+ subject(:rendered) do
+ render_inline IconListComponent.new do |c|
+ c.with_item(icon: :check_circle, color: :success) { 'First' }
+ c.with_item(icon: :cancel, color: :error) { 'Second' }
+ end
+ end
+
+ it 'renders items with their attributes' do
+ expect(rendered).to have_css('.usa-icon-list__icon.text-success', count: 1)
+ expect(rendered).to have_css('.usa-icon use[href$=".svg#check_circle"]', count: 1)
+ expect(rendered).to have_css('.usa-icon-list__icon.text-error', count: 1)
+ expect(rendered).to have_css('.usa-icon use[href$=".svg#cancel"]', count: 1)
+ end
+ end
+ end
+end
diff --git a/spec/components/previews/icon_list_component_preview.rb b/spec/components/previews/icon_list_component_preview.rb
new file mode 100644
index 00000000000..f2fe0301c90
--- /dev/null
+++ b/spec/components/previews/icon_list_component_preview.rb
@@ -0,0 +1,25 @@
+class IconListComponentPreview < BaseComponentPreview
+ # @!group Preview
+ def default
+ render(IconListComponent.new(icon: :cancel, color: :error)) do |c|
+ c.with_item { 'You cannot pass identity verification if your ID is expired.' }
+ c.with_item { 'You cannot use extension documents in place of an unexpired ID.' }
+ c.with_item { 'You cannot use a paper or temporary ID.' }
+ end
+ end
+ # @!endgroup
+
+ # rubocop:disable Layout/LineLength
+
+ # @param icon select [~,accessibility_new,accessible_forward,account_balance,account_box,account_circle,add,add_circle,add_circle_outline,alarm,alternate_email,announcement,api,arrow_back,arrow_downward,arrow_drop_down,arrow_drop_up,arrow_forward,arrow_upward,assessment,attach_file,attach_money,autorenew,backpack,bathtub,bedding,bookmark,bug_report,build,calendar_today,campaign,camping,cancel,chat,check,check_box_outline_blank,check_circle,check_circle_outline,checkroom,chevron_left,chevron_right,clean_hands,close,closed_caption,clothes,cloud,code,comment,connect_without_contact,construction,construction_worker,contact_page,content_copy,coronavirus,credit_card,deck,delete,device_thermostat,directions,directions_bike,directions_bus,directions_car,directions_walk,do_not_disturb,do_not_touch,drag_handle,eco,edit,electrical_services,emoji_events,error,error_outline,event,expand_less,expand_more,facebook,fast_forward,fast_rewind,favorite,favorite_border,file_download,file_present,file_upload,filter_alt,filter_list,fingerprint,first_page,flag,flickr,flight,flooding,folder,folder_open,format_quote,format_size,forum,github,grid_view,group_add,groups,hearing,help,help_outline,highlight_off,history,home,hospital,hotel,hourglass_empty,hurricane,identification,image,info,info_outline,insights,instagram,keyboard,label,language,last_page,launch,lightbulb,lightbulb_outline,link,link_off,list,local_cafe,local_fire_department,local_gas_station,local_grocery_store,local_hospital,local_laundry_service,local_library,local_offer,local_parking,local_pharmacy,local_police,local_taxi,location_city,location_on,lock,lock_open,lock_outline,login,logout,loop,mail,mail_outline,map,masks,medical_services,menu,military_tech,more_horiz,more_vert,my_location,navigate_before,navigate_far_before,navigate_far_next,navigate_next,near_me,notifications,notifications_active,notifications_none,notifications_off,park,people,person,pets,phone,photo_camera,print,priority_high,public,push_pin,radio_button_unchecked,rain,reduce_capacity,remove,report,restaurant,rss_feed,safety_divider,sanitizer,save_alt,schedule,school,science,search,security,send,sentiment_dissatisfied,sentiment_neutral,sentiment_satisfied,sentiment_satisfied_alt,sentiment_very_dissatisfied,settings,severe_weather,share,shield,shopping_basket,snow,soap,social_distance,sort_arrow,spellcheck,star,star_half,star_outline,store,support,support_agent,text_fields,thumb_down_alt,thumb_up_alt,timer,toggle_off,toggle_on,topic,tornado,translate,trending_down,trending_up,twitter,undo,unfold_less,unfold_more,update,upload_file,verified,verified_user,visibility,visibility_off,volume_off,warning,wash,wifi,work,youtube,zoom_in,zoom_out,zoom_out_map]
+ # @param size select [~,md]
+ # @param color select [~,info,error,warning,success]
+ def workbench(icon: :cancel, size: 'md', color: 'error')
+ render(IconListComponent.new(icon:, size:, **{ color: }.compact)) do |c|
+ c.with_item { 'You cannot pass identity verification if your ID is expired.' }
+ c.with_item { 'You cannot use extension documents in place of an unexpired ID.' }
+ c.with_item { 'You cannot use a paper or temporary ID.' }
+ end
+ end
+ # rubocop:enable Layout/LineLength
+end