Skip to content

Commit

Permalink
fix: remove automatic valid styling from form fields (#703)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Before this change `sd-input`, `sd-select` and
`sd-textarea` immediately showed "valid styles" (success color +
checkmark) immediately when an form field was valid. In most cases this
isn't relevant, e. g. in search fields, app interfaces etc.
With this change you now have to explicitly set the attribute
`style-on-valid` on the mentioned components to show "valid styles", as
soon as the component is valid.
  • Loading branch information
mariohamann authored Jan 26, 2024
1 parent d24359b commit eb08265
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 18 deletions.
64 changes: 63 additions & 1 deletion packages/components/src/components/input/input.stories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../../solid-components';
import { html } from 'lit-html';
import { storybookDefaults, storybookHelpers, storybookTemplate } from '../../../scripts/storybook/helper';
import { userEvent } from '@storybook/testing-library';
import { waitUntil } from '@open-wc/testing-helpers';
import { withActions } from '@storybook/addon-actions/decorator';
import type SdInput from './input';
Expand Down Expand Up @@ -231,6 +232,49 @@ export const Sizes = {
}
};

/**
* Per default the input will indicate an error state when the input is invalid. Use the `style-on-valid` attribute to indicate a valid state as well.
*/

export const StyleOnValid = {
parameters: {
controls: {
exclude: ['style-on-valid']
}
},
args: overrideArgs([
{ type: 'attribute', name: 'value', value: 'valu' },
{ type: 'attribute', name: 'label', value: 'Label' },
{ type: 'attribute', name: 'help-text', value: 'help-text' },
{ type: 'attribute', name: 'clearable', value: true },
{
type: 'slot',
name: 'right',
value: '<sd-icon slot="right" library="global-resources" name="system/picture"></sd-icon>'
}
]),
render: (args: any) => {
return generateTemplate({
axis: {
y: { type: 'attribute', name: 'style-on-valid' }
},
args
});
},

play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => {
const els = canvasElement.querySelectorAll('sd-input');

for (const el of els) {
await waitUntil(() => el?.shadowRoot?.querySelector('input'));
await userEvent.type(el.shadowRoot!.querySelector('input')!, 'e');
}

// tab to next element to loose focus
await userEvent.tab();
}
};

/**
* Demonstrates the allowed input types.
*/
Expand Down Expand Up @@ -374,6 +418,24 @@ export const Validation = {
render: (args: any) => {
return html`
<form action="" method="get" id="testForm" name="testForm" class="w-[370px]">
<div class="mb-2">
${generateTemplate({
constants: [
{ type: 'attribute', name: 'label', value: 'Indicate Valid State' },
{ type: 'attribute', name: 'name', value: 'required field' },
{ type: 'attribute', name: 'placeholder', value: '.*' },
{ type: 'attribute', name: 'help-text', value: 'indicate on valid' },
{ type: 'attribute', name: 'form', value: 'testForm' },
{ type: 'attribute', name: 'style-on-valid', value: true },
{
type: 'slot',
name: 'right',
value: '<sd-icon slot="right" library="global-resources" name="system/picture"></sd-icon>'
}
],
args
})}
</div>
<div class="mb-2">
${generateTemplate({
constants: [
Expand Down Expand Up @@ -728,7 +790,7 @@ export const setCustomValidity = {
return html`
<!-- block submit and show alert instead -->
<form id="validationForm" class="flex flex-col gap-2">
<sd-input id="custom-input" label="Input"></sd-input>
<sd-input id="custom-input" label="Input" style-on-valid></sd-input>
<div>
<sd-button type="submit">Submit</sd-button>
<sd-button id="error-button" variant="secondary">Set custom error</sd-button>
Expand Down
18 changes: 16 additions & 2 deletions packages/components/src/components/input/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ describe('<sd-input>', () => {
expect(input.shadowRoot!.querySelector('#invalid-message')!.hasAttribute('hidden')).to.be.true;
});

it('should show correct icon when calling reportValidity()', async () => {
const input = await fixture<HTMLFormElement>(html` <sd-input></sd-input> `);
it('should show correct icon when calling reportValidity() with style-on-valid attribute', async () => {
const input = await fixture<HTMLFormElement>(html` <sd-input style-on-valid></sd-input> `);

input.setCustomValidity('Invalid selection');
await input.updateComplete;
Expand All @@ -279,6 +279,20 @@ describe('<sd-input>', () => {
expect(input.shadowRoot!.querySelector('[part~="valid-icon"]')).to.exist;
});

it('should not show icon when calling reportValidity() without style-on-valid attribute', async () => {
const input = await fixture<HTMLFormElement>(html` <sd-input></sd-input> `);

input.setCustomValidity('');
await input.updateComplete;

input.reportValidity();
await input.updateComplete;
await input.updateComplete; // Currently there are two updates in the component

expect(input.shadowRoot!.querySelector('[part~="invalid-icon"]')).to.not.exist;
expect(input.shadowRoot!.querySelector('[part~="valid-icon"]')).to.not.exist;
});

it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
const el = await fixture<HTMLFormElement>(html`
<div>
Expand Down
9 changes: 6 additions & 3 deletions packages/components/src/components/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ export default class SdInput extends SolidElement implements SolidFormControl {
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
@property() enterkeyhint: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';

/** Shows success styles if the validity of the input is valid. */
@property({ type: Boolean, reflect: true, attribute: 'style-on-valid' }) styleOnValid = false;

/** Enables spell checking on the input. */
@property({
type: Boolean,
Expand Down Expand Up @@ -432,13 +435,13 @@ export default class SdInput extends SolidElement implements SolidFormControl {
? 'readonly'
: this.hasFocus && this.showInvalidStyle
? 'activeInvalid'
: this.hasFocus && this.showValidStyle
: this.hasFocus && this.styleOnValid && this.showValidStyle
? 'activeValid'
: this.hasFocus
? 'active'
: this.showInvalidStyle
? 'invalid'
: this.showValidStyle
: this.styleOnValid && this.showValidStyle
? 'valid'
: 'default';

Expand Down Expand Up @@ -621,7 +624,7 @@ export default class SdInput extends SolidElement implements SolidFormControl {
></sd-icon>
`
: ''}
${this.showValidStyle
${this.showValidStyle && this.styleOnValid
? html`
<sd-icon
class=${cx('text-success', iconMarginLeft, iconSize)}
Expand Down
50 changes: 49 additions & 1 deletion packages/components/src/components/select/select.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,54 @@ export const Parts = {
}
};

/**
* Per default the select will indicate an error state when the input is invalid. Use the `style-on-valid` attribute to indicate a valid state as well.
*/

export const StyleOnValid = {
parameters: {
controls: {
exclude: ['open-attr']
}
},
render: (args: { 'open-attr'?: string }) => {
delete args['open-attr'];

return html`<div class="h-[340px]">
${generateTemplate({
options: {
classes: 'w-full'
},
axis: {
x: {
type: 'attribute',
name: 'style-on-valid'
}
},
constants: [fiveOptionsConstant, { type: 'attribute', name: 'value', value: '' }],
args
})}
</div>`;
},

play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => {
await Promise.all([customElements.whenDefined('sd-select'), customElements.whenDefined('sd-option')]).then(
async () => {
const els = canvasElement.querySelectorAll('sd-select');

for (const el of els) {
await waitUntil(() => el?.shadowRoot?.querySelector('input'));
await userEvent.click(el.shadowRoot!.querySelector('input')!);
await userEvent.click(el.querySelector('sd-option')!);
}

// tab to next element to loose focus
await userEvent.tab();
}
);
}
};

/**
* `sd-select` is fully accessibile via keyboard.
*/
Expand Down Expand Up @@ -510,7 +558,7 @@ export const setCustomValidity = {
return html`
<!-- block submit and show alert instead -->
<form id="validationForm" class="flex flex-col gap-2">
<sd-select id="custom-input">
<sd-select id="custom-input" style-on-valid>
<sd-option value="option-1">Option 1</sd-option>
<sd-option value="option-2">Option 2</sd-option>
<sd-option value="option-3">Option 3</sd-option>
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/select/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,9 @@ describe('<sd-select>', () => {
expect(input.shadowRoot!.querySelector('#invalid-message')!.hasAttribute('hidden')).to.be.true;
});

it('should show correct icon when calling reportValidity()', async () => {
it('should show correct icon when calling reportValidity() with style-on-valid attribute', async () => {
const input = await fixture<SdSelect>(html`
<sd-select name="a" value="option-1">
<sd-select name="a" value="option-1" style-on-valid>
<sd-option value="option-1">Option 1</sd-option>
<sd-option value="option-2">Option 2</sd-option>
<sd-option value="option-3">Option 3</sd-option>
Expand Down
9 changes: 6 additions & 3 deletions packages/components/src/components/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export default class SdSelect extends SolidElement implements SolidFormControl {
/** The select's required attribute. */
@property({ type: Boolean, reflect: true }) required = false;

/** Shows success styles if the validity of the input is valid. */
@property({ type: Boolean, reflect: true, attribute: 'style-on-valid' }) styleOnValid = false;

/**
* Enable this option to prevent the listbox from being clipped when the component is placed inside a container with
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
Expand Down Expand Up @@ -785,13 +788,13 @@ export default class SdSelect extends SolidElement implements SolidFormControl {
? 'disabled'
: this.hasFocus && this.showInvalidStyle
? 'activeInvalid'
: this.hasFocus && this.showValidStyle
: this.hasFocus && this.styleOnValid && this.showValidStyle
? 'activeValid'
: this.hasFocus || this.open
? 'active'
: this.showInvalidStyle
? 'invalid'
: this.showValidStyle
: this.styleOnValid && this.showValidStyle
? 'valid'
: 'default';

Expand Down Expand Up @@ -973,7 +976,7 @@ export default class SdSelect extends SolidElement implements SolidFormControl {
></sd-icon>
`
: ''}
${this.showValidStyle
${this.styleOnValid && this.showValidStyle
? html`
<sd-icon
part="valid-icon"
Expand Down
Loading

0 comments on commit eb08265

Please sign in to comment.