Skip to content

Commit fc81503

Browse files
refactor: Form associated components validity behavior (#1786)
Changed how validation is performed in the base form associated mixin class. Before, validation was based on an internal `_dirty` flag and the `hasUpdated` state of the form associated element. However, this was awkward when the element is rendered/updated multiple times (ahem, React) and did not align with the behavior of native browser form elements, more precisely the `:user-invalid` state. Now, validation will try to stay as closely to the native `:user-invalid` behavior as possible, with some little changes to accommodate for our own UX patterns and designs. The important parts of the changes are the following: * the `invalid` property **no longer reflects** back as an attribute on the element and should not be used in styling rules. * a new custom state, `ig-invalid` is exposed for styling rules and custom logic. * Moved all the ElementInternals handling into the new controller. * Fixed several bugs around styles and invalid states. --------- Co-authored-by: sivanova <[email protected]> Co-authored-by: Silvia Ivanova <[email protected]>
1 parent a05528d commit fc81503

File tree

81 files changed

+1014
-851
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1014
-851
lines changed

src/components/checkbox/checkbox-base.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ export class IgcCheckboxBaseComponent extends FormAssociatedCheckboxRequiredMixi
8181
@property({ type: Boolean })
8282
public set checked(value: boolean) {
8383
this._formValue.setValueAndFormState(value);
84-
this._validate();
8584
}
8685

8786
public get checked(): boolean {
@@ -119,13 +118,10 @@ export class IgcCheckboxBaseComponent extends FormAssociatedCheckboxRequiredMixi
119118
protected _handleClick(event: PointerEvent): void {
120119
event.stopPropagation();
121120

121+
this._setTouchedState();
122122
this.checked = !this.checked;
123123
this.emitEvent('igcChange', {
124124
detail: { checked: this.checked, value: this.value },
125125
});
126126
}
127-
128-
protected _handleFocus(): void {
129-
this._dirty = true;
130-
}
131127
}

src/components/checkbox/checkbox.spec.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { defineComponents } from '../common/definitions/defineComponents.js';
55
import {
66
createFormAssociatedTestBed,
77
isFocused,
8+
} from '../common/utils.spec.js';
9+
import {
810
runValidationContainerTests,
911
type ValidationContainerTestsParams,
10-
} from '../common/utils.spec.js';
12+
} from '../common/validity-helpers.spec.js';
1113
import IgcCheckboxComponent from './checkbox.js';
1214

1315
describe('Checkbox', () => {
@@ -123,18 +125,6 @@ describe('Checkbox', () => {
123125
expect(input).dom.to.equal(`<input value="${value}"/>`, DIFF_OPTIONS);
124126
});
125127

126-
it('should set the checkbox invalid property correctly', () => {
127-
element.invalid = true;
128-
expect(element).dom.to.equal(
129-
`<igc-checkbox invalid label-position="after">${label}</igc-checkbox>`
130-
);
131-
132-
element.invalid = false;
133-
expect(element).dom.to.equal(
134-
`<igc-checkbox label-position="after">${label}</igc-checkbox>`
135-
);
136-
});
137-
138128
it('should correctly report validity status', () => {
139129
element.required = true;
140130
expect(element.reportValidity()).to.be.false;

src/components/checkbox/checkbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export default class IgcCheckboxComponent extends IgcCheckboxBaseComponent {
108108
aria-labelledby=${labelledBy ? labelledBy : this._labelId}
109109
aria-describedby=${describedBy}
110110
@click=${this._handleClick}
111-
@focus=${this._handleFocus}
111+
@blur=${this._handleBlur}
112112
/>
113113
<span part=${partMap({ control: true, checked })}>
114114
<span part=${partMap({ indicator: true, checked })}>

src/components/checkbox/switch.spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,6 @@ describe('Switch', () => {
8787
expect(input).dom.to.equal(`<input value="${value}"/>`, DIFF_OPTIONS);
8888
});
8989

90-
it('should set the switch invalid property correctly', async () => {
91-
element.invalid = true;
92-
await elementUpdated(element);
93-
94-
expect(element).dom.to.equal(
95-
`<igc-switch invalid label-position="after">${label}</igc-switch>`
96-
);
97-
98-
element.invalid = false;
99-
await elementUpdated(element);
100-
101-
expect(element).dom.to.equal(
102-
`<igc-switch label-position="after">${label}</igc-switch>`
103-
);
104-
});
105-
10690
it('should correctly report validity status', () => {
10791
element.required = true;
10892
expect(element.reportValidity()).to.be.false;

src/components/checkbox/switch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default class IgcSwitchComponent extends IgcCheckboxBaseComponent {
5858
.checked=${live(checked)}
5959
aria-labelledby=${labelledBy ? labelledBy : this._labelId}
6060
@click=${this._handleClick}
61-
@focus=${this._handleFocus}
61+
@blur=${this._handleBlur}
6262
/>
6363
<span
6464
part=${partMap({

src/components/checkbox/themes/dark/checkbox/checkbox.bootstrap.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ $theme: $bootstrap;
88
@include css-vars-from-theme(diff(light.$base, $theme), 'ig-checkbox');
99
}
1010

11-
:host(:not([disabled])[invalid]),
12-
:host(:not(:disabled)[invalid]) {
11+
:host(:not([disabled]):state(ig-invalid)),
12+
:host(:not(:disabled):state(ig-invalid)) {
1313
[part~='indicator'] {
1414
stroke: black;
1515
}

src/components/checkbox/themes/dark/checkbox/checkbox.fluent.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ $theme: $fluent;
88
@include css-vars-from-theme(diff(light.$base, $theme), 'ig-checkbox');
99
}
1010

11-
:host([invalid]:hover) {
11+
:host(:state(ig-invalid):hover) {
1212
[part~='indicator'] {
1313
stroke: color('error', 500);
1414
}

src/components/checkbox/themes/shared/checkbox/checkbox.bootstrap.scss

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ $theme: $bootstrap;
100100
}
101101
}
102102

103-
:host(:not([disabled])[invalid]),
104-
:host(:not(:disabled)[invalid]) {
103+
:host(:not([disabled]):state(ig-invalid)),
104+
:host(:not(:disabled):state(ig-invalid)) {
105105
[part~='control'] {
106106
&::after {
107107
box-shadow: inset 0 0 0 rem(1px) var-get($theme, 'error-color');
@@ -123,10 +123,10 @@ $theme: $bootstrap;
123123
}
124124
}
125125

126-
:host(:hover:not([disabled])[invalid]),
127-
:host(:hover:not(:disabled)[invalid]),
128-
:host(:hover:not([disabled])[invalid][indeterminate]),
129-
:host(:hover:not(:disabled)[invalid][indeterminate]) {
126+
:host(:hover:not([disabled]):state(ig-invalid)),
127+
:host(:hover:not(:disabled):state(ig-invalid)),
128+
:host(:hover:not([disabled]):state(ig-invalid)[indeterminate]),
129+
:host(:hover:not(:disabled):state(ig-invalid)[indeterminate]) {
130130
[part~='control'],
131131
[part='control checked'] {
132132
&::after {
@@ -141,28 +141,28 @@ $theme: $bootstrap;
141141
}
142142
}
143143

144-
:host(:not([disabled])[invalid][indeterminate]),
145-
:host(:not(:disabled)[invalid][indeterminate]) {
144+
:host(:not([disabled]):state(ig-invalid)[indeterminate]),
145+
:host(:not(:disabled):state(ig-invalid)[indeterminate]) {
146146
[part~='control'] {
147147
&::after {
148148
background: var-get($theme, 'error-color');
149149
}
150150
}
151151
}
152152

153-
:host(:hover:not([disabled])[invalid][indeterminate]),
154-
:host(:hover:not(:disabled)[invalid][indeterminate]) {
153+
:host(:hover:not([disabled]):state(ig-invalid)[indeterminate]),
154+
:host(:hover:not(:disabled):state(ig-invalid)[indeterminate]) {
155155
[part~='control'] {
156156
&::after {
157157
background: var-get($theme, 'error-color-hover');
158158
}
159159
}
160160
}
161161

162-
:host(:focus-visible:not([disabled])[invalid]) [part~='control'],
163-
:host(:focus-visible:not(:disabled)[invalid]) [part~='control'],
164-
:host(:not([disabled])[invalid]) [part~='focused'] [part~='control'],
165-
:host(:not(:disabled)[invalid]) [part~='focused'] [part~='control'] {
162+
:host(:focus-visible:not([disabled]):state(ig-invalid)) [part~='control'],
163+
:host(:focus-visible:not(:disabled):state(ig-invalid)) [part~='control'],
164+
:host(:not([disabled]):state(ig-invalid)) [part~='focused'] [part~='control'],
165+
:host(:not(:disabled):state(ig-invalid)) [part~='focused'] [part~='control'] {
166166
box-shadow: 0 0 0 rem(4px) var-get($theme, 'focus-outline-color-error');
167167
}
168168

src/components/checkbox/themes/shared/checkbox/checkbox.common.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ $theme: $base;
1717
stroke: var-get($theme, 'tick-color');
1818
}
1919

20-
:host(:not([disabled])[invalid]),
21-
:host(:not(:disabled)[invalid]) {
20+
:host(:not([disabled]):state(ig-invalid)),
21+
:host(:not(:disabled):state(ig-invalid)) {
2222
[part~='label'] {
2323
color: var-get($theme, 'error-color');
2424
}

src/components/checkbox/themes/shared/checkbox/checkbox.fluent.scss

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ $disabled-color: var-get($theme, 'disabled-color');
107107
}
108108
}
109109

110-
:host([invalid]:not([disabled])) {
110+
:host(:state(ig-invalid):not([disabled])) {
111111
[part~='control'] {
112112
&::after {
113113
box-shadow: inset 0 0 0 rem(1px) $invalid-color;
@@ -121,7 +121,7 @@ $disabled-color: var-get($theme, 'disabled-color');
121121
}
122122
}
123123

124-
:host([invalid]:focus-visible) {
124+
:host(:state(ig-invalid):focus-visible) {
125125
[part~='control'] {
126126
&::after {
127127
box-shadow: inset 0 0 0 rem(1px) $invalid-hover-color;
@@ -135,7 +135,7 @@ $disabled-color: var-get($theme, 'disabled-color');
135135
}
136136
}
137137

138-
:host([invalid]:hover) {
138+
:host(:state(ig-invalid):hover) {
139139
[part~='indicator'] {
140140
stroke: $invalid-color;
141141
}
@@ -157,8 +157,8 @@ $disabled-color: var-get($theme, 'disabled-color');
157157
}
158158
}
159159

160-
:host(:not([disabled])[invalid][indeterminate]),
161-
:host(:not(:disabled)[invalid][indeterminate]) {
160+
:host(:not([disabled]):state(ig-invalid)[indeterminate]),
161+
:host(:not(:disabled):state(ig-invalid)[indeterminate]) {
162162
[part~='control'] {
163163
&::before {
164164
background: $invalid-color;
@@ -184,8 +184,8 @@ $disabled-color: var-get($theme, 'disabled-color');
184184
}
185185
}
186186

187-
:host(:hover:not([disabled])[invalid][indeterminate]),
188-
:host(:hover:not(:disabled)[invalid][indeterminate]) {
187+
:host(:hover:not([disabled]):state(ig-invalid)[indeterminate]),
188+
:host(:hover:not(:disabled):state(ig-invalid)[indeterminate]) {
189189
[part~='control'] {
190190
&::before {
191191
background: $invalid-hover-color;
@@ -198,13 +198,6 @@ $disabled-color: var-get($theme, 'disabled-color');
198198
}
199199
}
200200

201-
:host([invalid][checked]) [part='control checked'] {
202-
&::after {
203-
box-shadow: inset 0 0 0 rem(1px) $invalid-color;
204-
background: $invalid-color;
205-
}
206-
}
207-
208201
[part~='focused'] {
209202
position: relative;
210203

@@ -250,33 +243,3 @@ $disabled-color: var-get($theme, 'disabled-color');
250243
}
251244
}
252245
}
253-
254-
:host([disabled][invalid]),
255-
:host(:disabled[invalid]) {
256-
[part~='control'] {
257-
&::after {
258-
box-shadow: inset 0 0 0 rem(1px) $disabled-color;
259-
}
260-
}
261-
262-
[part='control checked'] {
263-
&::after {
264-
background: $disabled-color;
265-
}
266-
}
267-
}
268-
269-
:host([disabled][invalid][indeterminate]),
270-
:host(:disabled[invalid][indeterminate]) {
271-
[part~='control'] {
272-
&::before {
273-
background: $disabled-color;
274-
}
275-
276-
&::after {
277-
box-shadow: inset 0 0 0 rem(1px) $disabled-color;
278-
background: transparent;
279-
}
280-
}
281-
}
282-

0 commit comments

Comments
 (0)