@vaadin/field-base 22.0.0-beta2 → 22.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +4 -4
- package/src/field-aria-controller.d.ts +56 -0
- package/src/field-aria-controller.js +175 -0
- package/src/field-mixin.d.ts +12 -10
- package/src/field-mixin.js +54 -53
- package/src/input-control-mixin.d.ts +2 -0
- package/src/input-controller.d.ts +2 -2
- package/src/input-controller.js +4 -3
- package/src/input-field-mixin.d.ts +2 -0
- package/src/label-mixin.js +0 -1
- package/src/{aria-label-controller.d.ts → labelled-input-controller.d.ts} +2 -2
- package/src/{aria-label-controller.js → labelled-input-controller.js} +3 -22
- package/src/text-area-controller.d.ts +2 -2
- package/src/text-area-controller.js +4 -3
- package/src/slot-controller.d.ts +0 -8
- package/src/slot-controller.js +0 -36
package/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export { AriaLabelController } from './src/aria-label-controller.js';
|
|
2
1
|
export { CheckedMixin } from './src/checked-mixin.js';
|
|
3
2
|
export { DelegateFocusMixin } from './src/delegate-focus-mixin.js';
|
|
4
3
|
export { DelegateStateMixin } from './src/delegate-state-mixin.js';
|
|
4
|
+
export { FieldAriaController } from './src/field-aria-controller.js';
|
|
5
5
|
export { FieldMixin } from './src/field-mixin.js';
|
|
6
6
|
export { InputController } from './src/input-controller.js';
|
|
7
7
|
export { InputControlMixin } from './src/input-control-mixin.js';
|
|
8
8
|
export { InputFieldMixin } from './src/input-field-mixin.js';
|
|
9
9
|
export { InputMixin } from './src/input-mixin.js';
|
|
10
|
+
export { LabelledInputController } from './src/labelled-input-controller.js';
|
|
10
11
|
export { LabelMixin } from './src/label-mixin.js';
|
|
11
12
|
export { PatternMixin } from './src/pattern-mixin.js';
|
|
12
13
|
export { ShadowFocusMixin } from './src/shadow-focus-mixin.js';
|
package/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
export { AriaLabelController } from './src/aria-label-controller.js';
|
|
2
1
|
export { CheckedMixin } from './src/checked-mixin.js';
|
|
3
2
|
export { DelegateFocusMixin } from './src/delegate-focus-mixin.js';
|
|
4
3
|
export { DelegateStateMixin } from './src/delegate-state-mixin.js';
|
|
4
|
+
export { FieldAriaController } from './src/field-aria-controller.js';
|
|
5
5
|
export { FieldMixin } from './src/field-mixin.js';
|
|
6
6
|
export { InputController } from './src/input-controller.js';
|
|
7
7
|
export { InputControlMixin } from './src/input-control-mixin.js';
|
|
8
8
|
export { InputFieldMixin } from './src/input-field-mixin.js';
|
|
9
9
|
export { InputMixin } from './src/input-mixin.js';
|
|
10
|
+
export { LabelledInputController } from './src/labelled-input-controller.js';
|
|
10
11
|
export { LabelMixin } from './src/label-mixin.js';
|
|
11
12
|
export { PatternMixin } from './src/pattern-mixin.js';
|
|
12
13
|
export { ShadowFocusMixin } from './src/shadow-focus-mixin.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaadin/field-base",
|
|
3
|
-
"version": "22.0.
|
|
3
|
+
"version": "22.0.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@open-wc/dedupe-mixin": "^1.3.0",
|
|
33
33
|
"@polymer/polymer": "^3.0.0",
|
|
34
|
-
"@vaadin/component-base": "22.0.
|
|
34
|
+
"@vaadin/component-base": "^22.0.2",
|
|
35
35
|
"lit": "^2.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@esm-bundle/chai": "^4.3.4",
|
|
39
|
-
"@vaadin/testing-helpers": "^0.3.
|
|
39
|
+
"@vaadin/testing-helpers": "^0.3.2",
|
|
40
40
|
"sinon": "^9.2.1"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "df21370c4a655a38eac11f79686021ab3b0887ad"
|
|
43
43
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A controller for managing ARIA attributes for a field element:
|
|
9
|
+
* either the component itself or slotted `<input>` element.
|
|
10
|
+
*/
|
|
11
|
+
export class FieldAriaController {
|
|
12
|
+
constructor(host: HTMLElement);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The controller host element.
|
|
16
|
+
*/
|
|
17
|
+
host: HTMLElement;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets a target element to which ARIA attributes are added.
|
|
21
|
+
*/
|
|
22
|
+
setTarget(target: HTMLElement): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Toggles the `aria-required` attribute on the target element
|
|
26
|
+
* if the target is the host component (e.g. a field group).
|
|
27
|
+
* Otherwise, it does nothing.
|
|
28
|
+
*/
|
|
29
|
+
setRequired(required: boolean): void;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Links the target element with a slotted label element
|
|
33
|
+
* via the target's attribute `aria-labelledby`.
|
|
34
|
+
*
|
|
35
|
+
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
36
|
+
*/
|
|
37
|
+
setLabelId(labelId: string | null): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Links the target element with a slotted error element via the target's attribute:
|
|
41
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
42
|
+
* - `aria-describedby` otherwise.
|
|
43
|
+
*
|
|
44
|
+
* To unlink the previous slotted error element, pass `null` as `errorId`.
|
|
45
|
+
*/
|
|
46
|
+
setErrorId(errorId: string | null): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Links the target element with a slotted helper element via the target's attribute:
|
|
50
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
51
|
+
* - `aria-describedby` otherwise.
|
|
52
|
+
*
|
|
53
|
+
* To unlink the previous slotted helper element, pass `null` as `helperId`.
|
|
54
|
+
*/
|
|
55
|
+
setHelperId(helperId: string | null): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A controller for managing ARIA attributes for a field element:
|
|
9
|
+
* either the component itself or slotted `<input>` element.
|
|
10
|
+
*/
|
|
11
|
+
export class FieldAriaController {
|
|
12
|
+
constructor(host) {
|
|
13
|
+
this.host = host;
|
|
14
|
+
this.__required = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets a target element to which ARIA attributes are added.
|
|
19
|
+
*
|
|
20
|
+
* @param {HTMLElement} target
|
|
21
|
+
*/
|
|
22
|
+
setTarget(target) {
|
|
23
|
+
this.__target = target;
|
|
24
|
+
this.__setAriaRequiredAttribute(this.__required);
|
|
25
|
+
this.__setLabelIdToAriaAttribute(this.__labelId);
|
|
26
|
+
this.__setErrorIdToAriaAttribute(this.__errorId);
|
|
27
|
+
this.__setHelperIdToAriaAttribute(this.__helperId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Toggles the `aria-required` attribute on the target element
|
|
32
|
+
* if the target is the host component (e.g. a field group).
|
|
33
|
+
* Otherwise, it does nothing.
|
|
34
|
+
*
|
|
35
|
+
* @param {boolean} required
|
|
36
|
+
*/
|
|
37
|
+
setRequired(required) {
|
|
38
|
+
this.__setAriaRequiredAttribute(required);
|
|
39
|
+
this.__required = required;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Links the target element with a slotted label element
|
|
44
|
+
* via the target's attribute `aria-labelledby`.
|
|
45
|
+
*
|
|
46
|
+
* To unlink the previous slotted label element, pass `null` as `labelId`.
|
|
47
|
+
*
|
|
48
|
+
* @param {string | null} labelId
|
|
49
|
+
*/
|
|
50
|
+
setLabelId(labelId) {
|
|
51
|
+
this.__setLabelIdToAriaAttribute(labelId, this.__labelId);
|
|
52
|
+
this.__labelId = labelId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Links the target element with a slotted error element via the target's attribute:
|
|
57
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
58
|
+
* - `aria-describedby` otherwise.
|
|
59
|
+
*
|
|
60
|
+
* To unlink the previous slotted error element, pass `null` as `errorId`.
|
|
61
|
+
*
|
|
62
|
+
* @param {string | null} errorId
|
|
63
|
+
*/
|
|
64
|
+
setErrorId(errorId) {
|
|
65
|
+
this.__setErrorIdToAriaAttribute(errorId, this.__errorId);
|
|
66
|
+
this.__errorId = errorId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Links the target element with a slotted helper element via the target's attribute:
|
|
71
|
+
* - `aria-labelledby` if the target is the host component (e.g a field group).
|
|
72
|
+
* - `aria-describedby` otherwise.
|
|
73
|
+
*
|
|
74
|
+
* To unlink the previous slotted helper element, pass `null` as `helperId`.
|
|
75
|
+
*
|
|
76
|
+
* @param {string | null} helperId
|
|
77
|
+
*/
|
|
78
|
+
setHelperId(helperId) {
|
|
79
|
+
this.__setHelperIdToAriaAttribute(helperId, this.__helperId);
|
|
80
|
+
this.__helperId = helperId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* `true` if the target element is the host component itself, `false` otherwise.
|
|
85
|
+
*
|
|
86
|
+
* @return {boolean}
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
get __isGroupField() {
|
|
90
|
+
return this.__target === this.host;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string | null | undefined} labelId
|
|
95
|
+
* @param {string | null | undefined} oldLabelId
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
__setLabelIdToAriaAttribute(labelId, oldLabelId) {
|
|
99
|
+
this.__setAriaAttributeId('aria-labelledby', labelId, oldLabelId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {string | null | undefined} errorId
|
|
104
|
+
* @param {string | null | undefined} oldErrorId
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
__setErrorIdToAriaAttribute(errorId, oldErrorId) {
|
|
108
|
+
// For groups, add all IDs to aria-labelledby rather than aria-describedby -
|
|
109
|
+
// that should guarantee that it's announced when the group is entered.
|
|
110
|
+
if (this.__isGroupField) {
|
|
111
|
+
this.__setAriaAttributeId('aria-labelledby', errorId, oldErrorId);
|
|
112
|
+
} else {
|
|
113
|
+
this.__setAriaAttributeId('aria-describedby', errorId, oldErrorId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string | null | undefined} helperId
|
|
119
|
+
* @param {string | null | undefined} oldHelperId
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
__setHelperIdToAriaAttribute(helperId, oldHelperId) {
|
|
123
|
+
// For groups, add all IDs to aria-labelledby rather than aria-describedby -
|
|
124
|
+
// that should guarantee that it's announced when the group is entered.
|
|
125
|
+
if (this.__isGroupField) {
|
|
126
|
+
this.__setAriaAttributeId('aria-labelledby', helperId, oldHelperId);
|
|
127
|
+
} else {
|
|
128
|
+
this.__setAriaAttributeId('aria-describedby', helperId, oldHelperId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {boolean} required
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
__setAriaRequiredAttribute(required) {
|
|
137
|
+
if (!this.__target) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!this.__isGroupField) {
|
|
142
|
+
// native <input> or <textarea>, required is enough
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (required) {
|
|
147
|
+
this.__target.setAttribute('aria-required', 'true');
|
|
148
|
+
} else {
|
|
149
|
+
this.__target.removeAttribute('aria-required');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {string | null | undefined} newId
|
|
155
|
+
* @param {string | null | undefined} oldId
|
|
156
|
+
* @private
|
|
157
|
+
*/
|
|
158
|
+
__setAriaAttributeId(attr, newId, oldId) {
|
|
159
|
+
if (!this.__target) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const value = this.__target.getAttribute(attr);
|
|
164
|
+
const ids = value ? new Set(value.split(' ')) : new Set();
|
|
165
|
+
|
|
166
|
+
if (oldId) {
|
|
167
|
+
ids.delete(oldId);
|
|
168
|
+
}
|
|
169
|
+
if (newId) {
|
|
170
|
+
ids.add(newId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.__target.setAttribute(attr, [...ids].join(' '));
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/field-mixin.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
-
import {
|
|
7
|
+
import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
|
|
8
8
|
import { LabelMixinClass } from './label-mixin.js';
|
|
9
9
|
import { ValidateMixinClass } from './validate-mixin.js';
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ import { ValidateMixinClass } from './validate-mixin.js';
|
|
|
14
14
|
export declare function FieldMixin<T extends Constructor<HTMLElement>>(
|
|
15
15
|
superclass: T
|
|
16
16
|
): T &
|
|
17
|
-
Constructor<
|
|
17
|
+
Constructor<ControllerMixinClass> &
|
|
18
18
|
Constructor<FieldMixinClass> &
|
|
19
19
|
Constructor<LabelMixinClass> &
|
|
20
20
|
Constructor<ValidateMixinClass>;
|
|
@@ -33,15 +33,11 @@ export declare class FieldMixinClass {
|
|
|
33
33
|
helperText: string | null | undefined;
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Error to show when the field is invalid.
|
|
36
|
+
* Error message to show when the field is invalid.
|
|
37
37
|
*
|
|
38
38
|
* @attr {string} error-message
|
|
39
39
|
*/
|
|
40
|
-
errorMessage: string;
|
|
41
|
-
|
|
42
|
-
protected readonly _ariaAttr: 'aria-labelledby' | 'aria-describedby';
|
|
43
|
-
|
|
44
|
-
protected _ariaTargetChanged(target: HTMLElement): void;
|
|
40
|
+
errorMessage: string | null | undefined;
|
|
45
41
|
|
|
46
42
|
protected readonly _errorNode: HTMLElement;
|
|
47
43
|
|
|
@@ -49,7 +45,13 @@ export declare class FieldMixinClass {
|
|
|
49
45
|
|
|
50
46
|
protected _helperTextChanged(helperText: string | null | undefined): void;
|
|
51
47
|
|
|
52
|
-
protected
|
|
48
|
+
protected _ariaTargetChanged(target: HTMLElement): void;
|
|
49
|
+
|
|
50
|
+
protected _updateErrorMessage(invalid: boolean, errorMessage: string | null | undefined): void;
|
|
51
|
+
|
|
52
|
+
protected _requiredChanged(required: boolean): void;
|
|
53
|
+
|
|
54
|
+
protected _helperIdChanged(helperId: string): void;
|
|
53
55
|
|
|
54
|
-
protected
|
|
56
|
+
protected _invalidChanged(invalid: boolean): void;
|
|
55
57
|
}
|
package/src/field-mixin.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js';
|
|
7
7
|
import { animationFrame } from '@vaadin/component-base/src/async.js';
|
|
8
|
+
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
|
|
8
9
|
import { Debouncer } from '@vaadin/component-base/src/debounce.js';
|
|
10
|
+
import { FieldAriaController } from './field-aria-controller.js';
|
|
9
11
|
import { LabelMixin } from './label-mixin.js';
|
|
10
12
|
import { ValidateMixin } from './validate-mixin.js';
|
|
11
13
|
|
|
@@ -13,11 +15,12 @@ import { ValidateMixin } from './validate-mixin.js';
|
|
|
13
15
|
* A mixin to provide common field logic: label, error message and helper text.
|
|
14
16
|
*
|
|
15
17
|
* @polymerMixin
|
|
18
|
+
* @mixes ControllerMixin
|
|
16
19
|
* @mixes LabelMixin
|
|
17
20
|
* @mixes ValidateMixin
|
|
18
21
|
*/
|
|
19
22
|
export const FieldMixin = (superclass) =>
|
|
20
|
-
class FieldMixinClass extends ValidateMixin(LabelMixin(superclass)) {
|
|
23
|
+
class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(superclass))) {
|
|
21
24
|
static get properties() {
|
|
22
25
|
return {
|
|
23
26
|
/**
|
|
@@ -66,9 +69,11 @@ export const FieldMixin = (superclass) =>
|
|
|
66
69
|
|
|
67
70
|
static get observers() {
|
|
68
71
|
return [
|
|
69
|
-
'__ariaChanged(invalid, _helperId, required)',
|
|
70
72
|
'__observeOffsetHeight(errorMessage, invalid, label, helperText)',
|
|
71
|
-
'_updateErrorMessage(invalid, errorMessage)'
|
|
73
|
+
'_updateErrorMessage(invalid, errorMessage)',
|
|
74
|
+
'_invalidChanged(invalid)',
|
|
75
|
+
'_requiredChanged(required)',
|
|
76
|
+
'_helperIdChanged(_helperId)'
|
|
72
77
|
];
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -88,14 +93,6 @@ export const FieldMixin = (superclass) =>
|
|
|
88
93
|
return this._getDirectSlotChild('helper');
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
/**
|
|
92
|
-
* @protected
|
|
93
|
-
* @return {string}
|
|
94
|
-
*/
|
|
95
|
-
get _ariaAttr() {
|
|
96
|
-
return 'aria-describedby';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
96
|
constructor() {
|
|
100
97
|
super();
|
|
101
98
|
|
|
@@ -106,6 +103,8 @@ export const FieldMixin = (superclass) =>
|
|
|
106
103
|
|
|
107
104
|
// Save generated ID to restore later
|
|
108
105
|
this.__savedHelperId = this._helperId;
|
|
106
|
+
|
|
107
|
+
this._fieldAriaController = new FieldAriaController(this);
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
/** @protected */
|
|
@@ -166,6 +165,8 @@ export const FieldMixin = (superclass) =>
|
|
|
166
165
|
this.__applyDefaultHelper(this.helperText);
|
|
167
166
|
}
|
|
168
167
|
});
|
|
168
|
+
|
|
169
|
+
this.addController(this._fieldAriaController);
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/** @private */
|
|
@@ -206,6 +207,7 @@ export const FieldMixin = (superclass) =>
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
helper.id = this.__savedHelperId;
|
|
210
|
+
this._helperId = helper.id;
|
|
209
211
|
this.appendChild(helper);
|
|
210
212
|
this._currentHelper = helper;
|
|
211
213
|
|
|
@@ -266,9 +268,25 @@ export const FieldMixin = (superclass) =>
|
|
|
266
268
|
);
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
/**
|
|
272
|
+
* @protected
|
|
273
|
+
* @override
|
|
274
|
+
*/
|
|
275
|
+
_toggleHasLabelAttribute() {
|
|
276
|
+
super._toggleHasLabelAttribute();
|
|
277
|
+
|
|
278
|
+
// Label ID should be only added when the label content is present.
|
|
279
|
+
// Otherwise, it may conflict with an `aria-label` attribute possibly added by the user.
|
|
280
|
+
if (this.hasAttribute('has-label')) {
|
|
281
|
+
this._fieldAriaController.setLabelId(this._labelId);
|
|
282
|
+
} else {
|
|
283
|
+
this._fieldAriaController.setLabelId(null);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
269
287
|
/**
|
|
270
288
|
* @param {boolean} invalid
|
|
271
|
-
* @param {string} errorMessage
|
|
289
|
+
* @param {string | null | undefined} errorMessage
|
|
272
290
|
* @protected
|
|
273
291
|
*/
|
|
274
292
|
_updateErrorMessage(invalid, errorMessage) {
|
|
@@ -283,6 +301,7 @@ export const FieldMixin = (superclass) =>
|
|
|
283
301
|
}
|
|
284
302
|
const hasError = Boolean(invalid && errorMessage);
|
|
285
303
|
error.textContent = hasError ? errorMessage : '';
|
|
304
|
+
error.hidden = !hasError;
|
|
286
305
|
this.toggleAttribute('has-error-message', hasError);
|
|
287
306
|
|
|
288
307
|
// Role alert will make the error message announce immediately
|
|
@@ -320,66 +339,48 @@ export const FieldMixin = (superclass) =>
|
|
|
320
339
|
}
|
|
321
340
|
|
|
322
341
|
/**
|
|
323
|
-
* @param {HTMLElement} target
|
|
342
|
+
* @param {HTMLElement | null | undefined} target
|
|
324
343
|
* @protected
|
|
325
344
|
*/
|
|
326
345
|
_ariaTargetChanged(target) {
|
|
327
346
|
if (target) {
|
|
328
|
-
this.
|
|
329
|
-
this._updateAriaRequiredAttribute(target, this.required);
|
|
347
|
+
this._fieldAriaController.setTarget(target);
|
|
330
348
|
}
|
|
331
349
|
}
|
|
332
350
|
|
|
333
351
|
/**
|
|
334
|
-
* @param {
|
|
335
|
-
* @param {boolean} invalid
|
|
336
|
-
* @param {string} helperId
|
|
352
|
+
* @param {boolean} required
|
|
337
353
|
* @protected
|
|
338
354
|
*/
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (target && attr) {
|
|
343
|
-
// For groups, add all IDs to aria-labelledby rather than aria-describedby -
|
|
344
|
-
// that should guarantee that it's announced when the group is entered.
|
|
345
|
-
const ariaIds = attr === 'aria-describedby' ? [helperId] : [this._labelId, helperId];
|
|
346
|
-
|
|
347
|
-
// Error message ID needs to be dynamically added / removed based on the validity
|
|
348
|
-
// Otherwise assistive technologies would announce the error, even if we hide it.
|
|
349
|
-
if (invalid) {
|
|
350
|
-
ariaIds.push(this._errorId);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
target.setAttribute(attr, ariaIds.join(' '));
|
|
354
|
-
}
|
|
355
|
+
_requiredChanged(required) {
|
|
356
|
+
this._fieldAriaController.setRequired(required);
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
/**
|
|
358
|
-
* @param {
|
|
359
|
-
* @param {boolean} required
|
|
360
|
+
* @param {string} helperId
|
|
360
361
|
* @protected
|
|
361
362
|
*/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// native <input> or <textarea>, required is enough
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (required) {
|
|
369
|
-
target.setAttribute('aria-required', true);
|
|
370
|
-
} else {
|
|
371
|
-
target.removeAttribute('aria-required');
|
|
372
|
-
}
|
|
363
|
+
_helperIdChanged(helperId) {
|
|
364
|
+
this._fieldAriaController.setHelperId(helperId);
|
|
373
365
|
}
|
|
374
366
|
|
|
375
367
|
/**
|
|
376
|
-
* @param {boolean} invalid
|
|
377
|
-
* @param {string} helperId
|
|
378
368
|
* @param {boolean} required
|
|
379
|
-
* @
|
|
369
|
+
* @protected
|
|
380
370
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
371
|
+
_invalidChanged(invalid) {
|
|
372
|
+
// This timeout is needed to prevent NVDA from announcing the error message twice:
|
|
373
|
+
// 1. Once adding the `[role=alert]` attribute by the `_updateErrorMessage` method (OK).
|
|
374
|
+
// 2. Once linking the error ID with the ARIA target here (unwanted).
|
|
375
|
+
// Related issue: https://github.com/vaadin/web-components/issues/3061.
|
|
376
|
+
setTimeout(() => {
|
|
377
|
+
// Error message ID needs to be dynamically added / removed based on the validity
|
|
378
|
+
// Otherwise assistive technologies would announce the error, even if we hide it.
|
|
379
|
+
if (invalid) {
|
|
380
|
+
this._fieldAriaController.setErrorId(this._errorId);
|
|
381
|
+
} else {
|
|
382
|
+
this._fieldAriaController.setErrorId(null);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
384
385
|
}
|
|
385
386
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
|
|
7
8
|
import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
|
|
8
9
|
import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
|
|
9
10
|
import { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
|
|
@@ -21,6 +22,7 @@ import { ValidateMixinClass } from './validate-mixin.js';
|
|
|
21
22
|
export declare function InputControlMixin<T extends Constructor<HTMLElement>>(
|
|
22
23
|
base: T
|
|
23
24
|
): T &
|
|
25
|
+
Constructor<ControllerMixinClass> &
|
|
24
26
|
Constructor<DelegateFocusMixinClass> &
|
|
25
27
|
Constructor<DelegateStateMixinClass> &
|
|
26
28
|
Constructor<DisabledMixinClass> &
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import { SlotController } from '
|
|
6
|
+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A controller to create and initialize slotted `<input>` element.
|
|
10
10
|
*/
|
|
11
|
-
export class InputController
|
|
11
|
+
export class InputController extends SlotController {}
|
package/src/input-controller.js
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import { SlotController } from '
|
|
6
|
+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A controller to create and initialize slotted `<input>` element.
|
|
10
10
|
*/
|
|
11
11
|
export class InputController extends SlotController {
|
|
12
12
|
constructor(host, callback) {
|
|
13
|
-
super(
|
|
13
|
+
super(
|
|
14
|
+
host,
|
|
14
15
|
'input',
|
|
15
16
|
() => document.createElement('input'),
|
|
16
17
|
(host, node) => {
|
|
@@ -30,6 +31,6 @@ export class InputController extends SlotController {
|
|
|
30
31
|
callback(node);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
6
|
import { Constructor } from '@open-wc/dedupe-mixin';
|
|
7
|
+
import { ControllerMixinClass } from '@vaadin/component-base/src/controller-mixin.js';
|
|
7
8
|
import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
|
|
8
9
|
import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
|
|
9
10
|
import { KeyboardMixinClass } from '@vaadin/component-base/src/keyboard-mixin.js';
|
|
@@ -22,6 +23,7 @@ import { ValidateMixinClass } from './validate-mixin.js';
|
|
|
22
23
|
export declare function InputFieldMixin<T extends Constructor<HTMLElement>>(
|
|
23
24
|
base: T
|
|
24
25
|
): T &
|
|
26
|
+
Constructor<ControllerMixinClass> &
|
|
25
27
|
Constructor<DelegateFocusMixinClass> &
|
|
26
28
|
Constructor<DelegateStateMixinClass> &
|
|
27
29
|
Constructor<DisabledMixinClass> &
|
package/src/label-mixin.js
CHANGED
|
@@ -87,7 +87,6 @@ export const LabelMixin = dedupingMixin(
|
|
|
87
87
|
const hasLabel = this._labelNode.children.length > 0 || this._labelNode.textContent.trim() !== '';
|
|
88
88
|
|
|
89
89
|
this.toggleAttribute('has-label', hasLabel);
|
|
90
|
-
this.dispatchEvent(new CustomEvent('has-label-changed', { detail: { value: hasLabel } }));
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
import { ReactiveController } from 'lit';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* A controller
|
|
9
|
+
* A controller for linking a `<label>` element with an `<input>` element.
|
|
10
10
|
*/
|
|
11
|
-
export class
|
|
11
|
+
export class LabelledInputController implements ReactiveController {}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* A controller
|
|
8
|
+
* A controller for linking a `<label>` element with an `<input>` element.
|
|
9
9
|
*/
|
|
10
|
-
export class
|
|
11
|
-
constructor(
|
|
10
|
+
export class LabelledInputController {
|
|
11
|
+
constructor(input, label) {
|
|
12
12
|
this.input = input;
|
|
13
13
|
this.__preventDuplicateLabelClick = this.__preventDuplicateLabelClick.bind(this);
|
|
14
14
|
|
|
@@ -17,11 +17,6 @@ export class AriaLabelController {
|
|
|
17
17
|
|
|
18
18
|
if (input) {
|
|
19
19
|
label.setAttribute('for', input.id);
|
|
20
|
-
|
|
21
|
-
this.__setAriaLabelledBy(input, host.hasAttribute('has-label') ? label.id : null);
|
|
22
|
-
host.addEventListener('has-label-changed', (event) =>
|
|
23
|
-
this.__setAriaLabelledBy(input, event.detail.value ? label.id : null)
|
|
24
|
-
);
|
|
25
20
|
}
|
|
26
21
|
}
|
|
27
22
|
}
|
|
@@ -41,18 +36,4 @@ export class AriaLabelController {
|
|
|
41
36
|
};
|
|
42
37
|
this.input.addEventListener('click', inputClickHandler);
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Sets or removes the `aria-labelledby` attribute on the input element.
|
|
47
|
-
* @param {HTMLElement} input
|
|
48
|
-
* @param {string | null | undefined} value
|
|
49
|
-
* @private
|
|
50
|
-
*/
|
|
51
|
-
__setAriaLabelledBy(input, value) {
|
|
52
|
-
if (value) {
|
|
53
|
-
input.setAttribute('aria-labelledby', value);
|
|
54
|
-
} else {
|
|
55
|
-
input.removeAttribute('aria-labelledby');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
39
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import { SlotController } from '
|
|
6
|
+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A controller to create and initialize slotted `<textarea>` element.
|
|
10
10
|
*/
|
|
11
|
-
export class TextAreaController
|
|
11
|
+
export class TextAreaController extends SlotController {}
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
4
|
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
5
|
*/
|
|
6
|
-
import { SlotController } from '
|
|
6
|
+
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A controller to create and initialize slotted `<textarea>` element.
|
|
10
10
|
*/
|
|
11
11
|
export class TextAreaController extends SlotController {
|
|
12
12
|
constructor(host, callback) {
|
|
13
|
-
super(
|
|
13
|
+
super(
|
|
14
|
+
host,
|
|
14
15
|
'textarea',
|
|
15
16
|
() => document.createElement('textarea'),
|
|
16
17
|
(host, node) => {
|
|
@@ -33,6 +34,6 @@ export class TextAreaController extends SlotController {
|
|
|
33
34
|
callback(node);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
);
|
|
37
38
|
}
|
|
38
39
|
}
|
package/src/slot-controller.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
import { ReactiveController } from 'lit';
|
|
7
|
-
|
|
8
|
-
export class SlotController implements ReactiveController {}
|
package/src/slot-controller.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
-
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
-
*/
|
|
6
|
-
export class SlotController {
|
|
7
|
-
constructor(host, [slotName, slotFactory, slotInitializer]) {
|
|
8
|
-
this.host = host;
|
|
9
|
-
this.slotName = slotName;
|
|
10
|
-
this.slotFactory = slotFactory;
|
|
11
|
-
this.slotInitializer = slotInitializer;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
hostConnected() {
|
|
15
|
-
if (!this.__initialized) {
|
|
16
|
-
const { host, slotName, slotFactory } = this;
|
|
17
|
-
|
|
18
|
-
const slotted = host.querySelector(`[slot=${slotName}]`);
|
|
19
|
-
|
|
20
|
-
if (!slotted) {
|
|
21
|
-
const slotContent = slotFactory(host);
|
|
22
|
-
if (slotContent instanceof Element) {
|
|
23
|
-
slotContent.setAttribute('slot', slotName);
|
|
24
|
-
host.appendChild(slotContent);
|
|
25
|
-
this.__slotContent = slotContent;
|
|
26
|
-
}
|
|
27
|
-
} else {
|
|
28
|
-
this.__slotContent = slotted;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
this.slotInitializer(host, this.__slotContent);
|
|
32
|
-
|
|
33
|
-
this.__initialized = true;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|