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 (