7
7
import { LitElement , isServer } from 'lit' ;
8
8
9
9
import { ConstraintValidation } from './constraint-validation.js' ;
10
+ import { WithElementInternals , internals } from './element-internals.js' ;
10
11
import { MixinBase , MixinReturn } from './mixin.js' ;
11
12
12
13
/**
@@ -47,6 +48,8 @@ export const onReportValidity = Symbol('onReportValidity');
47
48
// Private symbol members, used to avoid name clashing.
48
49
const privateCleanupFormListeners = Symbol ( 'privateCleanupFormListeners' ) ;
49
50
const privateDoNotReportInvalid = Symbol ( 'privateDoNotReportInvalid' ) ;
51
+ const privateIsSelfReportingValidity = Symbol ( 'privateIsSelfReportingValidity' ) ;
52
+ const privateCallOnReportValidity = Symbol ( 'privateCallOnReportValidity' ) ;
50
53
51
54
/**
52
55
* Mixes in a callback for constraint validation when validity should be
@@ -81,7 +84,7 @@ const privateDoNotReportInvalid = Symbol('privateDoNotReportInvalid');
81
84
* @return The provided class with `OnReportValidity` mixed in.
82
85
*/
83
86
export function mixinOnReportValidity <
84
- T extends MixinBase < LitElement & ConstraintValidation > ,
87
+ T extends MixinBase < LitElement & ConstraintValidation & WithElementInternals > ,
85
88
> ( base : T ) : MixinReturn < T , OnReportValidity > {
86
89
abstract class OnReportValidityElement
87
90
extends base
@@ -98,6 +101,13 @@ export function mixinOnReportValidity<
98
101
*/
99
102
[ privateDoNotReportInvalid ] = false ;
100
103
104
+ /**
105
+ * Used to determine if the control is reporting validity from itself, or
106
+ * if a `<form>` is causing the validity report. Forms have different
107
+ * control focusing behavior.
108
+ */
109
+ [ privateIsSelfReportingValidity ] = false ;
110
+
101
111
// Mixins must have a constructor with `...args: any[]`
102
112
// tslint:disable-next-line:no-any
103
113
constructor ( ...args : any [ ] ) {
@@ -124,9 +134,7 @@ export function mixinOnReportValidity<
124
134
// A normal bubbling phase event listener. By adding it here, we
125
135
// ensure it's the last event listener that is called during the
126
136
// bubbling phase.
127
- if ( ! invalidEvent . defaultPrevented ) {
128
- this [ onReportValidity ] ( invalidEvent ) ;
129
- }
137
+ this [ privateCallOnReportValidity ] ( invalidEvent ) ;
130
138
} ,
131
139
{ once : true } ,
132
140
) ;
@@ -149,15 +157,50 @@ export function mixinOnReportValidity<
149
157
}
150
158
151
159
override reportValidity ( ) {
160
+ this [ privateIsSelfReportingValidity ] = true ;
152
161
const valid = super . reportValidity ( ) ;
153
162
// Constructor's invalid listener will handle reporting invalid events.
154
163
if ( valid ) {
155
- this [ onReportValidity ] ( null ) ;
164
+ this [ privateCallOnReportValidity ] ( null ) ;
156
165
}
157
166
167
+ this [ privateIsSelfReportingValidity ] = false ;
158
168
return valid ;
159
169
}
160
170
171
+ [ privateCallOnReportValidity ] ( invalidEvent : Event | null ) {
172
+ // Since invalid events do not bubble to parent listeners, and because
173
+ // our invalid listeners are added lazily after other listeners, we can
174
+ // reliably read `defaultPrevented` synchronously without worrying
175
+ // about waiting for another listener that could cancel it.
176
+ const wasCanceled = invalidEvent ?. defaultPrevented ;
177
+ if ( wasCanceled ) {
178
+ return ;
179
+ }
180
+
181
+ this [ onReportValidity ] ( invalidEvent ) ;
182
+
183
+ // If an implementation calls invalidEvent.preventDefault() to stop the
184
+ // platform popup from displaying, focusing is also prevented, so we need
185
+ // to manually focus.
186
+ const implementationCanceledFocus =
187
+ ! wasCanceled && invalidEvent ?. defaultPrevented ;
188
+ if ( ! implementationCanceledFocus ) {
189
+ return ;
190
+ }
191
+
192
+ // The control should be focused when:
193
+ // - `control.reportValidity()` is called (self-reporting).
194
+ // - a form is reporting validity for its controls and this is the first
195
+ // invalid control.
196
+ if (
197
+ this [ privateIsSelfReportingValidity ] ||
198
+ isFirstInvalidControlInForm ( this [ internals ] . form , this )
199
+ ) {
200
+ this . focus ( ) ;
201
+ }
202
+ }
203
+
161
204
[ onReportValidity ] ( invalidEvent : Event | null ) {
162
205
throw new Error ( 'Implement [onReportValidity]' ) ;
163
206
}
@@ -185,7 +228,7 @@ export function mixinOnReportValidity<
185
228
this ,
186
229
form ,
187
230
( ) => {
188
- this [ onReportValidity ] ( null ) ;
231
+ this [ privateCallOnReportValidity ] ( null ) ;
189
232
} ,
190
233
this [ privateCleanupFormListeners ] . signal ,
191
234
) ;
@@ -328,3 +371,31 @@ function getFormValidateHooks(form: HTMLFormElement) {
328
371
329
372
return FORM_VALIDATE_HOOKS . get ( form ) ! ;
330
373
}
374
+
375
+ /**
376
+ * Checks if a control is the first invalid control in a form.
377
+ *
378
+ * @param form The control's form. When `null`, the control doesn't have a form
379
+ * and the method returns true.
380
+ * @param control The control to check.
381
+ * @return True if there is no form or if the control is the form's first
382
+ * invalid control.
383
+ */
384
+ function isFirstInvalidControlInForm (
385
+ form : HTMLFormElement | null ,
386
+ control : HTMLElement ,
387
+ ) {
388
+ if ( ! form ) {
389
+ return true ;
390
+ }
391
+
392
+ let firstInvalidControl : Element | undefined ;
393
+ for ( const element of form . elements ) {
394
+ if ( element . matches ( ':invalid' ) ) {
395
+ firstInvalidControl = element ;
396
+ break ;
397
+ }
398
+ }
399
+
400
+ return firstInvalidControl === control ;
401
+ }
0 commit comments