Ui/toggle component (#8610)
* Toggle UI component, storybook, and tests * Update secret-edit template with new Toggle
This commit is contained in:
parent
65ad0d87f9
commit
6a0e20a719
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -35,6 +35,7 @@
|
|||
@import './core/tables';
|
||||
@import './core/tags';
|
||||
@import './core/title';
|
||||
@import './core/toggle';
|
||||
|
||||
// bulma additions
|
||||
@import './core/layout';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>"'medium'"</code> | Sizing can be small or medium |
|
||||
| [status] | <code>string</code> | <code>"'normal'"</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)
|
||||
|
||||
---
|
|
@ -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 }
|
||||
);
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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]'),
|
||||
|
|
Loading…
Reference in New Issue