diff --git a/ui/app/components/toggle.js b/ui/app/components/toggle.js new file mode 100644 index 0000000000..2732798dd2 --- /dev/null +++ b/ui/app/components/toggle.js @@ -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 + * + * ``` + * @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); + }, + }, +}); diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index e265d4cd7a..03ffa0f88c 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -35,6 +35,7 @@ @import './core/tables'; @import './core/tags'; @import './core/title'; +@import './core/toggle'; // bulma additions @import './core/layout'; diff --git a/ui/app/styles/core/toggle.scss b/ui/app/styles/core/toggle.scss new file mode 100644 index 0000000000..869da9e128 --- /dev/null +++ b/ui/app/styles/core/toggle.scss @@ -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; +} diff --git a/ui/app/templates/components/secret-edit.hbs b/ui/app/templates/components/secret-edit.hbs index 06bf6bdf3b..9719fb0334 100644 --- a/ui/app/templates/components/secret-edit.hbs +++ b/ui/app/templates/components/secret-edit.hbs @@ -26,17 +26,16 @@ {{#unless (and (eq mode 'show') isWriteWithoutRead)}} - - + + JSON + {{/unless}} diff --git a/ui/app/templates/components/toggle.hbs b/ui/app/templates/components/toggle.hbs new file mode 100644 index 0000000000..16cf4700be --- /dev/null +++ b/ui/app/templates/components/toggle.hbs @@ -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 +}} + \ No newline at end of file diff --git a/ui/stories/toggle.md b/ui/stories/toggle.md new file mode 100644 index 0000000000..148a11b3da --- /dev/null +++ b/ui/stories/toggle.md @@ -0,0 +1,29 @@ + + +## 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 | function | | onChange is triggered on checkbox change (select, deselect). Must manually mutate checked value | +| name | string | | name is passed along to the form field, as well as to generate the ID of the input & "for" value of the label | +| [checked] | boolean | false | checked status of the input, and must be passed in and mutated from the parent | +| [disabled] | boolean | false | disabled makes the switch unclickable | +| [size] | string | "'medium'" | Sizing can be small or medium | +| [status] | string | "'normal'" | Status can be normal or success, which makes the switch have a blue background when checked=true | + +**Example** + +```js + +``` + +**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) + +--- diff --git a/ui/stories/toggle.stories.js b/ui/stories/toggle.stories.js new file mode 100644 index 0000000000..c08e1613c7 --- /dev/null +++ b/ui/stories/toggle.stories.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` +
Toggle
+ + {{yielded}} + + `, + 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 } + ); diff --git a/ui/tests/integration/components/secret-edit-test.js b/ui/tests/integration/components/secret-edit-test.js index 6ffa24a851..3f92b6b9b3 100644 --- a/ui/tests/integration/components/secret-edit-test.js +++ b/ui/tests/integration/components/secret-edit-test.js @@ -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) { diff --git a/ui/tests/integration/components/toggle-test.js b/ui/tests/integration/components/toggle-test.js new file mode 100644 index 0000000000..06b27cc664 --- /dev/null +++ b/ui/tests/integration/components/toggle-test.js @@ -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``); + + assert.equal(findAll('label')[0].textContent.trim(), ''); + + await render(hbs` + + template block text + + `); + 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` + + template block text + + `); + assert.dom('.toggle.is-small').exists('toggle has is-small class'); + }); +}); diff --git a/ui/tests/pages/secrets/backend/kv/edit-secret.js b/ui/tests/pages/secrets/backend/kv/edit-secret.js index cdb7007e6f..3a7a719117 100644 --- a/ui/tests/pages/secrets/backend/kv/edit-secret.js +++ b/ui/tests/pages/secrets/backend/kv/edit-secret.js @@ -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]'),