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
34 changes: 32 additions & 2 deletions app/assets/stylesheets/components/_btn.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,46 @@
}

.usa-button:disabled.usa-button--active,
.usa-button--disabled.usa-button--active {
[aria-disabled='true'].usa-button--active {
&:not(
.usa-button--unstyled,
.usa-button--secondary,
.usa-button--accent-cool,
.usa-button--accent-warm,
.usa-button--base,
.usa-button--outline,
.usa-button--inverse
.usa-button--inverse,
.usa-button--danger
) {
@include set-text-and-bg('primary-darker', $context: 'Button');
}
}

// Upstream: https://github.com/18F/identity-design-system/pull/383
.usa-button--danger.usa-button--outline {
&:not(:disabled, [aria-disabled='true']) {
background-color: color('white');
box-shadow: inset 0 0 0 $theme-button-stroke-width color('secondary');
color: color('secondary');

&:hover,
&.usa-button--hover {
background-color: color('secondary-lightest');
box-shadow: inset 0 0 0 $theme-button-stroke-width color('secondary-dark');
color: color('secondary-dark');
}
}

&:active,
&.usa-button--active {
&,
&:focus,
&.usa-button--focus,
&:hover,
&.usa-button--hover {
background-color: color('secondary-lighter');
box-shadow: inset 0 0 0 $theme-button-stroke-width color('secondary-darker');
color: color('secondary-darker');
}
}
}
120 changes: 120 additions & 0 deletions app/components/manageable_authenticator_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<%= content_tag(
:'lg-manageable-authenticator',
**tag_options,
'api-url': manage_api_url,
'configuration-name': configuration.name,
'unique-id': unique_id,
'reauthenticate-at': reauthenticate_at.iso8601,
'reauthentication-url': reauthentication_url,
) do %>
<%= content_tag(
:script,
strings.
slice(:renamed, :delete_confirm, :deleted).
transform_keys { |key| key.to_s.camelcase(:lower) }.
to_json,
{
type: 'application/json',
class: 'manageable-authenticator__strings',
},
false,
) %>
<%= tag.div(
class: 'manageable-authenticator__edit',
tabindex: '-1',
role: 'group',
aria: { labelledby: "manageable-authenticator-manage-accessible-label-#{unique_id}" },
) do %>
<%= render AlertComponent.new(
type: :success,
class: 'manageable-authenticator__alert',
tabindex: '-1',
) %>
<form class="manageable-authenticator__rename">
<%= tag.input(
class: 'manageable-authenticator__rename-input usa-input width-full',
aria: { label: t('components.manageable_authenticator.nickname') },
value: configuration.name,
) %>
<div class="display-flex margin-top-205">
<%= render SpinnerButtonComponent.new(
type: :submit,
wrapper_options: { class: 'manageable-authenticator__save-rename-button' },
action_message: t('components.manageable_authenticator.saving'),
long_wait_duration: Float::INFINITY,
).with_content(t('components.manageable_authenticator.save')) %>
<%= render ButtonComponent.new(
type: :button,
outline: true,
class: 'margin-left-1 manageable-authenticator__cancel-rename-button',
).with_content(t('components.manageable_authenticator.cancel')) %>
</div>
</form>
<div class="manageable-authenticator__details">
<span class="usa-sr-only"><%= t('components.manageable_authenticator.nickname') %>:</span>
<strong class="manageable-authenticator__name manageable-authenticator__details-name">
<%= configuration.name %>
</strong>
<div>
<%= t(
'components.manageable_authenticator.created_on',
date: l(configuration.created_at, format: :event_date),
) %>
</div>
<div class="grid-row margin-top-205">
<div class="grid-col-auto grid-row display-flex">
<%= render ButtonComponent.new(
type: :button,
class: 'manageable-authenticator__rename-button',
).with_content(t('components.manageable_authenticator.rename')) %>
<%= render SpinnerButtonComponent.new(
type: :button,
danger: true,
outline: true,
wrapper_options: { class: 'manageable-authenticator__delete-button' },
class: 'margin-left-1',
action_message: t('components.manageable_authenticator.deleting'),
long_wait_duration: Float::INFINITY,
).with_content(t('components.manageable_authenticator.delete')) %>
</div>
<div class="grid-col-fill text-right">
<%= render ButtonComponent.new(
type: :button,
outline: true,
class: 'manageable-authenticator__done-button',
).with_content(t('components.manageable_authenticator.done')) %>
</div>
</div>
</div>
<% end %>
<div class="manageable-authenticator__summary">
<div class="manageable-authenticator__name manageable-authenticator__summary-name"><%= configuration.name %></div>
<div class="manageable-authenticator__actions">
<%= render ButtonComponent.new(
action: ->(**tag_options, &block) { link_to(manage_url, **tag_options, &block) },
type: :button,
unstyled: true,
class: 'no-js',
) do %>
<span aria-hidden="true">
<%= t('components.manageable_authenticator.manage') %>
</span>
<span class="usa-sr-only">
<%= strings[:manage_accessible_label] %>: <%= tag.span(configuration.name, class: 'manageable-authenticator__name') %>
</span>
<% end %>
<%= render ButtonComponent.new(
type: :button,
unstyled: true,
class: 'js manageable-authenticator__manage-button',
) do %>
<span aria-hidden="true">
<%= t('components.manageable_authenticator.manage') %>
</span>
<span class="usa-sr-only" id="manageable-authenticator-manage-accessible-label-<%= unique_id %>">
<%= strings[:manage_accessible_label] %>: <%= tag.span(configuration.name, class: 'manageable-authenticator__name') %>
</span>
<% end %>
</div>
</div>
<% end %>
57 changes: 57 additions & 0 deletions app/components/manageable_authenticator_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class ManageableAuthenticatorComponent < BaseComponent
attr_reader :configuration,
:user_session,
:manage_api_url,
:manage_url,
:custom_strings,
:tag_options

def initialize(
configuration:,
user_session:,
manage_api_url:,
manage_url:,
custom_strings: {},
**tag_options
)
if ![:name, :id, :created_at].all? { |method| configuration.respond_to?(method) }
raise ArgumentError, '`configuration` must respond to `name`, `id`, `created_at`'
end

@configuration = configuration
@user_session = user_session
@manage_api_url = manage_api_url
@manage_url = manage_url
@custom_strings = custom_strings
@tag_options = tag_options
end

def reauthentication_url
account_reauthentication_path(manage_authenticator: unique_id)
end

def unique_id
@unique_id ||= [configuration.class.name.downcase, configuration.id].join('-')
end

def strings
default_strings.merge(custom_strings)
end

delegate :reauthenticate_at, to: :auth_methods_session

private

def auth_methods_session
@auth_methods_session ||= AuthMethodsSession.new(user_session:)
end

def default_strings
{
renamed: t('components.manageable_authenticator.renamed'),
delete_confirm: t('components.manageable_authenticator.delete_confirm'),
deleted: t('components.manageable_authenticator.deleted'),
manage_accessible_label: t('components.manageable_authenticator.manage_accessible_label'),
}
end
end
77 changes: 77 additions & 0 deletions app/components/manageable_authenticator_component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
@use 'uswds-core' as *;

.manageable-authenticator__edit {
@include u-margin-y(1.5);
@include u-padding(2);
@include u-border(1px, 'primary-light');
@include u-radius('lg');
display: none;

.manageable-authenticator--editing & {
display: block;
}

.manageable-authenticator--deleted & {
@include u-padding(0);
@include u-border(0);
}

&:focus:not(:focus-visible) {
outline: none;
}
}

.manageable-authenticator__alert {
@include u-margin-bottom(2);
display: none;

.manageable-authenticator--alert-visible & {
display: block;
}

&:focus:not(:focus-visible) {
outline: none;
}
}

.manageable-authenticator__rename {
display: none;

.manageable-authenticator--renaming & {
display: block;
}
}

.manageable-authenticator__details {
.manageable-authenticator--renaming &,
.manageable-authenticator--deleted & {
display: none;
}
}

.manageable-authenticator__details-name {
display: block;
}

.manageable-authenticator__summary {
@include grid-row;
@include u-padding(1);
@include u-border(1px, 'primary-light');

lg-manageable-authenticator + lg-manageable-authenticator & {
border-top: none;
}

.manageable-authenticator--editing &,
.manageable-authenticator--deleted & {
display: none;
}
}

.manageable-authenticator__summary-name {
@include grid-col('fill');
}

.manageable-authenticator__actions {
@include grid-col('auto');
}
1 change: 1 addition & 0 deletions app/components/manageable_authenticator_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@18f/identity-manageable-authenticator/manageable-authenticator-element';
8 changes: 7 additions & 1 deletion app/components/spinner_button_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<%= content_tag(:'lg-spinner-button', class: css_class, 'spin-on-click': spin_on_click) do %>
<%= content_tag(
:'lg-spinner-button',
**wrapper_options,
class: css_class,
'spin-on-click': spin_on_click,
'long-wait-duration-ms': long_wait_duration.in_milliseconds,
) do %>
<%= render ButtonComponent.new(type: :button, **button_options) do %>
<span class="spinner-button__content"><%= content %></span>
<span class="spinner-dots spinner-dots--centered" aria-hidden="true">
Expand Down
26 changes: 23 additions & 3 deletions app/components/spinner_button_component.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
class SpinnerButtonComponent < BaseComponent
attr_reader :action_message, :button_options, :outline, :spin_on_click
DEFAULT_LONG_WAIT_DURATION = 15.seconds

attr_reader :action_message,
:button_options,
:outline,
:long_wait_duration,
:spin_on_click,
:wrapper_options

# @param [String] action_message Message describing the action being performed, shown visually to
# users when the animation has been active for a long time, and
# immediately to users of assistive technology.
def initialize(action_message: nil, spin_on_click: nil, **button_options)
# @param [Boolean] spin_on_click Whether to start the spinning animation immediately on click.
# @param [ActiveSupport::Duration] long_wait_duration Time until the action message becomes
# visible.
def initialize(
action_message: nil,
spin_on_click: nil,
long_wait_duration: DEFAULT_LONG_WAIT_DURATION,
wrapper_options: {},
**button_options
)
@action_message = action_message
@button_options = button_options
@outline = button_options[:outline]
@long_wait_duration = long_wait_duration
@spin_on_click = spin_on_click
@wrapper_options = wrapper_options
end

def css_class
'spinner-button--outline' if outline
classes = [*wrapper_options[:class]]
classes << 'spinner-button--outline' if outline
classes
end
end
8 changes: 4 additions & 4 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ def show
)
end

# This action is used to re-authenticate when PII on the account page is locked on `show` action
# This allows users to view their PII after reauthenticating their MFA.

def reauthentication
user_session[:stored_location] = account_url
# This route sends a user through reauthentication and returns them to the account page, since
# some actions within the account dashboard require a fresh reauthentication (e.g. managing an
# MFA method or viewing verified profile information).
user_session[:stored_location] = account_url(params.permit(:manage_authenticator))
user_session[:context] = 'reauthentication'

redirect_to login_two_factor_options_path
Expand Down
15 changes: 15 additions & 0 deletions app/javascript/packages/manageable-authenticator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# `@18f/identity-manageable-authenticator`

Custom element for inline management of an authentication method.

## Usage

### Custom Element

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

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

The custom element will implement all JavaScript behaviors for the element, but the `<lg-manageable-authenticator>` markup must already exist.
Loading