diff --git a/app/helpers/script_helper.rb b/app/helpers/script_helper.rb
index 3e3ca632bc2..4ab9708d705 100644
--- a/app/helpers/script_helper.rb
+++ b/app/helpers/script_helper.rb
@@ -31,6 +31,10 @@ def render_javascript_pack_once_tags(*names)
private
+ SAME_ORIGIN_ASSETS = %w[
+ identity-style-guide/dist/assets/img/sprite.svg
+ ].to_set.freeze
+
def local_crossorigin_sources?
Rails.env.development? && ENV['WEBPACK_PORT'].present?
end
@@ -38,7 +42,7 @@ def local_crossorigin_sources?
def javascript_assets_tag(*names)
assets = AssetSources.get_assets(*names)
if assets.present?
- asset_map = assets.index_with { |path| asset_path(path) }
+ asset_map = assets.index_with { |path| asset_path(path, host: asset_host(path)) }
content_tag(
:script,
asset_map.to_json,
@@ -63,5 +67,17 @@ def without_preload_links_header
ActionView::Helpers::AssetTagHelper.preload_links_header = original_preload_links_header
result
end
+
+ def asset_host(path)
+ if IdentityConfig.store.asset_host.present?
+ if SAME_ORIGIN_ASSETS.include?(path)
+ IdentityConfig.store.domain_name
+ else
+ IdentityConfig.store.asset_host
+ end
+ elsif request
+ request.base_url
+ end
+ end
end
# rubocop:enable Rails/HelperInstanceVariable
diff --git a/app/javascript/packages/clipboard-button/clipboard-button.spec.tsx b/app/javascript/packages/clipboard-button/clipboard-button.spec.tsx
index 3b558db0357..844195a379a 100644
--- a/app/javascript/packages/clipboard-button/clipboard-button.spec.tsx
+++ b/app/javascript/packages/clipboard-button/clipboard-button.spec.tsx
@@ -22,4 +22,13 @@ describe('ClipboardButton', () => {
expect(button.closest('lg-clipboard-button')).to.exist();
expect(button.classList.contains('usa-button--outline')).to.be.true();
});
+
+ it('renders with print icon', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(icon.classList.contains('usa-icon')).to.be.true();
+ expect(icon.querySelector('use[href$="#content_copy"]'));
+ });
});
diff --git a/app/javascript/packages/clipboard-button/clipboard-button.tsx b/app/javascript/packages/clipboard-button/clipboard-button.tsx
index 6c8a71787c0..a2dd5669a03 100644
--- a/app/javascript/packages/clipboard-button/clipboard-button.tsx
+++ b/app/javascript/packages/clipboard-button/clipboard-button.tsx
@@ -23,7 +23,9 @@ interface ClipboardButtonProps {
function ClipboardButton({ clipboardText, ...buttonProps }: ClipboardButtonProps & ButtonProps) {
return (
-
+
);
}
diff --git a/app/javascript/packages/components/button.spec.tsx b/app/javascript/packages/components/button.spec.tsx
index 82bc61181cd..94b9882eabd 100644
--- a/app/javascript/packages/components/button.spec.tsx
+++ b/app/javascript/packages/components/button.spec.tsx
@@ -3,7 +3,7 @@ import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import Button from './button';
-describe('document-capture/components/button', () => {
+describe('Button', () => {
it('renders with default props', () => {
const { getByText } = render();
@@ -119,4 +119,13 @@ describe('document-capture/components/button', () => {
expect(button.classList.contains('my-button')).to.be.true();
});
+
+ it('renders icon', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(icon.classList.contains('usa-icon')).to.be.true();
+ expect(icon.querySelector('use')!.getAttribute('href')).to.match(/#add$/);
+ });
});
diff --git a/app/javascript/packages/components/button.tsx b/app/javascript/packages/components/button.tsx
index 0ac53b08c89..9741325e3bc 100644
--- a/app/javascript/packages/components/button.tsx
+++ b/app/javascript/packages/components/button.tsx
@@ -1,5 +1,7 @@
-import { createElement, MouseEvent, ReactNode } from 'react';
-import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
+import { createElement } from 'react';
+import type { AnchorHTMLAttributes, ButtonHTMLAttributes, MouseEvent, ReactNode } from 'react';
+import Icon from './icon';
+import type { DesignSystemIcon } from './icon';
type ButtonType = 'button' | 'reset' | 'submit';
@@ -54,6 +56,11 @@ export interface ButtonProps {
*/
isUnstyled?: boolean;
+ /**
+ * Icon to show next to button text.
+ */
+ icon?: DesignSystemIcon;
+
/**
* Optional additional class names.
*/
@@ -70,6 +77,7 @@ function Button({
isOutline,
isDisabled,
isUnstyled,
+ icon,
className,
...htmlAttributes
}: ButtonProps &
@@ -92,6 +100,7 @@ function Button({
return createElement(
tagName,
{ type, href, disabled: isDisabled, className: classes, ...htmlAttributes },
+ icon && ,
children,
);
}
diff --git a/app/javascript/packages/components/icon.jsx b/app/javascript/packages/components/icon.jsx
deleted file mode 100644
index 4ff419999a0..00000000000
--- a/app/javascript/packages/components/icon.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @typedef IconProps
- *
- * @prop {string=} className Optional class name to apply to SVG element.
- */
-
-/**
- * Creates a new icon component, accepting common props, and applying common wrapping elements.
- *
- * @param {string} path SVG icon path definition.
- *
- * @return {import('react').FunctionComponent}
- */
-function createIconComponent(path) {
- return ({ className }) => (
-
- );
-}
-
-const Icon = {
- Camera: createIconComponent(
- 'M512 144v288c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V144c0-26.5 21.5-48 48-48h88l12.3-32.9c7-18.7 24.9-31.1 44.9-31.1h125.5c20 0 37.9 12.4 44.9 31.1L376 96h88c26.5 0 48 21.5 48 48zM376 288c0-66.2-53.8-120-120-120s-120 53.8-120 120 53.8 120 120 120 120-53.8 120-120zm-32 0c0 48.5-39.5 88-88 88s-88-39.5-88-88 39.5-88 88-88 88 39.5 88 88z',
- ),
-};
-
-export default Icon;
diff --git a/app/javascript/packages/components/icon.spec.tsx b/app/javascript/packages/components/icon.spec.tsx
new file mode 100644
index 00000000000..e093cef583f
--- /dev/null
+++ b/app/javascript/packages/components/icon.spec.tsx
@@ -0,0 +1,26 @@
+import { render } from '@testing-library/react';
+import Icon from './icon';
+
+describe('Icon', () => {
+ it('renders given icon', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(icon.classList.contains('usa-icon')).to.be.true();
+ expect(icon.querySelector('use')!.getAttribute('href')).to.match(/#add$/);
+ });
+
+ context('with className prop', () => {
+ it('renders with additional CSS class', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(Array.from(icon.classList.values())).to.include.members([
+ 'usa-icon',
+ 'my-custom-class',
+ ]);
+ });
+ });
+});
diff --git a/app/javascript/packages/components/icon.tsx b/app/javascript/packages/components/icon.tsx
new file mode 100644
index 00000000000..a6867ac5194
--- /dev/null
+++ b/app/javascript/packages/components/icon.tsx
@@ -0,0 +1,296 @@
+import type { ComponentType } from 'react';
+import { getAssetPath } from '@18f/identity-assets';
+
+const SPRITE_URL = getAssetPath('identity-style-guide/dist/assets/img/sprite.svg');
+
+export type DesignSystemIcon =
+ | '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';
+
+interface BaseIconProps {
+ /**
+ * Optional class name to apply to SVG element.
+ */
+ className?: string;
+}
+
+interface IconProps extends BaseIconProps {
+ icon: DesignSystemIcon;
+}
+
+interface CustomIconProps extends BaseIconProps {}
+
+/**
+ * Creates a new icon component, accepting common props, and applying common wrapping elements.
+ *
+ * @param path SVG icon path definition.
+ */
+function createIconComponent(path: string): ComponentType {
+ return ({ className }) => (
+
+ );
+}
+
+function Icon({ icon, className }: IconProps) {
+ const classes = ['usa-icon', className].filter(Boolean).join(' ');
+
+ return (
+
+ );
+}
+
+Icon.Camera = createIconComponent(
+ 'M512 144v288c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V144c0-26.5 21.5-48 48-48h88l12.3-32.9c7-18.7 24.9-31.1 44.9-31.1h125.5c20 0 37.9 12.4 44.9 31.1L376 96h88c26.5 0 48 21.5 48 48zM376 288c0-66.2-53.8-120-120-120s-120 53.8-120 120 53.8 120 120 120 120-53.8 120-120zm-32 0c0 48.5-39.5 88-88 88s-88-39.5-88-88 39.5-88 88-88 88 39.5 88 88z',
+);
+
+export default Icon;
diff --git a/app/javascript/packages/print-button/print-button.spec.tsx b/app/javascript/packages/print-button/print-button.spec.tsx
index aeb85206a28..65a72e45de8 100644
--- a/app/javascript/packages/print-button/print-button.spec.tsx
+++ b/app/javascript/packages/print-button/print-button.spec.tsx
@@ -32,4 +32,13 @@ describe('PrintButton', () => {
expect(button.closest('lg-print-button')).to.exist();
expect(button.classList.contains('usa-button--outline')).to.be.true();
});
+
+ it('renders with print icon', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(icon.classList.contains('usa-icon')).to.be.true();
+ expect(icon.querySelector('use[href$="#print"]'));
+ });
});
diff --git a/app/javascript/packages/print-button/print-button.tsx b/app/javascript/packages/print-button/print-button.tsx
index 889cbefa332..194348200d7 100644
--- a/app/javascript/packages/print-button/print-button.tsx
+++ b/app/javascript/packages/print-button/print-button.tsx
@@ -16,7 +16,9 @@ declare global {
function PrintButton(buttonProps: ButtonProps) {
return (
-
+
);
}
diff --git a/app/javascript/packages/verify-flow/steps/personal-key/download-button.spec.tsx b/app/javascript/packages/verify-flow/steps/personal-key/download-button.spec.tsx
index 4ebeeafb009..23e41e1c5a6 100644
--- a/app/javascript/packages/verify-flow/steps/personal-key/download-button.spec.tsx
+++ b/app/javascript/packages/verify-flow/steps/personal-key/download-button.spec.tsx
@@ -14,6 +14,15 @@ describe('DownloadButton', () => {
expect(link.getAttribute('href')).to.equal('data:,Hello%2C%20world!');
});
+ it('renders with download icon', () => {
+ const { getByRole } = render();
+
+ const icon = getByRole('img', { hidden: true });
+
+ expect(icon.classList.contains('usa-icon')).to.be.true();
+ expect(icon.querySelector('use[href$="#file_download"]'));
+ });
+
it('does not prevent default when clicked', () => {
const { getByRole } = render();
diff --git a/app/javascript/packages/verify-flow/steps/personal-key/download-button.tsx b/app/javascript/packages/verify-flow/steps/personal-key/download-button.tsx
index 3c1e26be1cd..66ea4bd8ade 100644
--- a/app/javascript/packages/verify-flow/steps/personal-key/download-button.tsx
+++ b/app/javascript/packages/verify-flow/steps/personal-key/download-button.tsx
@@ -36,6 +36,7 @@ function DownloadButton({ content, fileName, ...buttonProps }: DownloadButtonPro
return (