Skip to content

Commit

Permalink
Toggle switch error messages (#1760)
Browse files Browse the repository at this point in the history
Co-authored-by: Neal Lindsay <neal.lindsay@gmail.com>
Co-authored-by: camertron <camertron@users.noreply.github.com>
Co-authored-by: Katie Langerman <18661030+langermank@users.noreply.github.com>
  • Loading branch information
4 people authored Jan 18, 2023
1 parent c9cb95c commit fdd7bc1
Show file tree
Hide file tree
Showing 17 changed files with 114 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-jokes-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/view-components': patch
---

Show error messages when toggle switches fail
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
],
"custom-elements/tag-name-matches-class": [
"error", {"suffix": "Element"}
]
],
"i18n-text/no-en": "off"
}
}
]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions app/components/primer/alpha/text_field.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@
}
}

.FormControl-toggleSwitchInput {
display: flex;
align-items: flex-start;
gap: var(--base-size-16, 16px);
}

/* positioning for leading/trailing items for TextInput */
.FormControl-input-wrap {
position: relative;
Expand Down
6 changes: 4 additions & 2 deletions app/components/primer/alpha/toggle_switch.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<%= render(Primer::BaseComponent.new(tag: "toggle-switch", **@system_arguments)) do %>
<%= render(Primer::Beta::Octicon.new(size: :small, color: :danger, icon: :alert, hidden: "true", data: { target: "toggle-switch.errorIcon" })) %>
<%= render(Primer::Beta::Spinner.new(size: :small, hidden: "true", data: { target: "toggle-switch.loadingSpinner" })) %>
<span class="ToggleSwitch-statusIcon">
<%= render(Primer::Beta::Octicon.new(size: :small, color: :danger, icon: :alert, hidden: "true", data: { target: "toggle-switch.errorIcon" })) %>
<%= render(Primer::Beta::Spinner.new(size: :small, hidden: "true", data: { target: "toggle-switch.loadingSpinner" })) %>
</span>
<%= render(Primer::Beta::Text.new(aria: { hidden: true }, classes: "ToggleSwitch-status")) do %>
<%= render(Primer::Box.new(classes: "ToggleSwitch-statusOn").with_content("On")) %>
<%= render(Primer::Box.new(classes: "ToggleSwitch-statusOff").with_content("Off")) %>
Expand Down
6 changes: 6 additions & 0 deletions app/components/primer/alpha/toggle_switch.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@
text-align: right;
}

.ToggleSwitch-statusIcon {
width: var(--base-size-16, 16px);
display: flex;
margin-top: 0.063rem;
}

.ToggleSwitch--small {
& .ToggleSwitch-status {
font-size: var(--primer-text-body-size-small, 12px);
Expand Down
38 changes: 27 additions & 11 deletions app/components/primer/alpha/toggle_switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,16 @@ class ToggleSwitchElement extends HTMLElement {
}

private setSuccessState(): void {
const event = new CustomEvent('toggleSwitchSuccess', {bubbles: true})
this.dispatchEvent(event)

this.setFinishedState(false)
}

private setErrorState(): void {
private setErrorState(message: string): void {
const event = new CustomEvent('toggleSwitchError', {bubbles: true, detail: message})
this.dispatchEvent(event)

this.setFinishedState(true)
}

Expand All @@ -125,22 +131,32 @@ class ToggleSwitchElement extends HTMLElement {

try {
if (!this.src) throw new Error('invalid src')
const response = await fetch(this.src, {
credentials: 'same-origin',
method: 'POST',
headers: {
'Requested-With': 'XMLHttpRequest'
},
body
})

let response

try {
response = await fetch(this.src, {
credentials: 'same-origin',
method: 'POST',
headers: {
'Requested-With': 'XMLHttpRequest'
},
body
})
} catch (error) {
throw new Error('A network error occurred, please try again.')
}

if (response.ok) {
this.setSuccessState()
this.performToggle()
} else {
this.setErrorState()
throw new Error(await response.text())
}
} catch (error) {
this.setErrorState()
if (error instanceof Error) {
this.setErrorState(error.message || 'An error occurred, please try again.')
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/components/primer/primer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ import './alpha/tab_container'
import './time_ago_component'
import '../../../lib/primer/forms/primer_multi_input'
import '../../../lib/primer/forms/primer_text_field'
import '../../../lib/primer/forms/toggle_switch_input'
2 changes: 1 addition & 1 deletion app/forms/example_toggle_switch_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
# :nodoc:
class ExampleToggleSwitchForm < Primer::Forms::ToggleSwitchForm
def initialize(**system_arguments)
super(name: :example_field, label: "Example", **system_arguments)
super(name: :example_field, label: "Example", caption: "This is an example toggle switch.", **system_arguments)
end
end
2 changes: 1 addition & 1 deletion demo/app/controllers/toggle_switch_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def verify_artificial_authenticity_token
# if provided, check token
return if form_params[:authenticity_token] == "let_me_in"

head :unauthorized
render status: :unauthorized, plain: "Bad CSRF token"
end

def form_params
Expand Down
9 changes: 7 additions & 2 deletions lib/primer/forms/toggle_switch.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<%= content_tag(:div, **@form_group_arguments) do %>
<%= content_tag("toggle-switch-input", **@input.input_arguments) do %>
<span style="flex-grow: 1">
<%= builder.label(@input.name, **@input.label_arguments) do %>
<%= @input.label %>
<% end %>
<%= render(Caption.new(input: @input)) %>
<%= content_tag(:div, data: { target: "toggle-switch-input.validationElement" }, **@input.validation_arguments) do %>
<%= content_tag(:span, @input.validation_messages.first, data: { target: "toggle-switch-input.validationMessageElement" }, **@input.validation_message_arguments) %>
<% end %>

<div><%= render(Caption.new(input: @input)) %></div>
</span>
<%
csrf = @input.csrf || @view_context.form_authenticity_token(
Expand Down
6 changes: 2 additions & 4 deletions lib/primer/forms/toggle_switch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ class ToggleSwitch < BaseComponent
def initialize(input:)
@input = input
@input.add_label_classes("FormControl-label")

@form_group_arguments = { class: "d-flex" }

@form_group_arguments[:hidden] = "hidden" if @input.hidden?
@input.add_input_classes("FormControl-toggleSwitchInput")
@input.input_arguments[:hidden] = "hidden" if @input.hidden?
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/primer/forms/toggle_switch_input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {controller, target} from '@github/catalyst'

@controller
export class ToggleSwitchInputElement extends HTMLElement {
@target validationElement: HTMLElement
@target validationMessageElement: HTMLElement

connectedCallback() {
this.addEventListener('toggleSwitchError', (event: Event) => {
this.validationMessageElement.innerText = (event as CustomEvent).detail
this.validationElement.removeAttribute('hidden')
})

this.addEventListener('toggleSwitchSuccess', () => {
this.validationMessageElement.innerText = ''
this.validationElement.setAttribute('hidden', 'hidden')
})
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
<%= render(ExampleToggleSwitchForm.new(csrf: "let_me_in", src: toggle_switch_index_path)) %>
<%= render(ExampleToggleSwitchForm.new(csrf: "let_me_in", src: toggle_switch_index_path, id: "success-toggle")) %>
<hr>
<%= render(ExampleToggleSwitchForm.new(csrf: "a_bad_value", src: toggle_switch_index_path, id: "error-toggle")) %>
3 changes: 2 additions & 1 deletion test/css/component_specific_selectors_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class ComponentSpecificSelectorsTest < Minitest::Test
".FormControl-input-wrap",
".FormControl-select-wrap",
".FormControl-checkbox-wrap",
".FormControl-radio-wrap"
".FormControl-radio-wrap",
".FormControl-toggleSwitchInput"
],
Primer::Alpha::ButtonMarketing => [
".btn-mktg.disabled",
Expand Down
28 changes: 28 additions & 0 deletions test/lib/primer/forms/integration_forms_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,33 @@ def test_multi_submit
assert result["country"], "CA"
assert result["region"], "SK"
end

def test_toggle_switch_form_errors
visit_preview(:example_toggle_switch_form)

find("#error-toggle toggle-switch").click
wait_for_toggle_switch_spinner

assert_selector("#error-toggle [data-target='toggle-switch.errorIcon']")
assert_selector("#error-toggle", text: "Bad CSRF token")

page.evaluate_script(<<~JAVASCRIPT)
document
.querySelector('#error-toggle toggle-switch')
.setAttribute('csrf', 'let_me_in');
JAVASCRIPT

find("#error-toggle toggle-switch").click
wait_for_toggle_switch_spinner

refute_selector("#error-toggle [data-target='toggle-switch.errorIcon']")
refute_selector("#error-toggle", text: "Bad CSRF token")
end

private

def wait_for_toggle_switch_spinner
refute_selector("[data-target='toggle-switch.loadingSpinner']")
end
end
end

0 comments on commit fdd7bc1

Please sign in to comment.