-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* WIP * docs: build docs * Adding previews and controller * Copy over tests * Fix tests * Fix linting issues * Fix erb lint issues * Remove erblint comment * Add tests for csrf token * Actually use form_params in controller * Adjust comments * build Co-authored-by: Actions Auto Build <actions@github.com> Co-authored-by: Jon Rohan <yes@jonrohan.codes>
- Loading branch information
1 parent
b89ac4d
commit f18ef0f
Showing
25 changed files
with
543 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@primer/view-components": patch | ||
--- | ||
|
||
Adding Primer::Alpha::ToggleSwitch component |
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
/* eslint-disable custom-elements/expose-class-on-global */ | ||
/* eslint-disable custom-elements/define-tag-after-class-definition */ | ||
|
||
import {controller, target} from '@github/catalyst' | ||
import {debounce} from '@github/mini-throttle/decorators' | ||
|
||
@controller | ||
export class ToggleSwitchElement extends HTMLElement { | ||
@target switch: HTMLElement | ||
@target loadingSpinner: HTMLElement | ||
@target errorIcon: HTMLElement | ||
|
||
get src(): string | null { | ||
const src = this.getAttribute('src') | ||
if (!src) return null | ||
|
||
const link = this.ownerDocument.createElement('a') | ||
link.href = src | ||
return link.href | ||
} | ||
|
||
get csrf(): string | null { | ||
const csrfElement = this.querySelector('[data-csrf]') | ||
return this.getAttribute('csrf') || (csrfElement instanceof HTMLInputElement && csrfElement.value) || null | ||
} | ||
|
||
get csrfField(): string { | ||
// the authenticity token is passed into the element and is not generated in js land | ||
|
||
return this.getAttribute('csrf-field') || 'authenticity_token' | ||
} | ||
|
||
isRemote(): boolean { | ||
return this.src != null | ||
} | ||
|
||
toggle() { | ||
if (this.isRemote()) { | ||
this.setLoadingState() | ||
this.check() | ||
} else { | ||
this.performToggle() | ||
} | ||
} | ||
|
||
turnOn(): void { | ||
if (this.isDisabled()) { | ||
return | ||
} | ||
|
||
this.switch.setAttribute('aria-checked', 'true') | ||
this.classList.add('ToggleSwitch--checked') | ||
} | ||
|
||
turnOff(): void { | ||
if (this.isDisabled()) { | ||
return | ||
} | ||
|
||
this.switch.setAttribute('aria-checked', 'false') | ||
this.classList.remove('ToggleSwitch--checked') | ||
} | ||
|
||
isOn(): boolean { | ||
return this.switch.getAttribute('aria-checked') === 'true' | ||
} | ||
|
||
isOff(): boolean { | ||
return !this.isOn() | ||
} | ||
|
||
isDisabled(): boolean { | ||
return this.switch.getAttribute('aria-disabled') === 'true' | ||
} | ||
|
||
disable(): void { | ||
this.switch.setAttribute('aria-disabled', 'true') | ||
} | ||
|
||
enable(): void { | ||
this.switch.setAttribute('aria-disabled', 'false') | ||
} | ||
|
||
private performToggle(): void { | ||
if (this.isOn()) { | ||
this.turnOff() | ||
} else { | ||
this.turnOn() | ||
} | ||
} | ||
|
||
private setLoadingState(): void { | ||
this.disable() | ||
this.errorIcon.setAttribute('hidden', 'hidden') | ||
this.loadingSpinner.removeAttribute('hidden') | ||
} | ||
|
||
private setSuccessState(): void { | ||
this.setFinishedState(false) | ||
} | ||
|
||
private setErrorState(): void { | ||
this.setFinishedState(true) | ||
} | ||
|
||
private setFinishedState(error: boolean): void { | ||
if (error) { | ||
this.errorIcon.removeAttribute('hidden') | ||
} | ||
|
||
this.loadingSpinner.setAttribute('hidden', 'hidden') | ||
this.enable() | ||
} | ||
|
||
@debounce(300) | ||
private async check() { | ||
const body = new FormData() | ||
|
||
if (this.csrf) { | ||
body.append(this.csrfField, this.csrf) | ||
} | ||
|
||
body.append('value', this.isOn() ? '1' : '0') | ||
|
||
try { | ||
const response = await fetch(this.src!, { | ||
credentials: 'same-origin', | ||
method: 'POST', | ||
body | ||
}) | ||
if (response.ok) { | ||
this.setSuccessState() | ||
this.performToggle() | ||
} else { | ||
this.setErrorState() | ||
} | ||
} catch (error) { | ||
this.setErrorState() | ||
} | ||
} | ||
} | ||
|
||
declare global { | ||
interface Window { | ||
ToggleSwitchElement: typeof ToggleSwitchElement | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<%= render(Primer::BaseComponent.new(tag: "toggle-switch", **@system_arguments)) do %> | ||
<%= render(Primer::OcticonComponent.new(size: :small, color: :danger, icon: :alert, hidden: "true", data: { target: "toggle-switch.errorIcon" })) %> | ||
<%= render(Primer::SpinnerComponent.new(size: :small, hidden: "true", data: { target: "toggle-switch.loadingSpinner" })) %> | ||
<%= 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")) %> | ||
<% end %> | ||
<%= render(Primer::BaseComponent.new(tag: :button, classes: "ToggleSwitch-track", role: "switch", data: { target: "toggle-switch.switch", action: "click:toggle-switch#toggle" }, aria: { checked: on?, disabled: disabled?, label: "Switch" })) do %> | ||
<%= render(Primer::Box.new(classes: "ToggleSwitch-icons", aria: { hidden: true })) do %> | ||
<%= render(Primer::Box.new(classes: "ToggleSwitch-lineIcon")) do %> | ||
<%= render(Primer::BaseComponent.new( | ||
tag: :svg, | ||
width: @size == :small ? 12 : 16, | ||
height: @size == :small ? 12 : 16, | ||
viewBox: "0 0 16 16", | ||
fill: "currentColor", | ||
xmlns: "http://www.w3.org/2000/svg", | ||
aria: { hidden: true }, | ||
focusable: false | ||
)) do %> | ||
<path fill-rule="evenodd" d="M8 2a.75.75 0 0 1 .75.75v11.5a.75.75 0 0 1-1.5 0V2.75A.75.75 0 0 1 8 2Z" /> | ||
<% end %> | ||
<% end %> | ||
<%= render(Primer::Box.new(classes: "ToggleSwitch-circleIcon")) do %> | ||
<%= render(Primer::BaseComponent.new( | ||
tag: :svg, | ||
width: @size == :small ? 12 : 16, | ||
height: @size == :small ? 12 : 16, | ||
viewBox: "0 0 16 16", | ||
fill: "currentColor", | ||
xmlns: "http://www.w3.org/2000/svg", | ||
aria: { hidden: true }, | ||
focusable: false | ||
)) do %> | ||
<path fill-rule="evenodd" d="M8 12.5a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z" /> | ||
<% end %> | ||
<% end %> | ||
<% end %> | ||
<%= render(Primer::Box.new(classes: "ToggleSwitch-knob")) %> | ||
<% end %> | ||
<% end %> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# frozen_string_literal: true | ||
|
||
module Primer | ||
module Alpha | ||
# The ToggleSwitch component is a button that toggles between two boolean states. It is meant to be used for | ||
# settings that should cause an immediate update. If configured with a "src" attribute, the component will | ||
# make a POST request containing data of the form "value: 0 | 1". | ||
class ToggleSwitch < Primer::Component | ||
SIZE_DEFAULT = :medium | ||
SIZE_MAPPINGS = { | ||
SIZE_DEFAULT => nil, | ||
:small => "ToggleSwitch--small" | ||
}.freeze | ||
SIZE_OPTIONS = SIZE_MAPPINGS.keys.freeze | ||
|
||
STATUS_LABEL_POSITION_DEFAULT = :start | ||
STATUS_LABEL_POSITION_MAPPINGS = { | ||
STATUS_LABEL_POSITION_DEFAULT => nil, | ||
:end => "ToggleSwitch--statusAtEnd" | ||
}.freeze | ||
STATUS_LABEL_POSITION_OPTIONS = STATUS_LABEL_POSITION_MAPPINGS.keys.freeze | ||
|
||
# @example Default | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo")) %> | ||
# | ||
# @example Checked | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo", checked: true)) %> | ||
# | ||
# @example Disabled | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo", enabled: false)) %> | ||
# | ||
# @example Checked and Disabled | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo", checked: true, enabled: false)) %> | ||
# | ||
# @example Small | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo", size: :small)) %> | ||
# | ||
# @example With status label positioned at the end | ||
# <%= render(Primer::Alpha::ToggleSwitch.new(src: "/foo", status_label_position: :end)) %> | ||
# | ||
# @param src [String] The URL to POST to when the toggle switch is toggled. If `nil`, the toggle switch will not make any requests. | ||
# @param csrf_token [String] A CSRF token that will be sent to the server as "authenticity_token" when the toggle switch is toggled. Unused if `src` is `nil`. | ||
# @param checked [Boolean] Whether the toggle switch is on or off. | ||
# @param enabled [Boolean] Whether or not the toggle switch responds to user input. | ||
# @param size [Symbol] What size toggle switch to render. <%= one_of(Primer::Alpha::ToggleSwitch::STATUS_LABEL_POSITION_OPTIONS) %> | ||
# @param status_label_position [Symbol] Which side of the toggle switch to render the status label. <%= one_of(Primer::Alpha::ToggleSwitch::SIZE_OPTIONS) %> | ||
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %> | ||
def initialize(src: nil, csrf_token: nil, checked: false, enabled: true, size: SIZE_DEFAULT, status_label_position: STATUS_LABEL_POSITION_DEFAULT, **system_arguments) | ||
@src = src | ||
@csrf_token = csrf_token | ||
@checked = checked | ||
@enabled = enabled | ||
@system_arguments = system_arguments | ||
|
||
@size = fetch_or_fallback(SIZE_OPTIONS, size, SIZE_DEFAULT) | ||
@status_label_position = fetch_or_fallback( | ||
STATUS_LABEL_POSITION_OPTIONS, status_label_position, STATUS_LABEL_POSITION_DEFAULT | ||
) | ||
|
||
@system_arguments[:classes] = class_names( | ||
@system_arguments.delete(:classes), | ||
"ToggleSwitch", | ||
on? ? "ToggleSwitch--checked" : nil, | ||
enabled? ? nil : "ToggleSwitch--disabled", | ||
STATUS_LABEL_POSITION_MAPPINGS[@status_label_position], | ||
SIZE_MAPPINGS[@size] | ||
) | ||
|
||
@system_arguments[:src] = @src if @src | ||
|
||
return unless @src && @csrf_token | ||
|
||
@system_arguments[:csrf] = @csrf_token | ||
end | ||
|
||
def on? | ||
@checked | ||
end | ||
|
||
def enabled? | ||
@enabled | ||
end | ||
|
||
def disabled? | ||
!enabled? | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# frozen_string_literal: true | ||
|
||
# For toggle switch previews/tests | ||
class ToggleSwitchController < ApplicationController | ||
skip_before_action :verify_authenticity_token | ||
|
||
def create | ||
sleep 1 unless Rails.env.test? | ||
|
||
if form_params[:authenticity_token] && form_params[:authenticity_token] != "let_me_in" | ||
head :unauthorized | ||
return | ||
end | ||
|
||
head :accepted | ||
end | ||
|
||
private | ||
|
||
def form_params | ||
params.permit(:value, :authenticity_token) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# frozen_string_literal: true | ||
|
||
# For toggle switch previews/tests | ||
class ToggleSwitchController < ApplicationController | ||
skip_before_action :verify_authenticity_token | ||
|
||
def create | ||
sleep 1 unless Rails.env.test? | ||
|
||
if form_params[:authenticity_token] && form_params[:authenticity_token] != "let_me_in" | ||
head :unauthorized | ||
return | ||
end | ||
|
||
head :accepted | ||
end | ||
|
||
private | ||
|
||
def form_params | ||
params.permit(:value, :authenticity_token) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.