@vaadin/field-base 23.0.0-alpha5 → 23.0.0-beta4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/field-base",
3
- "version": "23.0.0-alpha5",
3
+ "version": "23.0.0-beta4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@open-wc/dedupe-mixin": "^1.3.0",
34
34
  "@polymer/polymer": "^3.0.0",
35
- "@vaadin/component-base": "23.0.0-alpha5",
35
+ "@vaadin/component-base": "23.0.0-beta4",
36
36
  "lit": "^2.0.0"
37
37
  },
38
38
  "devDependencies": {
@@ -40,5 +40,5 @@
40
40
  "@vaadin/testing-helpers": "^0.3.2",
41
41
  "sinon": "^9.2.1"
42
42
  },
43
- "gitHead": "74f9294964eb8552d96578c14af6ad214f5257bc"
43
+ "gitHead": "d0b447f1c31ca4256a5e26f2dcd27784447ff79b"
44
44
  }
@@ -6,13 +6,18 @@
6
6
  import { Constructor } from '@open-wc/dedupe-mixin';
7
7
  import { DisabledMixinClass } from '@vaadin/component-base/src/disabled-mixin.js';
8
8
  import { FocusMixinClass } from '@vaadin/component-base/src/focus-mixin.js';
9
+ import { TabindexMixinClass } from '@vaadin/component-base/src/tabindex-mixin.js';
9
10
 
10
11
  /**
11
12
  * A mixin to forward focus to an element in the light DOM.
12
13
  */
13
14
  export declare function DelegateFocusMixin<T extends Constructor<HTMLElement>>(
14
15
  base: T
15
- ): T & Constructor<DelegateFocusMixinClass> & Constructor<DisabledMixinClass> & Constructor<FocusMixinClass>;
16
+ ): T &
17
+ Constructor<DelegateFocusMixinClass> &
18
+ Constructor<DisabledMixinClass> &
19
+ Constructor<FocusMixinClass> &
20
+ Constructor<TabindexMixinClass>;
16
21
 
17
22
  export declare class DelegateFocusMixinClass {
18
23
  /**
@@ -4,19 +4,19 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
- import { DisabledMixin } from '@vaadin/component-base/src/disabled-mixin.js';
8
7
  import { FocusMixin } from '@vaadin/component-base/src/focus-mixin.js';
8
+ import { TabindexMixin } from '@vaadin/component-base/src/tabindex-mixin.js';
9
9
 
10
10
  /**
11
11
  * A mixin to forward focus to an element in the light DOM.
12
12
  *
13
13
  * @polymerMixin
14
- * @mixes DisabledMixin
15
14
  * @mixes FocusMixin
15
+ * @mixes TabindexMixin
16
16
  */
17
17
  export const DelegateFocusMixin = dedupingMixin(
18
18
  (superclass) =>
19
- class DelegateFocusMixinClass extends FocusMixin(DisabledMixin(superclass)) {
19
+ class DelegateFocusMixinClass extends FocusMixin(TabindexMixin(superclass)) {
20
20
  static get properties() {
21
21
  return {
22
22
  /**
@@ -40,6 +40,19 @@ export const DelegateFocusMixin = dedupingMixin(
40
40
  type: Object,
41
41
  readOnly: true,
42
42
  observer: '_focusElementChanged'
43
+ },
44
+
45
+ /**
46
+ * Indicates whether the element can be focused and where it participates in sequential keyboard navigation.
47
+ *
48
+ * By default, the host element does not have tabindex attribute. Instead, `focusElement` should have it.
49
+ * Toggling `tabindex` attribute on the host element propagates its value to `focusElement`.
50
+ *
51
+ * @protected
52
+ */
53
+ tabindex: {
54
+ type: Number,
55
+ value: undefined
43
56
  }
44
57
  };
45
58
  }
@@ -103,6 +116,7 @@ export const DelegateFocusMixin = dedupingMixin(
103
116
  if (element) {
104
117
  element.disabled = this.disabled;
105
118
  this._addFocusListeners(element);
119
+ this.__forwardTabIndex(this.tabindex);
106
120
  } else if (oldElement) {
107
121
  this._removeFocusListeners(oldElement);
108
122
  }
@@ -162,10 +176,12 @@ export const DelegateFocusMixin = dedupingMixin(
162
176
 
163
177
  /**
164
178
  * @param {boolean} disabled
179
+ * @param {boolean} oldDisabled
165
180
  * @protected
181
+ * @override
166
182
  */
167
- _disabledChanged(disabled) {
168
- super._disabledChanged(disabled);
183
+ _disabledChanged(disabled, oldDisabled) {
184
+ super._disabledChanged(disabled, oldDisabled);
169
185
 
170
186
  if (this.focusElement) {
171
187
  this.focusElement.disabled = disabled;
@@ -175,5 +191,37 @@ export const DelegateFocusMixin = dedupingMixin(
175
191
  this.blur();
176
192
  }
177
193
  }
194
+
195
+ /**
196
+ * Override an observer from `TabindexMixin`.
197
+ * Do not call super to remove tabindex attribute
198
+ * from the host after it has been forwarded.
199
+ * @param {string} tabindex
200
+ * @protected
201
+ * @override
202
+ */
203
+ _tabindexChanged(tabindex) {
204
+ this.__forwardTabIndex(tabindex);
205
+ }
206
+
207
+ /** @private */
208
+ __forwardTabIndex(tabindex) {
209
+ if (tabindex !== undefined && this.focusElement) {
210
+ this.focusElement.tabIndex = tabindex;
211
+
212
+ // Preserve tabindex="-1" on the host element
213
+ if (tabindex !== -1) {
214
+ this.tabindex = undefined;
215
+ }
216
+ }
217
+
218
+ if (this.disabled && tabindex) {
219
+ // If tabindex attribute was changed while component was disabled
220
+ if (tabindex !== -1) {
221
+ this.__lastTabIndex = tabindex;
222
+ }
223
+ this.tabindex = undefined;
224
+ }
225
+ }
178
226
  }
179
227
  );
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
7
+
8
+ /**
9
+ * A controller that manages the error message node content.
10
+ */
11
+ export class ErrorController extends SlotController {
12
+ /**
13
+ * ID attribute value set on the error message element.
14
+ */
15
+ readonly errorId: string;
16
+
17
+ /**
18
+ * String used for the error message text content.
19
+ */
20
+ protected errorMessage: string | null | undefined;
21
+
22
+ /**
23
+ * Set to true when the host element is invalid.
24
+ */
25
+ protected invalid: boolean;
26
+
27
+ /**
28
+ * Set the error message element text content.
29
+ */
30
+ setErrorMessage(errorMessage: string | null | undefined): void;
31
+
32
+ /**
33
+ * Set invalid state for detecting whether to show error message.
34
+ */
35
+ setInvalid(invalid: boolean): void;
36
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
7
+
8
+ /**
9
+ * A controller that manages the error message node content.
10
+ */
11
+ export class ErrorController extends SlotController {
12
+ constructor(host) {
13
+ super(
14
+ host,
15
+ 'error-message',
16
+ () => document.createElement('div'),
17
+ (_host, node) => {
18
+ this.__updateErrorId(node);
19
+
20
+ this.__updateHasError();
21
+ }
22
+ );
23
+ }
24
+
25
+ /**
26
+ * ID attribute value set on the error message element.
27
+ *
28
+ * @return {string}
29
+ */
30
+ get errorId() {
31
+ return this.node && this.node.id;
32
+ }
33
+
34
+ /**
35
+ * Set the error message element text content.
36
+ *
37
+ * @param {string} errorMessage
38
+ */
39
+ setErrorMessage(errorMessage) {
40
+ this.errorMessage = errorMessage;
41
+
42
+ this.__updateHasError();
43
+ }
44
+
45
+ /**
46
+ * Set invalid state for detecting whether to show error message.
47
+ *
48
+ * @param {boolean} invalid
49
+ */
50
+ setInvalid(invalid) {
51
+ this.invalid = invalid;
52
+
53
+ this.__updateHasError();
54
+ }
55
+
56
+ /**
57
+ * Override to initialize the newly added custom label.
58
+ *
59
+ * @param {Node} errorNode
60
+ * @protected
61
+ * @override
62
+ */
63
+ initCustomNode(errorNode) {
64
+ this.__updateErrorId(errorNode);
65
+
66
+ // Save the custom error message content on the host.
67
+ if (errorNode.textContent && !this.errorMessage) {
68
+ this.errorMessage = errorNode.textContent.trim();
69
+ }
70
+
71
+ this.__updateHasError();
72
+ }
73
+
74
+ /**
75
+ * Override to cleanup label node when it's removed.
76
+ *
77
+ * @param {Node} node
78
+ * @protected
79
+ * @override
80
+ */
81
+ teardownNode(node) {
82
+ let errorNode = this.getSlotChild();
83
+
84
+ // If custom error was removed, restore the default one.
85
+ if (!errorNode && node !== this.defaultNode) {
86
+ errorNode = this.attachDefaultNode();
87
+
88
+ // Run initializer to update default label and ID.
89
+ this.initNode(errorNode);
90
+ }
91
+
92
+ this.__updateHasError();
93
+ }
94
+
95
+ /**
96
+ * @param {string} error
97
+ * @private
98
+ */
99
+ __isNotEmpty(error) {
100
+ return Boolean(error && error.trim() !== '');
101
+ }
102
+
103
+ /** @private */
104
+ __updateHasError() {
105
+ const errorNode = this.node;
106
+ const hasError = Boolean(this.invalid && this.__isNotEmpty(this.errorMessage));
107
+
108
+ // Update both default and custom error message node.
109
+ if (errorNode) {
110
+ errorNode.textContent = hasError ? this.errorMessage : '';
111
+ errorNode.hidden = !hasError;
112
+
113
+ // Role alert will make the error message announce immediately
114
+ // as the field becomes invalid
115
+ if (hasError) {
116
+ errorNode.setAttribute('role', 'alert');
117
+ } else {
118
+ errorNode.removeAttribute('role');
119
+ }
120
+ }
121
+
122
+ this.host.toggleAttribute('has-error-message', hasError);
123
+ }
124
+
125
+ /**
126
+ * @param {HTMLElement} errorNode
127
+ * @private
128
+ */
129
+ __updateErrorId(errorNode) {
130
+ if (!errorNode.id) {
131
+ errorNode.id = this.defaultId;
132
+ }
133
+ }
134
+ }
@@ -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 { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
7
- import { SlotMixin } from '@vaadin/component-base/src/slot-mixin.js';
7
+ import { ErrorController } from './error-controller.js';
8
8
  import { FieldAriaController } from './field-aria-controller.js';
9
9
  import { HelperController } from './helper-controller.js';
10
10
  import { LabelMixin } from './label-mixin.js';
@@ -16,11 +16,10 @@ import { ValidateMixin } from './validate-mixin.js';
16
16
  * @polymerMixin
17
17
  * @mixes ControllerMixin
18
18
  * @mixes LabelMixin
19
- * @mixes SlotMixin
20
19
  * @mixes ValidateMixin
21
20
  */
22
21
  export const FieldMixin = (superclass) =>
23
- class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(SlotMixin(superclass)))) {
22
+ class FieldMixinClass extends ValidateMixin(LabelMixin(ControllerMixin(superclass))) {
24
23
  static get properties() {
25
24
  return {
26
25
  /**
@@ -38,7 +37,8 @@ export const FieldMixin = (superclass) =>
38
37
  * @attr {string} error-message
39
38
  */
40
39
  errorMessage: {
41
- type: String
40
+ type: String,
41
+ observer: '_errorMessageChanged'
42
42
  },
43
43
 
44
44
  /**
@@ -55,20 +55,13 @@ export const FieldMixin = (superclass) =>
55
55
  };
56
56
  }
57
57
 
58
- /** @protected */
59
- get slots() {
60
- return {
61
- ...super.slots,
62
- 'error-message': () => {
63
- const error = document.createElement('div');
64
- error.textContent = this.errorMessage;
65
- return error;
66
- }
67
- };
58
+ static get observers() {
59
+ return ['_invalidChanged(invalid)', '_requiredChanged(required)'];
68
60
  }
69
61
 
70
- static get observers() {
71
- return ['_updateErrorMessage(invalid, errorMessage)', '_invalidChanged(invalid)', '_requiredChanged(required)'];
62
+ /** @protected */
63
+ get _errorId() {
64
+ return this._errorController.errorId;
72
65
  }
73
66
 
74
67
  /**
@@ -76,7 +69,7 @@ export const FieldMixin = (superclass) =>
76
69
  * @return {HTMLElement}
77
70
  */
78
71
  get _errorNode() {
79
- return this._getDirectSlotChild('error-message');
72
+ return this._errorController.node;
80
73
  }
81
74
 
82
75
  /** @protected */
@@ -95,15 +88,13 @@ export const FieldMixin = (superclass) =>
95
88
  constructor() {
96
89
  super();
97
90
 
98
- // Ensure every instance has unique ID
99
- const uniqueId = (FieldMixinClass._uniqueFieldId = 1 + FieldMixinClass._uniqueFieldId || 0);
100
- this._errorId = `error-${this.localName}-${uniqueId}`;
101
-
102
91
  this._fieldAriaController = new FieldAriaController(this);
103
92
  this._helperController = new HelperController(this);
93
+ this._errorController = new ErrorController(this);
104
94
 
105
95
  this.addController(this._fieldAriaController);
106
96
  this.addController(this._helperController);
97
+ this.addController(this._errorController);
107
98
 
108
99
  this._labelController.addEventListener('label-changed', (event) => {
109
100
  const { hasLabel, node } = event.detail;
@@ -116,29 +107,6 @@ export const FieldMixin = (superclass) =>
116
107
  });
117
108
  }
118
109
 
119
- /** @protected */
120
- ready() {
121
- super.ready();
122
-
123
- const error = this._errorNode;
124
- if (error) {
125
- error.id = this._errorId;
126
-
127
- this.__applyCustomError();
128
-
129
- this._updateErrorMessage(this.invalid, this.errorMessage);
130
- }
131
- }
132
-
133
- /** @private */
134
- __applyCustomError() {
135
- const error = this.__errorMessage;
136
- if (error && error !== this.errorMessage) {
137
- this.errorMessage = error;
138
- delete this.__errorMessage;
139
- }
140
- }
141
-
142
110
  /** @private */
143
111
  __helperChanged(hasHelper, helperNode) {
144
112
  if (hasHelper) {
@@ -160,32 +128,11 @@ export const FieldMixin = (superclass) =>
160
128
  }
161
129
 
162
130
  /**
163
- * @param {boolean} invalid
164
131
  * @param {string | null | undefined} errorMessage
165
132
  * @protected
166
133
  */
167
- _updateErrorMessage(invalid, errorMessage) {
168
- const error = this._errorNode;
169
- if (!error) {
170
- return;
171
- }
172
-
173
- // save the custom error message content
174
- if (error.textContent && !errorMessage) {
175
- this.__errorMessage = error.textContent.trim();
176
- }
177
- const hasError = Boolean(invalid && errorMessage);
178
- error.textContent = hasError ? errorMessage : '';
179
- error.hidden = !hasError;
180
- this.toggleAttribute('has-error-message', hasError);
181
-
182
- // Role alert will make the error message announce immediately
183
- // as the field becomes invalid
184
- if (hasError) {
185
- error.setAttribute('role', 'alert');
186
- } else {
187
- error.removeAttribute('role');
188
- }
134
+ _errorMessageChanged(errorMessage) {
135
+ this._errorController.setErrorMessage(errorMessage);
189
136
  }
190
137
 
191
138
  /**
@@ -219,6 +166,8 @@ export const FieldMixin = (superclass) =>
219
166
  * @protected
220
167
  */
221
168
  _invalidChanged(invalid) {
169
+ this._errorController.setInvalid(invalid);
170
+
222
171
  // This timeout is needed to prevent NVDA from announcing the error message twice:
223
172
  // 1. Once adding the `[role=alert]` attribute by the `_updateErrorMessage` method (OK).
224
173
  // 2. Once linking the error ID with the ARIA target here (unwanted).
@@ -227,7 +176,7 @@ export const FieldMixin = (superclass) =>
227
176
  // Error message ID needs to be dynamically added / removed based on the validity
228
177
  // Otherwise assistive technologies would announce the error, even if we hide it.
229
178
  if (invalid) {
230
- this._fieldAriaController.setErrorId(this._errorId);
179
+ this._fieldAriaController.setErrorId(this._errorController.errorId);
231
180
  } else {
232
181
  this._fieldAriaController.setErrorId(null);
233
182
  }
@@ -107,9 +107,7 @@ export const InputControlMixin = (superclass) =>
107
107
  _onClearButtonClick(event) {
108
108
  event.preventDefault();
109
109
  this.inputElement.focus();
110
- this.clear();
111
- this.inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
112
- this.inputElement.dispatchEvent(new Event('change', { bubbles: true }));
110
+ this.__clear();
113
111
  }
114
112
 
115
113
  /**
@@ -137,10 +135,8 @@ export const InputControlMixin = (superclass) =>
137
135
  _onKeyDown(event) {
138
136
  super._onKeyDown(event);
139
137
 
140
- if (event.key === 'Escape' && this.clearButtonVisible) {
141
- const dispatchChange = !!this.value;
142
- this.clear();
143
- dispatchChange && this.inputElement.dispatchEvent(new Event('change', { bubbles: true }));
138
+ if (event.key === 'Escape' && this.clearButtonVisible && !!this.value) {
139
+ this.__clear();
144
140
  }
145
141
  }
146
142
 
@@ -167,4 +163,24 @@ export const InputControlMixin = (superclass) =>
167
163
  })
168
164
  );
169
165
  }
166
+
167
+ /** @private */
168
+ __clear() {
169
+ this.clear();
170
+ this.inputElement.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
171
+ this.inputElement.dispatchEvent(new Event('change', { bubbles: true }));
172
+ }
173
+
174
+ /**
175
+ * Fired when the user commits a value change.
176
+ *
177
+ * @event change
178
+ */
179
+
180
+ /**
181
+ * Fired when the value is changed by the user: on every typing keystroke,
182
+ * and the value is cleared using the clear button.
183
+ *
184
+ * @event input
185
+ */
170
186
  };
@@ -120,6 +120,21 @@ export const InputFieldMixin = (superclass) =>
120
120
  this.validate();
121
121
  }
122
122
 
123
+ /**
124
+ * Override an event listener from `InputMixin`
125
+ * to mark as valid after user started typing.
126
+ * @param {Event} event
127
+ * @protected
128
+ * @override
129
+ */
130
+ _onInput(event) {
131
+ super._onInput(event);
132
+
133
+ if (this.invalid) {
134
+ this.validate();
135
+ }
136
+ }
137
+
123
138
  /**
124
139
  * Override a method from `InputMixin` to validate the field
125
140
  * when a new value is set programmatically.
@@ -41,6 +41,8 @@ export class LabelController extends SlotController {
41
41
  * @override
42
42
  */
43
43
  initCustomNode(labelNode) {
44
+ this.__updateLabelId(labelNode);
45
+
44
46
  const hasLabel = this.__hasLabel(labelNode);
45
47
  this.__toggleHasLabel(hasLabel);
46
48
  }
@@ -4,7 +4,6 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { KeyboardMixin } from '@vaadin/component-base/src/keyboard-mixin.js';
7
- import { TabindexMixin } from '@vaadin/component-base/src/tabindex-mixin.js';
8
7
  import { DelegateFocusMixin } from './delegate-focus-mixin.js';
9
8
 
10
9
  /**
@@ -13,10 +12,23 @@ import { DelegateFocusMixin } from './delegate-focus-mixin.js';
13
12
  * @polymerMixin
14
13
  * @mixes DelegateFocusMixin
15
14
  * @mixes KeyboardMixin
16
- * @mixes TabindexMixin
17
15
  */
18
16
  export const ShadowFocusMixin = (superClass) =>
19
- class ShadowFocusMixinClass extends TabindexMixin(DelegateFocusMixin(KeyboardMixin(superClass))) {
17
+ class ShadowFocusMixinClass extends DelegateFocusMixin(KeyboardMixin(superClass)) {
18
+ static get properties() {
19
+ return {
20
+ /**
21
+ * Indicates whether the element can be focused and where it participates in sequential keyboard navigation.
22
+ *
23
+ * @protected
24
+ */
25
+ tabindex: {
26
+ type: Number,
27
+ value: 0
28
+ }
29
+ };
30
+ }
31
+
20
32
  /**
21
33
  * Override an event listener from `KeyboardMixin`
22
34
  * to prevent setting `focused` on Shift Tab.
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2022 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 SlotTargetController implements ReactiveController {
9
+ constructor(
10
+ sourceSlot: HTMLSlotElement,
11
+ targetFactory: () => HTMLElement,
12
+ copyCallback?: (nodes: HTMLElement[]) => void
13
+ );
14
+
15
+ hostConnected(): void;
16
+
17
+ /**
18
+ * The source `<slot>` element to copy nodes from.
19
+ */
20
+ protected sourceSlot: HTMLSlotElement;
21
+
22
+ /**
23
+ * Function used to get a reference to slot target.
24
+ */
25
+ protected targetFactory: () => HTMLElement;
26
+
27
+ /**
28
+ * Function called after copying nodes to target.
29
+ */
30
+ protected copyCallback?: (nodes: HTMLElement[]) => void;
31
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2022 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 to copy the content from a source slot to a target element.
9
+ */
10
+ export class SlotTargetController {
11
+ constructor(sourceSlot, targetFactory, callback) {
12
+ /**
13
+ * The source `<slot>` element to copy nodes from.
14
+ */
15
+ this.sourceSlot = sourceSlot;
16
+
17
+ /**
18
+ * Function used to get a reference to slot target.
19
+ */
20
+ this.targetFactory = targetFactory;
21
+
22
+ /**
23
+ * Function called after copying nodes to target.
24
+ */
25
+ this.copyCallback = callback;
26
+
27
+ if (sourceSlot) {
28
+ sourceSlot.addEventListener('slotchange', () => {
29
+ // Copy in progress, ignore this event.
30
+ if (this.__copying) {
31
+ this.__copying = false;
32
+ } else {
33
+ this.__checkAndCopyNodesToSlotTarget();
34
+ }
35
+ });
36
+ }
37
+ }
38
+
39
+ hostConnected() {
40
+ this.__sourceSlotObserver = new MutationObserver(() => this.__checkAndCopyNodesToSlotTarget());
41
+
42
+ // Ensure the content is up to date when host is connected
43
+ // to handle e.g. mutating text content while disconnected.
44
+ this.__checkAndCopyNodesToSlotTarget();
45
+ }
46
+
47
+ /**
48
+ * Copies every node from the source slot to the target element
49
+ * once the source slot' content is changed.
50
+ *
51
+ * @private
52
+ */
53
+ __checkAndCopyNodesToSlotTarget() {
54
+ this.__sourceSlotObserver.disconnect();
55
+
56
+ // Ensure slot target element is up to date.
57
+ const slotTarget = this.targetFactory();
58
+
59
+ if (!slotTarget) {
60
+ return;
61
+ }
62
+
63
+ // Remove any existing clones from the slot target
64
+ if (this.__slotTargetClones) {
65
+ this.__slotTargetClones.forEach((node) => {
66
+ if (node.parentElement === slotTarget) {
67
+ slotTarget.removeChild(node);
68
+ }
69
+ });
70
+ delete this.__slotTargetClones;
71
+ }
72
+
73
+ // Exclude whitespace text nodes
74
+ const nodes = this.sourceSlot
75
+ .assignedNodes({ flatten: true })
76
+ .filter((node) => !(node.nodeType == Node.TEXT_NODE && node.textContent.trim() === ''));
77
+
78
+ if (nodes.length > 0) {
79
+ slotTarget.innerHTML = '';
80
+
81
+ // Ignore next slotchange
82
+ this.__copying = true;
83
+
84
+ this.__copyNodesToSlotTarget(nodes, slotTarget);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Copies the nodes to the target element.
90
+ *
91
+ * @param {!Array<!Node>} nodes
92
+ * @param {HTMLElement} slotTarget
93
+ * @private
94
+ */
95
+ __copyNodesToSlotTarget(nodes, slotTarget) {
96
+ this.__slotTargetClones = this.__slotTargetClones || [];
97
+
98
+ nodes.forEach((node) => {
99
+ // Clone the nodes and append the clones to the target
100
+ const clone = node.cloneNode(true);
101
+ this.__slotTargetClones.push(clone);
102
+
103
+ slotTarget.appendChild(clone);
104
+
105
+ // Observe all changes to the source node to have the clones updated
106
+ this.__sourceSlotObserver.observe(node, {
107
+ attributes: true,
108
+ childList: true,
109
+ subtree: true,
110
+ characterData: true
111
+ });
112
+ });
113
+
114
+ // Run callback e.g. to show a deprecation warning
115
+ if (typeof this.copyCallback === 'function') {
116
+ this.copyCallback(nodes);
117
+ }
118
+ }
119
+ }
@@ -1,15 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { Constructor } from '@open-wc/dedupe-mixin';
7
- import { LabelMixinClass } from './label-mixin.js';
8
- import { SlotTargetMixinClass } from './slot-target-mixin.js';
9
-
10
- /**
11
- * A mixin to forward any content from the default slot to the label node.
12
- */
13
- export declare function SlotLabelMixin<T extends Constructor<HTMLElement>>(
14
- base: T
15
- ): T & Constructor<LabelMixinClass> & Constructor<SlotTargetMixinClass>;
@@ -1,25 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
- import { LabelMixin } from './label-mixin.js';
8
- import { SlotTargetMixin } from './slot-target-mixin.js';
9
-
10
- /**
11
- * A mixin to forward any content from the default slot to the label node.
12
- *
13
- * @polymerMixin
14
- * @mixes LabelMixin
15
- * @mixes SlotTargetMixin
16
- */
17
- export const SlotLabelMixin = dedupingMixin(
18
- (superclass) =>
19
- class SlotLabelMixinClass extends SlotTargetMixin(LabelMixin(superclass)) {
20
- /** @protected */
21
- get _slotTarget() {
22
- return this._labelNode;
23
- }
24
- }
25
- );
@@ -1,25 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { Constructor } from '@open-wc/dedupe-mixin';
7
-
8
- /**
9
- * A mixin to copy the content from a source slot to a target element.
10
- */
11
- export declare function SlotTargetMixin<T extends Constructor<HTMLElement>>(
12
- base: T
13
- ): T & Constructor<SlotTargetMixinClass>;
14
-
15
- export declare class SlotTargetMixinClass {
16
- /**
17
- * A reference to the source slot from which the content is copied to the target element.
18
- */
19
- protected readonly _sourceSlot: HTMLSlotElement;
20
-
21
- /**
22
- * A reference to the target element to which the content is copied from the source slot.
23
- */
24
- protected readonly _slotTarget: HTMLElement;
25
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 - 2022 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
7
-
8
- /**
9
- * A mixin to copy the content from a source slot to a target element.
10
- *
11
- * @polymerMixin
12
- */
13
- export const SlotTargetMixin = dedupingMixin(
14
- (superclass) =>
15
- class SlotTargetMixinClass extends superclass {
16
- /** @protected */
17
- ready() {
18
- super.ready();
19
-
20
- if (this._sourceSlot) {
21
- this.__sourceSlotObserver = new MutationObserver(() => this.__checkAndCopyNodesToSlotTarget());
22
-
23
- this.__checkAndCopyNodesToSlotTarget();
24
-
25
- this._sourceSlot.addEventListener('slotchange', () => {
26
- this.__checkAndCopyNodesToSlotTarget();
27
- });
28
- }
29
- }
30
-
31
- /**
32
- * A reference to the source slot from which the nodes are copied to the target element.
33
- *
34
- * @type {HTMLSlotElement | null}
35
- * @protected
36
- */
37
- get _sourceSlot() {
38
- console.warn(`Please implement the '_sourceSlot' property in <${this.localName}>`);
39
- return null;
40
- }
41
-
42
- /**
43
- * A reference to the target element to which the nodes are copied from the source slot.
44
- *
45
- * @type {HTMLElement | null}
46
- * @protected
47
- */
48
- get _slotTarget() {
49
- console.warn(`Please implement the '_slotTarget' property in <${this.localName}>`);
50
- return null;
51
- }
52
-
53
- /**
54
- * Copies every node from the source slot to the target element
55
- * once the source slot' content is changed.
56
- *
57
- * @private
58
- */
59
- __checkAndCopyNodesToSlotTarget() {
60
- this.__sourceSlotObserver.disconnect();
61
-
62
- if (!this._slotTarget) {
63
- return;
64
- }
65
-
66
- // Remove any existing clones from the slot target
67
- if (this.__slotTargetClones) {
68
- this.__slotTargetClones.forEach((node) => {
69
- if (node.parentElement === this._slotTarget) {
70
- this._slotTarget.removeChild(node);
71
- }
72
- });
73
- delete this.__slotTargetClones;
74
- }
75
-
76
- // Exclude whitespace text nodes
77
- const nodes = this._sourceSlot
78
- .assignedNodes({ flatten: true })
79
- .filter((node) => !(node.nodeType == Node.TEXT_NODE && node.textContent.trim() === ''));
80
-
81
- if (nodes.length > 0) {
82
- this._slotTarget.innerHTML = '';
83
- this.__copyNodesToSlotTarget(nodes);
84
- }
85
- }
86
-
87
- /**
88
- * Copies the nodes to the target element.
89
- *
90
- * @protected
91
- * @param {!Array<!Node>} nodes
92
- */
93
- __copyNodesToSlotTarget(nodes) {
94
- this.__slotTargetClones = this.__slotTargetClones || [];
95
- nodes.forEach((node) => {
96
- // Clone the nodes and append the clones to the target slot
97
- const clone = node.cloneNode(true);
98
- this.__slotTargetClones.push(clone);
99
- this._slotTarget.appendChild(clone);
100
- // Observe all changes to the source node to have the clones updated
101
- this.__sourceSlotObserver.observe(node, {
102
- attributes: true,
103
- childList: true,
104
- subtree: true,
105
- characterData: true
106
- });
107
- });
108
- }
109
- }
110
- );