UI: Create enable input component (#24427)

* enable input component

* add more stars

* update css comments

* Update ui/app/styles/helper-classes/flexbox-and-grid.scss

* make attrOptions optional

* add subtext to textfile

* add docLink arg to form field textfile

* update form field test

* add test

* add comment

* update jsdoc

* remove unused class

* Update ui/tests/integration/components/enable-input-test.js

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>

---------

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
This commit is contained in:
claire bontempo
2023-12-07 15:25:55 -08:00
committed by GitHub
parent 571b3cca47
commit 416d8bde5d
8 changed files with 211 additions and 29 deletions

View File

@@ -6,6 +6,32 @@
/* Helpers that define anything with the CSS flexbox or CSS grid. */
/* Flexbox helpers */
// FLEX CONTAINER (child helpers at end of file)
// new flex classes, these do not use !important
.flex {
display: flex;
// direction
&.row-wrap {
flex-flow: row wrap;
}
// alignment
&.space-between {
justify-content: space-between;
}
&.row-gap-8 {
row-gap: $spacing-8;
}
&.column-gap-16 {
column-gap: $spacing-16;
}
}
// avoid !important flex classes below
.is-flex {
display: flex !important;
}
@@ -104,28 +130,6 @@
flex: 50%;
}
// moving away from !important, fresh flex styles below
.flex {
display: flex;
// direction
&.row-wrap {
flex-flow: row wrap;
}
// alignment
&.space-between {
justify-content: space-between;
}
&.row-gap-8 {
row-gap: $spacing-8;
}
&.column-gap-16 {
column-gap: $spacing-16;
}
}
/* Flex Responsive */
@media screen and (min-width: 769px), print {
.is-flex-v-centered-tablet {
@@ -163,10 +167,6 @@
grid-template-columns: repeat(3, 1fr);
}
.align-self-center {
align-self: center;
}
.is-medium-height {
height: 125px;
}
@@ -178,3 +178,13 @@
.grid-align-items-start {
align-items: start;
}
// CHILD ELEMENT HELPERS
.align-self-center {
align-self: center;
}
.align-self-end {
align-self: end;
}

View File

@@ -0,0 +1,27 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if this.enable}}
{{yield}}
{{else}}
<div class="flex" ...attributes>
<div class="is-flex-grow-1">
{{#if @attr}}
<ReadonlyFormField @attr={{@attr}} @value="**********" />
{{else}}
<Input disabled class="input" @type="text" @value="**********" />
{{/if}}
</div>
<div class="align-self-end">
<Hds::Button
@text="Enable input"
@icon="edit"
@isIconOnly={{true}}
@color="tertiary"
{{on "click" (fn (mut this.enable))}}
/>
</div>
</div>
{{/if}}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
interface Args {
attr?: AttrData;
}
interface AttrData {
name: string; // required if @attr is passed
options?: {
label?: string;
helpText?: string;
subText?: string;
possibleValues?: string[];
};
}
/**
* @module EnableInput
* EnableInput components render a disabled input with a hardcoded masked value beside an "Edit" button to "enable" the input.
* Clicking "Edit" hides the disabled input and renders the yielded component. This way any data management is handled by the parent.
* These are useful for editing inputs of sensitive values not returned by the API. The extra click ensures the user is intentionally editing the field.
*
* @example
<EnableInput class="field" @attr={{attr}}>
<FormField @attr={{attr}} @model={{@destination}} @modelValidations={{this.modelValidations}} />
</EnableInput>
// without passing @attr
<EnableInput>
<Input @type="text" />
</EnableInput>
* @param {object} [attr] - used to generate label for `ReadonlyFormField`, `name` key is required. Can be an attribute from a model exported with expandAttributeMeta.
*/
export default class EnableInputComponent extends Component<Args> {
@tracked enable = false;
}

View File

@@ -104,7 +104,9 @@
<div class="has-bottom-margin-m">
<TextFile
@label={{this.labelString}}
@subText={{@attr.options.subText}}
@helpText={{@attr.options.helpText}}
@docLink={{@attr.options.docLink}}
@onChange={{this.setFile}}
@validationError={{this.validationError}}
/>

View File

@@ -42,7 +42,19 @@
data-test-text-file-textarea
as |F|
>
<F.HelperText>Enter the value as text </F.HelperText>
<F.HelperText>
Enter the value as text.
{{#if @subText}}
{{@subText}}
{{/if}}
{{#if @docLink}}
See our
<Hds::Link::Inline @href={{doc-link @docLink}} @icon="docs-link" @iconPosition="trailing">
documentation
</Hds::Link::Inline>
for help.
{{/if}}
</F.HelperText>
</Hds::Form::MaskedInput::Field>
{{else}}
<Hds::Form::FileInput::Field
@@ -51,7 +63,19 @@
data-test-text-file-input
as |F|
>
<F.HelperText>Select a file from your computer</F.HelperText>
<F.HelperText>
Select a file from your computer.
{{#if @subText}}
{{@subText}}
{{/if}}
{{#if @docLink}}
See our
<Hds::Link::Inline @href={{doc-link @docLink}} @icon="docs-link" @iconPosition="trailing">
documentation
</Hds::Link::Inline>
for help.
{{/if}}
</F.HelperText>
</Hds::Form::FileInput::Field>
{{#if (or @validationError this.uploadError)}}
<AlertInline

View File

@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/enable-input';

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | EnableInput', function (hooks) {
setupRenderingTest(hooks);
test('it renders and enables yielded input', async function (assert) {
assert.expect(4);
await render(hbs`
<EnableInput>
<Input data-test-yielded-input @type='text' />
</EnableInput>
`);
assert.dom('input').isDisabled('input is disabled');
assert.dom('input').hasValue('**********', 'disabled input renders asterisks');
await click('button');
assert.dom('[data-test-yielded-input]').isNotDisabled('toggles to enabled, yielded input');
assert.dom('button').doesNotExist('button disappears when input is enabled');
});
test('it renders passed attribute', async function (assert) {
assert.expect(6);
this.attr = {
name: 'specialClientCredentials',
type: 'string',
options: {
subText: 'This value is protected and not returned from the API. Enable input to update value.',
},
};
this.model = { specialClientCredentials: '' };
await render(hbs`
<EnableInput @attr={{this.attr}} >
<FormField @attr={{this.attr}} @model={{this.model}} />
</EnableInput>
`);
assert.dom(`[data-test-input="${this.attr.name}"]`).isDisabled('renders disabled ReadonlyFormField');
assert
.dom(`[data-test-input="${this.attr.name}"]`)
.hasValue('**********', 'disabled input renders asterisks');
assert.dom('[data-test-readonly-label]').hasText('Special client credentials');
assert.dom('p.sub-text').hasText(this.attr.options.subText);
await click('button');
assert
.dom(`[data-test-field="${this.attr.name}"] input`)
.isNotDisabled('toggles to enabled, yielded form field component');
assert.dom('button').doesNotExist('button disappears when input is enabled');
});
});

View File

@@ -106,9 +106,22 @@ module('Integration | Component | form field', function (hooks) {
});
test('it renders: editType file', async function (assert) {
await setup.call(this, createAttr('foo', 'string', { editType: 'file' }));
const subText = 'My subtext.';
await setup.call(this, createAttr('foo', 'string', { editType: 'file', subText, docLink: '/docs' }));
assert.ok(component.hasTextFile, 'renders the text-file component');
assert
.dom('.hds-form-helper-text')
.hasText(
`Select a file from your computer. ${subText} See our documentation for help.`,
'renders subtext'
);
assert.dom('.hds-form-helper-text a').exists('renders doc link');
await click('[data-test-text-toggle]');
// assert again after toggling because subtext is rendered differently for each input
assert
.dom('.hds-form-helper-text')
.hasText(`Enter the value as text. ${subText} See our documentation for help.`, 'renders subtext');
assert.dom('.hds-form-helper-text a').exists('renders doc link');
await fillIn('[data-test-text-file-textarea]', 'hello world');
});