Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/components/badge_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= content_tag('div', **tag_options, class: ['lg-verification-badge', *tag_options[:class]]) do %>
<%= render IconComponent.new(icon:, class: 'text-success') %>
<%= content_tag('div', **tag_options, class: ['lg-verification-badge', border_css_class, *tag_options[:class]]) do %>
<%= render IconComponent.new(icon:, class: icon_css_class) %>
<%= content %>
<% end %>
21 changes: 21 additions & 0 deletions app/components/badge_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class BadgeComponent < BaseComponent
ICONS = %i[
lock
check_circle
warning
info
].to_set.freeze

attr_reader :icon, :tag_options
Expand All @@ -13,4 +15,23 @@ def initialize(icon:, **tag_options)
@icon = icon
@tag_options = tag_options
end

def color_token
case icon
when :check_circle, :lock
'success'
when :warning
'warning'
else
'info'
end
end

def border_css_class
"border-#{color_token}"
end

def icon_css_class
"text-#{color_token}"
end
end
14 changes: 14 additions & 0 deletions app/components/tooltip_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class TooltipComponent < BaseComponent
attr_reader :tooltip_text, :tag_options

def initialize(tooltip_text:, **tag_options)
@tooltip_text = tooltip_text
@tag_options = tag_options
end

def call
content_tag(:'lg-tooltip', content, **tag_options, 'tooltip-text': tooltip_text)
end
end
9 changes: 9 additions & 0 deletions app/components/tooltip_component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@forward 'usa-tooltip';

// The USWDS tooltip component does not handle child elements gracefully. Prevent non-interactive
// child elements from triggering event handlers to avoid errors.
//
// See: https://github.com/uswds/uswds/pull/5263#issuecomment-2191808834
lg-tooltip .usa-icon {
pointer-events: none;
}
1 change: 1 addition & 0 deletions app/components/tooltip_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@18f/identity-tooltip/tooltip-element';
1 change: 1 addition & 0 deletions app/controllers/accounts/connected_accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def show
@presenter = AccountShowPresenter.new(
decrypted_pii: nil,
sp_session_request_url: sp_session_request_url_with_updated_params,
authn_context: resolved_authn_context_result,
sp_name: decorated_sp_session.sp_name,
user: current_user,
locked_for_session: pii_locked_for_session?(current_user),
Expand Down
1 change: 1 addition & 0 deletions app/controllers/accounts/history_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def show
@presenter = AccountShowPresenter.new(
decrypted_pii: nil,
sp_session_request_url: sp_session_request_url_with_updated_params,
authn_context: resolved_authn_context_result,
sp_name: decorated_sp_session.sp_name,
user: current_user,
locked_for_session: pii_locked_for_session?(current_user),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def show
@presenter = AccountShowPresenter.new(
decrypted_pii: nil,
sp_session_request_url: sp_session_request_url_with_updated_params,
authn_context: resolved_authn_context_result,
sp_name: decorated_sp_session.sp_name,
user: current_user,
locked_for_session: pii_locked_for_session?(current_user),
Expand Down
1 change: 1 addition & 0 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def show
@presenter = AccountShowPresenter.new(
decrypted_pii: cacher.fetch(current_user.active_or_pending_profile&.id),
sp_session_request_url: sp_session_request_url_with_updated_params,
authn_context: resolved_authn_context_result,
sp_name: decorated_sp_session.sp_name,
user: current_user,
locked_for_session: pii_locked_for_session?(current_user),
Expand Down
1 change: 1 addition & 0 deletions app/controllers/events_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def show
@presenter = AccountShowPresenter.new(
decrypted_pii: nil,
sp_session_request_url: sp_session_request_url_with_updated_params,
authn_context: resolved_authn_context_result,
sp_name: decorated_sp_session.sp_name,
user: current_user,
locked_for_session: pii_locked_for_session?(current_user),
Expand Down
24 changes: 24 additions & 0 deletions app/javascript/packages/tooltip/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# `@18f/identity-tooltip`

Custom element for a tooltip component.

## Usage

Importing the element will register the `<lg-tooltip>` custom element:

```ts
import '@18f/identity-tooltip/tooltip-element';
```

The custom element will implement behaviors for showing tooltip text on hover or focus, but all markup must already exist.

> [!WARNING]
> Due to existing issues with the U.S. Web Design System Tooltip component, there are a few limitations to be aware of:
> 1. Content must be wrapped in a wrapper element, such as a `<span>` tag.
> 2. Any other nested child elements must be non-interactive, using `pointer-events: none;`.

```html
<lg-tooltip tooltip-text="Your identity has been verified.">
<span>Verified</span>
</lg-tooltip>
```
11 changes: 11 additions & 0 deletions app/javascript/packages/tooltip/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@18f/identity-tooltip",
"version": "1.0.0",
"private": true,
"dependencies": {
"@18f/identity-design-system": "^9.3.0"
},
"sideEffects": [
"./tooltip-element.ts"
]
}
27 changes: 27 additions & 0 deletions app/javascript/packages/tooltip/tooltip-element.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { screen, getByText, waitFor } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import { computeAccessibleDescription } from 'dom-accessibility-api';
import './tooltip-element';

describe('TooltipElement', () => {
function createAndConnectElement({ tooltipText = '', innerHTML = '<span>Verified</span>' } = {}) {
const element = document.createElement('lg-tooltip');
element.setAttribute('tooltip-text', tooltipText);
element.innerHTML = innerHTML;
document.body.appendChild(element);
return element;
}

it('initializes tooltip element', async () => {
const tooltipText = 'Your identity has been verified';
const element = createAndConnectElement({ tooltipText });

const content = getByText(element, 'Verified');

await userEvent.hover(content);
expect(computeAccessibleDescription(content)).to.be.equal(tooltipText);
await waitFor(() => {
expect(screen.getByText(tooltipText).classList.contains('is-visible')).to.be.true();
});
});
});
32 changes: 32 additions & 0 deletions app/javascript/packages/tooltip/tooltip-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { tooltip } from '@18f/identity-design-system';

class TooltipElement extends HTMLElement {
connectedCallback() {
this.tooltipElement.setAttribute('title', this.tooltipText);
this.tooltipElement.classList.add('usa-tooltip');
tooltip.on(this.tooltipElement);
}

get tooltipElement(): HTMLElement {
return this.firstElementChild as HTMLElement;
}

/**
* Retrieves the text to be shown in the tooltip.
*/
get tooltipText(): string {
return this.getAttribute('tooltip-text')!;
}
}

declare global {
interface HTMLElementTagNameMap {
'lg-tooltip': TooltipElement;
}
}

if (!customElements.get('lg-tooltip')) {
customElements.define('lg-tooltip', TooltipElement);
}

export default TooltipElement;
70 changes: 56 additions & 14 deletions app/presenters/account_show_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
# frozen_string_literal: true

class AccountShowPresenter
attr_reader :user, :decrypted_pii, :locked_for_session, :pii, :sp_session_request_url, :sp_name

def initialize(decrypted_pii:, sp_session_request_url:, sp_name:, user:,
locked_for_session:)
attr_reader :user,
:decrypted_pii,
:locked_for_session,
:pii,
:sp_session_request_url,
:authn_context,
:sp_name

delegate :identity_verified_with_biometric_comparison?, to: :user

def initialize(
decrypted_pii:,
sp_session_request_url:,
authn_context:,
sp_name:,
user:,
locked_for_session:
)
@decrypted_pii = decrypted_pii
@user = user
@sp_name = sp_name
@sp_session_request_url = sp_session_request_url
@authn_context = authn_context
@locked_for_session = locked_for_session
@pii = determine_pii
end
Expand All @@ -17,10 +32,6 @@ def show_password_reset_partial?
user.password_reset_profile.present?
end

def show_pii_partial?
decrypted_pii.present? || user.identity_verified?
end

def show_manage_personal_key_partial?
user.encrypted_recovery_code_digest.present? &&
user.password_reset_profile.blank?
Expand All @@ -30,14 +41,45 @@ def show_service_provider_continue_partial?
sp_name.present? && sp_session_request_url.present?
end

def show_gpo_partial?
def showing_alerts?
show_service_provider_continue_partial? ||
show_password_reset_partial?
end

def active_profile?
user.active_profile.present?
end

def active_profile_for_authn_context?
return @active_profile_for_authn_context if defined?(@active_profile_for_authn_context)

@active_profile_for_authn_context = active_profile? && (
!authn_context.biometric_comparison? || identity_verified_with_biometric_comparison?
)
end

def pending_idv?
authn_context.identity_proofing? && !active_profile_for_authn_context?
end

def pending_ipp?
user.pending_in_person_enrollment.present?
end

def pending_gpo?
user.gpo_verification_pending_profile?
end

def showing_any_partials?
show_service_provider_continue_partial? ||
show_password_reset_partial? ||
show_gpo_partial?
def show_idv_partial?
active_profile? || pending_idv? || pending_ipp? || pending_gpo?
end

def formatted_ipp_due_date
I18n.l(user.pending_in_person_enrollment.due_date, format: :event_date)
end

def formatted_nonbiometric_idv_date
I18n.l(user.active_profile.created_at, format: :event_date)
end

def show_unphishable_badge?
Expand Down Expand Up @@ -119,7 +161,7 @@ def decrypted_pii_accessor
end

def determine_pii
return PiiAccessor.new unless show_pii_partial?
return PiiAccessor.new unless active_profile?
if decrypted_pii.present? && !@locked_for_session
decrypted_pii_accessor
else
Expand Down
4 changes: 0 additions & 4 deletions app/views/accounts/_badges.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
<% if @presenter.show_unphishable_badge? %>
<%= render BadgeComponent.new(icon: :lock).with_content(t('headings.account.unphishable')) %>
<% end %>

<% if @presenter.show_verified_badge? %>
<%= render BadgeComponent.new(icon: :check_circle).with_content(t('headings.account.verified_account')) %>
<% end %>
Loading