Ui/toggle component (#8610)

* Toggle UI component, storybook, and tests

* Update secret-edit template with new Toggle
This commit is contained in:
Chelsea Shaw 2020-03-24 13:47:56 -05:00 committed by GitHub
parent 65ad0d87f9
commit 6a0e20a719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 288 additions and 14 deletions

View File

@ -0,0 +1,40 @@
/**
* @module Toggle
* Toggle components are used to indicate boolean values which can be toggled on or off.
* They are a stylistic alternative to checkboxes, but still use the input[type="checkbox"] under the hood.
*
* @example
* ```js
* <Toggle @requiredParam={requiredParam} @optionalParam={optionalParam} @param1={{param1}}/>
* ```
* @param {function} onChange - onChange is triggered on checkbox change (select, deselect). Must manually mutate checked value
* @param {string} name - name is passed along to the form field, as well as to generate the ID of the input & "for" value of the label
* @param {boolean} [checked=false] - checked status of the input, and must be passed in and mutated from the parent
* @param {boolean} [disabled=false] - disabled makes the switch unclickable
* @param {string} [size='medium'] - Sizing can be small or medium
* @param {string} [status='normal'] - Status can be normal or success, which makes the switch have a blue background when checked=true
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: '',
checked: false,
disabled: false,
size: 'normal',
status: 'normal',
safeId: computed('name', function() {
return `toggle-${this.name}`;
}),
inputClasses: computed('size', 'status', function() {
const sizeClass = `is-${this.size}`;
const statusClass = `is-${this.status}`;
return `toggle ${statusClass} ${sizeClass}`;
}),
actions: {
handleChange(value) {
this.onChange(value);
},
},
});

View File

@ -35,6 +35,7 @@
@import './core/tables';
@import './core/tags';
@import './core/title';
@import './core/toggle';
// bulma additions
@import './core/layout';

View File

@ -0,0 +1,104 @@
/* COPIED FROM BULMA/SWITCH */
.toggle[type='checkbox'] {
outline: 0;
user-select: none;
position: absolute;
margin-bottom: 0;
opacity: 0;
left: 0;
}
.toggle[type='checkbox'][disabled] {
cursor: not-allowed;
}
.toggle[type='checkbox'][disabled] + label {
opacity: 0.5;
}
.toggle[type='checkbox'][disabled] + label::before {
opacity: 0.5;
}
.toggle[type='checkbox'][disabled] + label::after {
opacity: 0.5;
}
.toggle[type='checkbox'][disabled] + label:hover {
cursor: not-allowed;
}
.toggle[type='checkbox'] + label {
position: relative;
display: inline-block;
font-size: 1rem;
padding-left: 3.5rem;
cursor: pointer;
}
.toggle[type='checkbox'] + label::before {
position: absolute;
display: block;
top: 0;
left: 0;
width: 3rem;
height: 1.5rem;
border: 0.1rem solid transparent;
border-radius: 0.75rem;
background: $ui-gray-300;
content: '';
}
.toggle[type='checkbox'] + label::after {
display: block;
position: absolute;
top: 0.25rem;
left: 0.25rem;
width: 1rem;
height: 1rem;
transform: translate3d(0, 0, 0);
border-radius: 50%;
background: white;
transition: all 0.25s ease-out;
content: '';
}
.toggle[type='checkbox']:checked + label::before {
background: $ui-gray-700;
}
.toggle[type='checkbox']:checked + label::after {
left: 1.625rem;
}
/* CUSTOM */
.toggle[type='checkbox'] {
&.is-small {
+ label {
font-size: $size-8;
font-weight: bold;
padding-left: $size-8 * 2.5;
margin: 0 0.25rem;
&::before {
top: $size-8 / 5;
height: $size-8;
width: $size-8 * 2;
}
&::after {
width: $size-8 * 0.8;
height: $size-8 * 0.8;
transform: translateX(0.15rem);
left: 0;
top: $size-8/ 4;
}
}
&:checked + label::after {
left: 0;
transform: translateX(($size-8 * 2) - ($size-8 * 0.94));
}
}
&.is-large {
width: 4.5rem;
height: 2.25rem;
}
}
.toggle[type='checkbox'].is-small + label::after {
will-change: left;
}
.toggle[type='checkbox']:focus + label {
box-shadow: 0 0 1px $blue;
}
.toggle[type='checkbox'].is-success:checked + label::before {
background: $blue;
}

View File

@ -26,17 +26,16 @@
<Toolbar>
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
<ToolbarFilters>
<input
data-test-secret-json-toggle=true
id="json"
type="checkbox"
name="json"
class="switch is-rounded is-success is-small"
checked={{showAdvancedMode}}
onchange={{action "toggleAdvanced" value="target.checked"}}
disabled={{and (eq mode 'show') secretDataIsAdvanced}}
/>
<label for="json" class="has-text-grey">JSON</label>
<Toggle
@name="json"
@status="success"
@size="small"
@disabled={{and (eq mode 'show') secretDataIsAdvanced}}
@checked={{showAdvancedMode}}
@onChange={{action "toggleAdvanced"}}
>
<span class="has-text-grey">JSON</span>
</Toggle>
</ToolbarFilters>
{{/unless}}
<ToolbarActions>

View File

@ -0,0 +1,11 @@
{{input
id=safeId
name=name
type="checkbox"
checked=checked
change=(action "handleChange" value='target.checked')
class=inputClasses
disabled=disabled
data-test-toggle-input=name
}}
<label data-test-toggle-label={{name}} for={{safeId}}>{{yield}}</label>

29
ui/stories/toggle.md Normal file
View File

@ -0,0 +1,29 @@
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/toggle.js. To make changes, first edit that file and run "yarn gen-story-md toggle" to re-generate the content.-->
## Toggle
Toggle components are used to indicate boolean values which can be toggled on or off.
They are a stylistic alternative to checkboxes, but still use the input[type=checkbox] under the hood.
**Params**
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| onChange | <code>function</code> | | onChange is triggered on checkbox change (select, deselect). Must manually mutate checked value |
| name | <code>string</code> | | name is passed along to the form field, as well as to generate the ID of the input & "for" value of the label |
| [checked] | <code>boolean</code> | <code>false</code> | checked status of the input, and must be passed in and mutated from the parent |
| [disabled] | <code>boolean</code> | <code>false</code> | disabled makes the switch unclickable |
| [size] | <code>string</code> | <code>&quot;&#x27;medium&#x27;&quot;</code> | Sizing can be small or medium |
| [status] | <code>string</code> | <code>&quot;&#x27;normal&#x27;&quot;</code> | Status can be normal or success, which makes the switch have a blue background when checked=true |
**Example**
```js
<Toggle @requiredParam={requiredParam} @optionalParam={optionalParam} @param1={{param1}}/>
```
**See**
- [Uses of Toggle](https://github.com/hashicorp/vault/search?l=Handlebars&q=Toggle+OR+toggle)
- [Toggle Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/toggle.js)
---

View File

@ -0,0 +1,40 @@
import hbs from 'htmlbars-inline-precompile';
import { storiesOf } from '@storybook/ember';
import notes from './toggle.md';
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
storiesOf('Toggle/', module)
.addParameters({ options: { showPanel: true } })
.addDecorator(withKnobs())
.add(
`Toggle`,
() => ({
template: hbs`
<h5 class="title is-5">Toggle</h5>
<Toggle
@name={{name}}
@checked={{checked}}
@onChange={{onChange}}
@disabled={{disabled}}
@size={{size}}
@status={{status}}
@round={{round}}
data-test-secret-json-toggle
>
{{yielded}}
</Toggle>
`,
context: {
name: text('name', 'my-checkbox'),
checked: boolean('checked', true),
yielded: text('yield', 'Label content here ✔️'),
onChange() {
this.set('checked', !this.checked);
},
disabled: boolean('disabled', false),
size: select('size', ['small', 'medium'], 'small'),
status: select('status', ['normal', 'success'], 'success'),
},
}),
{ notes }
);

View File

@ -35,7 +35,7 @@ module('Integration | Component | secret edit', function(hooks) {
});
await render(hbs`{{secret-edit mode=mode model=model }}`);
assert.dom('[data-test-secret-json-toggle]').isDisabled();
assert.dom('[data-test-toggle-input="json"]').isDisabled();
});
test('it does JSON toggle in show mode when showing string data', async function(assert) {
@ -49,7 +49,7 @@ module('Integration | Component | secret edit', function(hooks) {
});
await render(hbs`{{secret-edit mode=mode model=model }}`);
assert.dom('[data-test-secret-json-toggle]').isNotDisabled();
assert.dom('[data-test-toggle-input="json"]').isNotDisabled();
});
test('it shows an error when creating and data is not an object', async function(assert) {

View File

@ -0,0 +1,50 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, findAll } from '@ember/test-helpers';
import sinon from 'sinon';
import hbs from 'htmlbars-inline-precompile';
let handler = (data, e) => {
if (e && e.preventDefault) e.preventDefault();
return;
};
module('Integration | Component | toggle', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
this.set('handler', sinon.spy(handler));
await render(hbs`<Toggle
@onChange={{handler}}
@name="thing"
/>`);
assert.equal(findAll('label')[0].textContent.trim(), '');
await render(hbs`
<Toggle
@onChange={{handler}}
@name="thing"
>
<span id="test-value" class="has-text-grey">template block text</span>
</Toggle>
`);
assert.dom('[data-test-toggle-label="thing"]').exists('toggle label exists');
assert.equal(find('#test-value').textContent.trim(), 'template block text', 'yielded text renders');
});
test('it has the correct classes', async function(assert) {
this.set('handler', sinon.spy(handler));
await render(hbs`
<Toggle
@onChange={{handler}}
@name="thing"
@size="small"
>
template block text
</Toggle>
`);
assert.dom('.toggle.is-small').exists('toggle has is-small class');
});
});

View File

@ -11,7 +11,7 @@ export default create({
confirmBtn: clickable('[data-test-confirm-button]'),
visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
toggleJSON: clickable('[data-test-secret-json-toggle]'),
toggleJSON: clickable('[data-test-toggle-input="json"]'),
hasMetadataFields: isPresent('[data-test-metadata-fields]'),
showsNoCASWarning: isPresent('[data-test-v2-no-cas-warning]'),
showsV2WriteWarning: isPresent('[data-test-v2-write-without-read]'),