mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 20:17:59 +00:00
Ui/toggle component (#8610)
* Toggle UI component, storybook, and tests * Update secret-edit template with new Toggle
This commit is contained in:
40
ui/app/components/toggle.js
Normal file
40
ui/app/components/toggle.js
Normal 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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
@import './core/tables';
|
@import './core/tables';
|
||||||
@import './core/tags';
|
@import './core/tags';
|
||||||
@import './core/title';
|
@import './core/title';
|
||||||
|
@import './core/toggle';
|
||||||
|
|
||||||
// bulma additions
|
// bulma additions
|
||||||
@import './core/layout';
|
@import './core/layout';
|
||||||
|
|||||||
104
ui/app/styles/core/toggle.scss
Normal file
104
ui/app/styles/core/toggle.scss
Normal 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;
|
||||||
|
}
|
||||||
@@ -26,17 +26,16 @@
|
|||||||
<Toolbar>
|
<Toolbar>
|
||||||
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
|
{{#unless (and (eq mode 'show') isWriteWithoutRead)}}
|
||||||
<ToolbarFilters>
|
<ToolbarFilters>
|
||||||
<input
|
<Toggle
|
||||||
data-test-secret-json-toggle=true
|
@name="json"
|
||||||
id="json"
|
@status="success"
|
||||||
type="checkbox"
|
@size="small"
|
||||||
name="json"
|
@disabled={{and (eq mode 'show') secretDataIsAdvanced}}
|
||||||
class="switch is-rounded is-success is-small"
|
@checked={{showAdvancedMode}}
|
||||||
checked={{showAdvancedMode}}
|
@onChange={{action "toggleAdvanced"}}
|
||||||
onchange={{action "toggleAdvanced" value="target.checked"}}
|
>
|
||||||
disabled={{and (eq mode 'show') secretDataIsAdvanced}}
|
<span class="has-text-grey">JSON</span>
|
||||||
/>
|
</Toggle>
|
||||||
<label for="json" class="has-text-grey">JSON</label>
|
|
||||||
</ToolbarFilters>
|
</ToolbarFilters>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
<ToolbarActions>
|
<ToolbarActions>
|
||||||
|
|||||||
11
ui/app/templates/components/toggle.hbs
Normal file
11
ui/app/templates/components/toggle.hbs
Normal 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
29
ui/stories/toggle.md
Normal 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>"'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)
|
||||||
|
|
||||||
|
---
|
||||||
40
ui/stories/toggle.stories.js
Normal file
40
ui/stories/toggle.stories.js
Normal 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 }
|
||||||
|
);
|
||||||
@@ -35,7 +35,7 @@ module('Integration | Component | secret edit', function(hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await render(hbs`{{secret-edit mode=mode model=model }}`);
|
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) {
|
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 }}`);
|
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) {
|
test('it shows an error when creating and data is not an object', async function(assert) {
|
||||||
|
|||||||
50
ui/tests/integration/components/toggle-test.js
Normal file
50
ui/tests/integration/components/toggle-test.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ export default create({
|
|||||||
confirmBtn: clickable('[data-test-confirm-button]'),
|
confirmBtn: clickable('[data-test-confirm-button]'),
|
||||||
visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
|
visitEdit: visitable('/vault/secrets/:backend/edit/:id'),
|
||||||
visitEditRoot: visitable('/vault/secrets/:backend/edit'),
|
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]'),
|
hasMetadataFields: isPresent('[data-test-metadata-fields]'),
|
||||||
showsNoCASWarning: isPresent('[data-test-v2-no-cas-warning]'),
|
showsNoCASWarning: isPresent('[data-test-v2-no-cas-warning]'),
|
||||||
showsV2WriteWarning: isPresent('[data-test-v2-write-without-read]'),
|
showsV2WriteWarning: isPresent('[data-test-v2-write-without-read]'),
|
||||||
|
|||||||
Reference in New Issue
Block a user