face-up 0.0.0 → 0.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.
Files changed (46) hide show
  1. package/.gitmodules +3 -0
  2. package/.kiro/steering/project-context.md +17 -0
  3. package/.vscode/settings.json +3 -0
  4. package/FaceUp.js +305 -0
  5. package/README.md +162 -2
  6. package/imports.html +8 -0
  7. package/package.json +30 -20
  8. package/tests/test1.html +115 -0
  9. package/types/.kiro/specs/conversion-template/README.md +128 -0
  10. package/types/.kiro/specs/conversion-template/design.md +360 -0
  11. package/types/.kiro/specs/conversion-template/requirements.md +191 -0
  12. package/types/.kiro/specs/conversion-template/tasks.md +174 -0
  13. package/types/.kiro/steering/coding-standards.md +17 -0
  14. package/types/.kiro/steering/conversion-guide.md +103 -0
  15. package/types/.kiro/steering/declarative-configuration.md +108 -0
  16. package/types/.kiro/steering/emc-json-serializability.md +306 -0
  17. package/types/EnhancementConversionInstructions.md +1626 -0
  18. package/types/LICENSE +21 -0
  19. package/types/NewCustomElementFeature.md +673 -0
  20. package/types/NewEnhancementInstructions.md +395 -0
  21. package/types/README.md +2 -0
  22. package/types/agrace/types.d.ts +11 -0
  23. package/types/assign-gingerly/types.d.ts +328 -0
  24. package/types/be-a-beacon/types.d.ts +17 -0
  25. package/types/be-bound/types.d.ts +61 -0
  26. package/types/be-buttoned-up/types.d.ts +19 -0
  27. package/types/be-clonable/types.d.ts +36 -0
  28. package/types/be-committed/types.d.ts +22 -0
  29. package/types/be-decked-with/types.d.ts +26 -0
  30. package/types/be-delible/types.d.ts +25 -0
  31. package/types/be-reflective/types.d.ts +80 -0
  32. package/types/be-render-neutral/types.d.ts +29 -0
  33. package/types/be-typed/types.d.ts +31 -0
  34. package/types/do-inc/types.d.ts +56 -0
  35. package/types/do-invoke/types.d.ts +38 -0
  36. package/types/do-merge/types.d.ts +28 -0
  37. package/types/do-toggle/types.d.ts +31 -0
  38. package/types/face-up/types.d.ts +104 -0
  39. package/types/global.d.ts +29 -0
  40. package/types/id-generation/types.d.ts +26 -0
  41. package/types/inferencer/types.d.ts +46 -0
  42. package/types/mount-observer/types.d.ts +363 -0
  43. package/types/nested-regex-groups/types.d.ts +101 -0
  44. package/types/roundabout/types.d.ts +255 -0
  45. package/types/time-ticker/types.d.ts +66 -0
  46. package/types/truth-sourcer/types.d.ts +46 -0
package/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "types"]
2
+ path = types
3
+ url = https://github.com/bahrus/types.git
@@ -0,0 +1,17 @@
1
+ ---
2
+ inclusion: auto
3
+ ---
4
+
5
+ # Project Context
6
+
7
+ This project uses shared type definitions and documentation from the `types` submodule.
8
+
9
+ ## Key References
10
+
11
+ #[[file:types/NewCustomElementFeature.md]]
12
+ #[[file:types/NewEnhancementInstructions.md]]
13
+ #[[file:types/EnhancementConversionInstructions.md]]
14
+
15
+ ## Coding Standards
16
+
17
+ #[[file:types/.kiro/steering/coding-standards.md]]
@@ -0,0 +1,3 @@
1
+ {
2
+ "explorer.fileNesting.enabled": true
3
+ }
package/FaceUp.js ADDED
@@ -0,0 +1,305 @@
1
+ // @ts-check
2
+ /** @import {FaceUpProps, AllProps, FeatureSpawnContext, FaceUpSharedContext, ValidationFlags} from './types/face-up/types' */
3
+
4
+ /**
5
+ * FaceUp — A Custom Element Feature that adds Form Associated behavior
6
+ * to a custom element via the ElementInternals API.
7
+ *
8
+ * This feature enables a custom element to:
9
+ * - Participate in form submission (setFormValue)
10
+ * - Participate in form validation (setValidity)
11
+ * - Respond to form lifecycle callbacks (reset, restore, disabled state)
12
+ * - Be styled with :valid/:invalid/:disabled pseudo-classes
13
+ * - Be labeled with <label> elements
14
+ *
15
+ * The host custom element MUST:
16
+ * - Call `this.attachInternals()` and pass the result via getSharedContext
17
+ *
18
+ * `static formAssociated = true` is set automatically via `static onAssigned`.
19
+ *
20
+ * @implements {FaceUpProps}
21
+ */
22
+ class FaceUp {
23
+
24
+ /**
25
+ * Called once by assignFeatures after registration.
26
+ * Sets `static formAssociated = true` on the host constructor so the
27
+ * consumer doesn't need to declare it manually.
28
+ * @param {Function} ctr - The custom element constructor
29
+ * @param {object} _featureConfig - The feature config (unused)
30
+ */
31
+ static onAssigned(ctr, _featureConfig) {
32
+ if (!ctr.formAssociated) {
33
+ ctr.formAssociated = true;
34
+ }
35
+ }
36
+ /** @type {WeakRef<HTMLElement> | undefined} */
37
+ #hostRef;
38
+
39
+ /** @type {ElementInternals | undefined} */
40
+ #internals;
41
+
42
+ /** @type {AbortController | undefined} */
43
+ #abortController;
44
+
45
+ /** @type {EventTarget | undefined} */
46
+ #hostPropagator;
47
+
48
+ /** @type {boolean} */
49
+ #hasDisconnected = false;
50
+
51
+ /** @type {string | File | FormData | null} */
52
+ #value = null;
53
+
54
+ /** @type {string | File | FormData | null} */
55
+ #state = null;
56
+
57
+ /** @type {boolean} */
58
+ #disabled = false;
59
+
60
+ /** @type {boolean} */
61
+ #required = false;
62
+
63
+ /** @type {string} */
64
+ #validationMessage = '';
65
+
66
+ /**
67
+ * @param {HTMLElement} hostElement
68
+ * @param {FeatureSpawnContext} ctx
69
+ * @param {Partial<FaceUpProps>} [initVals]
70
+ */
71
+ constructor(hostElement, ctx, initVals) {
72
+ this.#hostRef = new WeakRef(hostElement);
73
+ if (ctx.shared) {
74
+ this.#internals = ctx.shared.internals;
75
+ this.#hostPropagator = ctx.shared.hostPropagator;
76
+ }
77
+ if (initVals) {
78
+ if (initVals.value !== undefined) this.#value = initVals.value;
79
+ if (initVals.state !== undefined) this.#state = initVals.state;
80
+ if (initVals.disabled !== undefined) this.#disabled = initVals.disabled;
81
+ if (initVals.required !== undefined) this.#required = initVals.required;
82
+ if (initVals.validationMessage !== undefined) this.#validationMessage = initVals.validationMessage;
83
+ if (initVals.hostPropagator !== undefined) this.#hostPropagator = initVals.hostPropagator;
84
+ }
85
+ this.#connect();
86
+ }
87
+
88
+ // ─── Public Properties ───────────────────────────────────────────
89
+
90
+ get value() { return this.#value; }
91
+ set value(v) {
92
+ this.#value = v;
93
+ this.#syncFormValue();
94
+ this.#validateInternal();
95
+ }
96
+
97
+ get state() { return this.#state; }
98
+ set state(v) {
99
+ this.#state = v;
100
+ this.#syncFormValue();
101
+ }
102
+
103
+ get disabled() { return this.#disabled; }
104
+ set disabled(v) {
105
+ this.#disabled = !!v;
106
+ }
107
+
108
+ get required() { return this.#required; }
109
+ set required(v) {
110
+ this.#required = !!v;
111
+ this.#validateInternal();
112
+ }
113
+
114
+ get validationMessage() { return this.#validationMessage; }
115
+ set validationMessage(msg) {
116
+ this.#validationMessage = msg;
117
+ this.#validateInternal();
118
+ }
119
+
120
+ get hostPropagator() { return this.#hostPropagator ?? null; }
121
+ set hostPropagator(v) {
122
+ this.#hostPropagator = v ?? undefined;
123
+ this.#reconnectListeners();
124
+ }
125
+
126
+ // ─── Read-only Form Control Accessors ────────────────────────────
127
+
128
+ /** The form element the host is associated with */
129
+ get form() { return this.#internals?.form ?? null; }
130
+
131
+ /** The ValidityState of the control */
132
+ get validity() { return this.#internals?.validity ?? null; }
133
+
134
+ /** Whether the control will be validated */
135
+ get willValidate() { return this.#internals?.willValidate ?? false; }
136
+
137
+ // ─── Form Control Methods ────────────────────────────────────────
138
+
139
+ /** Runs constraint validation and returns true if valid */
140
+ checkValidity() {
141
+ return this.#internals?.checkValidity() ?? true;
142
+ }
143
+
144
+ /** Runs constraint validation and shows the browser validation UI if invalid */
145
+ reportValidity() {
146
+ return this.#internals?.reportValidity() ?? true;
147
+ }
148
+
149
+ /**
150
+ * Sets custom validity flags and message on the control.
151
+ * @param {ValidationFlags} flags - ValidityStateFlags object
152
+ * @param {string} [message] - Validation message (required if any flag is true)
153
+ * @param {HTMLElement} [anchor] - Element to anchor the validation popup to
154
+ */
155
+ setValidity(flags, message, anchor) {
156
+ if (!this.#internals) return;
157
+ if (message) {
158
+ this.#internals.setValidity(flags, message, anchor);
159
+ } else {
160
+ // Clear validity — no flags set
161
+ this.#internals.setValidity({});
162
+ }
163
+ }
164
+
165
+ // ─── Form Lifecycle Callbacks (forwarded via callbackForwarding) ─
166
+
167
+ /**
168
+ * Called when the host element's disabled state changes.
169
+ * @param {boolean} isDisabled
170
+ */
171
+ formDisabledCallback(isDisabled) {
172
+ this.#disabled = isDisabled;
173
+ }
174
+
175
+ /**
176
+ * Called when the form is reset.
177
+ * Resets value and state to null and clears validation.
178
+ */
179
+ formResetCallback() {
180
+ this.#value = null;
181
+ this.#state = null;
182
+ this.#validationMessage = '';
183
+ this.#syncFormValue();
184
+ if (this.#internals) {
185
+ this.#internals.setValidity({});
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Called when the browser restores form state (navigation, restart)
191
+ * or when autofill sets a value.
192
+ * @param {string | File | FormData | null} state
193
+ * @param {'restore' | 'autocomplete'} mode
194
+ */
195
+ formStateRestoreCallback(state, mode) {
196
+ if (mode === 'restore') {
197
+ this.#state = state;
198
+ this.#value = state;
199
+ this.#syncFormValue();
200
+ }
201
+ }
202
+
203
+ // ─── Lifecycle (callbackForwarding) ──────────────────────────────
204
+
205
+ connectedCallback() {
206
+ if (this.#hasDisconnected) {
207
+ this.#hasDisconnected = false;
208
+ this.#connect();
209
+ }
210
+ }
211
+
212
+ disconnectedCallback() {
213
+ this.#hasDisconnected = true;
214
+ this.#cleanup();
215
+ }
216
+
217
+ // ─── Private Methods ─────────────────────────────────────────────
218
+
219
+ #connect() {
220
+ this.#abortController = new AbortController();
221
+ const signal = this.#abortController.signal;
222
+
223
+ // Listen for value/state/required changes from the host propagator
224
+ if (this.#hostPropagator) {
225
+ this.#hostPropagator.addEventListener('value', (/** @type {CustomEvent} */ e) => {
226
+ this.#value = e.detail?.value ?? e.detail;
227
+ this.#syncFormValue();
228
+ this.#validateInternal();
229
+ }, { signal });
230
+
231
+ this.#hostPropagator.addEventListener('state', (/** @type {CustomEvent} */ e) => {
232
+ this.#state = e.detail?.value ?? e.detail;
233
+ this.#syncFormValue();
234
+ }, { signal });
235
+
236
+ this.#hostPropagator.addEventListener('required', (/** @type {CustomEvent} */ e) => {
237
+ this.#required = !!(e.detail?.value ?? e.detail);
238
+ this.#validateInternal();
239
+ }, { signal });
240
+
241
+ this.#hostPropagator.addEventListener('disabled', (/** @type {CustomEvent} */ e) => {
242
+ this.#disabled = !!(e.detail?.value ?? e.detail);
243
+ }, { signal });
244
+ }
245
+
246
+ // Sync initial value if already set
247
+ this.#syncFormValue();
248
+ this.#validateInternal();
249
+ }
250
+
251
+ #cleanup() {
252
+ if (this.#abortController) {
253
+ this.#abortController.abort();
254
+ this.#abortController = undefined;
255
+ }
256
+ }
257
+
258
+ #reconnectListeners() {
259
+ this.#cleanup();
260
+ if (!this.#hasDisconnected) {
261
+ this.#connect();
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Syncs the current value (and optional state) to ElementInternals.
267
+ */
268
+ #syncFormValue() {
269
+ if (!this.#internals) return;
270
+ if (this.#value === null) {
271
+ this.#internals.setFormValue(null);
272
+ } else if (this.#state !== null) {
273
+ this.#internals.setFormValue(this.#value, this.#state);
274
+ } else {
275
+ this.#internals.setFormValue(this.#value);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Runs internal validation based on required + custom validation message.
281
+ */
282
+ #validateInternal() {
283
+ if (!this.#internals) return;
284
+ const host = this.#hostRef?.deref();
285
+ if (!host) return;
286
+
287
+ if (this.#validationMessage) {
288
+ this.#internals.setValidity(
289
+ { customError: true },
290
+ this.#validationMessage,
291
+ host
292
+ );
293
+ } else if (this.#required && (this.#value === null || this.#value === '')) {
294
+ this.#internals.setValidity(
295
+ { valueMissing: true },
296
+ 'Please fill out this field.',
297
+ host
298
+ );
299
+ } else {
300
+ this.#internals.setValidity({});
301
+ }
302
+ }
303
+ }
304
+
305
+ export { FaceUp };
package/README.md CHANGED
@@ -1,2 +1,162 @@
1
- # face-it
2
- A Custom Element Feature that Adds Form Associated Behavior to a Custom Element
1
+ # face-up
2
+
3
+ A Custom Element Feature that adds Form Associated behavior to a custom element via the [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) API.
4
+
5
+ ## What it does
6
+
7
+ `FaceUp` enables any custom element to fully participate in HTML forms — matching the capabilities described in [More Capable Form Controls](https://web.dev/articles/more-capable-form-controls):
8
+
9
+ - **Form submission** — The control's value is automatically submitted with the form via `setFormValue()`.
10
+ - **Form validation** — The control participates in constraint validation with `:valid`/`:invalid` pseudo-classes.
11
+ - **Form reset** — The control resets to its default state when the form is reset.
12
+ - **Form state restoration** — The browser can restore the control's state after navigation or restart.
13
+ - **Disabled state** — The control responds to `disabled` attribute changes on itself or ancestor `<fieldset>`.
14
+ - **Label association** — The control can be labeled with `<label>` elements.
15
+
16
+ ## Usage
17
+
18
+ ```js
19
+ import 'assign-gingerly/assignFeatures.js';
20
+ import { FaceUp } from 'face-up/FaceUp.js';
21
+
22
+ class MyInput extends HTMLElement {
23
+ propagator = new EventTarget();
24
+ #internals;
25
+
26
+ static supportedFeatures = {
27
+ faceUp: {
28
+ fallbackSpawn: FaceUp,
29
+ callbackForwarding: [
30
+ 'connectedCallback',
31
+ 'disconnectedCallback',
32
+ 'formDisabledCallback',
33
+ 'formResetCallback',
34
+ 'formStateRestoreCallback'
35
+ ],
36
+ getSharedContext(instance) {
37
+ return {
38
+ internals: instance.#internals,
39
+ hostPropagator: instance.propagator
40
+ };
41
+ }
42
+ }
43
+ };
44
+
45
+ constructor() {
46
+ super();
47
+ this.#internals = this.attachInternals();
48
+ }
49
+ }
50
+
51
+ // assignFeatures calls FaceUp.onAssigned which sets static formAssociated = true
52
+ await customElements.assignFeatures(MyInput, {
53
+ faceUp: { spawn: FaceUp }
54
+ });
55
+
56
+ customElements.define('my-input', MyInput);
57
+ ```
58
+
59
+ Note: `static formAssociated = true` is set automatically by `FaceUp.onAssigned` — you don't need to declare it yourself.
60
+
61
+ Form lifecycle callbacks (`formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`) are forwarded automatically via `callbackForwarding` — no manual forwarding code needed in the host element.
62
+
63
+ ## Setting Values
64
+
65
+ Dispatch events on the host's `propagator` to update the form value:
66
+
67
+ ```js
68
+ // From within the custom element:
69
+ this.propagator.dispatchEvent(
70
+ new Event('value')
71
+ );
72
+ ```
73
+
74
+ Or set the value directly on the feature instance:
75
+
76
+ ```js
77
+ el.faceUp.value = 'new value';
78
+ ```
79
+
80
+ ## Validation
81
+
82
+ Set a custom validation message:
83
+
84
+ ```js
85
+ el.faceUp.validationMessage = 'This value is not allowed.';
86
+ ```
87
+
88
+ Or use the lower-level `setValidity()` API:
89
+
90
+ ```js
91
+ el.faceUp.setValidity({ rangeUnderflow: true }, 'Value must be at least 0.');
92
+ ```
93
+
94
+ Clear validation:
95
+
96
+ ```js
97
+ el.faceUp.validationMessage = '';
98
+ // or
99
+ el.faceUp.setValidity({});
100
+ ```
101
+
102
+ ## Form State Restoration
103
+
104
+ Pass a `state` parameter alongside `value` to enable proper form restoration:
105
+
106
+ ```js
107
+ el.faceUp.state = 'palette/#7fff00';
108
+ el.faceUp.value = '#7fff00';
109
+ ```
110
+
111
+ The `state` is stored internally by the browser and passed back to `formStateRestoreCallback` when the form is restored.
112
+
113
+ ## API
114
+
115
+ ### Properties
116
+
117
+ | Property | Type | Description |
118
+ |----------|------|-------------|
119
+ | `value` | `string \| File \| FormData \| null` | The submittable form value |
120
+ | `state` | `string \| File \| FormData \| null` | Internal state for form restoration |
121
+ | `disabled` | `boolean` | Whether the control is disabled |
122
+ | `required` | `boolean` | Whether the control requires a value |
123
+ | `validationMessage` | `string` | Custom validation error message |
124
+ | `form` | `HTMLFormElement \| null` | The associated form (read-only) |
125
+ | `validity` | `ValidityState \| null` | The validity state (read-only) |
126
+ | `willValidate` | `boolean` | Whether the control will be validated (read-only) |
127
+
128
+ ### Methods
129
+
130
+ | Method | Returns | Description |
131
+ |--------|---------|-------------|
132
+ | `checkValidity()` | `boolean` | Returns true if the control is valid |
133
+ | `reportValidity()` | `boolean` | Shows browser validation UI if invalid |
134
+ | `setValidity(flags, message?, anchor?)` | `void` | Sets custom validity flags |
135
+
136
+ ### Form Lifecycle Callbacks (forwarded via `callbackForwarding`)
137
+
138
+ | Callback | Description |
139
+ |----------|-------------|
140
+ | `formDisabledCallback(disabled)` | Called when disabled state changes |
141
+ | `formResetCallback()` | Called when the form is reset |
142
+ | `formStateRestoreCallback(state, mode)` | Called when browser restores form state |
143
+
144
+ These are forwarded automatically by `assign-gingerly`'s `callbackForwarding` mechanism — the host element does not need to implement them manually.
145
+
146
+ ## Requirements
147
+
148
+ The host custom element **must**:
149
+
150
+ 1. Call `this.attachInternals()` in its constructor
151
+ 2. Pass the internals via `getSharedContext` in `supportedFeatures`
152
+ 3. Include the form lifecycle callbacks in `callbackForwarding`
153
+
154
+ Both `static formAssociated = true` and form lifecycle callback forwarding are handled automatically — no manual boilerplate needed in the host element.
155
+
156
+ ## Dev
157
+
158
+ ```bash
159
+ npm install
160
+ npm run serve
161
+ # Open http://localhost:8000/tests/test1.html
162
+ ```
package/imports.html ADDED
@@ -0,0 +1,8 @@
1
+ <script type=importmap>
2
+ {
3
+ "imports": {
4
+ "assign-gingerly/": "/node_modules/assign-gingerly/",
5
+ "face-up/": "/"
6
+ }
7
+ }
8
+ </script>
package/package.json CHANGED
@@ -1,20 +1,30 @@
1
- {
2
- "name": "face-up",
3
- "version": "0.0.0",
4
- "description": "A Custom Element Feature that Adds Form Associated Behavior to a Custom Element",
5
- "homepage": "https://github.com/bahrus/face-it#readme",
6
- "bugs": {
7
- "url": "https://github.com/bahrus/face-it/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/bahrus/face-it.git"
12
- },
13
- "license": "MIT",
14
- "author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
15
- "type": "module",
16
- "main": "FaceIt.js",
17
- "scripts": {
18
- "test": "playwright test"
19
- }
20
- }
1
+ {
2
+ "name": "face-up",
3
+ "version": "0.0.2",
4
+ "description": "A Custom Element Feature that Adds Form Associated Behavior to a Custom Element",
5
+ "homepage": "https://github.com/bahrus/face-up#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/bahrus/face-up/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bahrus/face-up.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
15
+ "type": "module",
16
+ "main": "FaceUp.js",
17
+ "scripts": {
18
+ "serve": "node ./node_modules/spa-ssi/serve.js",
19
+ "test": "playwright test",
20
+ "update": "ncu -u && npm install",
21
+ "safari": "npx playwright wk http://localhost:8000",
22
+ "chrome": "npx playwright cr http://localhost:8000"
23
+ },
24
+ "devDependencies": {
25
+ "assign-gingerly": "0.0.43",
26
+ "@playwright/test": "1.60.0",
27
+ "spa-ssi": "0.0.27"
28
+ },
29
+ "dependencies": {}
30
+ }
@@ -0,0 +1,115 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Test - FaceUp Feature</title>
7
+ <!-- #include virtual="/imports.html" -->
8
+ <script type=module>
9
+ import 'assign-gingerly/assignFeatures.js';
10
+ import {FaceUp} from 'face-up/FaceUp.js';
11
+
12
+ /**
13
+ * A test custom element that uses FaceUp to become form-associated.
14
+ * Note: static formAssociated = true is set automatically by FaceUp.onAssigned
15
+ */
16
+ class TestInput extends HTMLElement {
17
+ propagator = new EventTarget();
18
+ #internals;
19
+
20
+ static supportedFeatures = {
21
+ faceUp: {
22
+ fallbackSpawn: FaceUp,
23
+ callbackForwarding: [
24
+ 'connectedCallback',
25
+ 'disconnectedCallback',
26
+ 'formDisabledCallback',
27
+ 'formResetCallback',
28
+ 'formStateRestoreCallback'
29
+ ],
30
+ getSharedContext(instance) {
31
+ return {
32
+ internals: instance.#internals,
33
+ hostPropagator: instance.propagator
34
+ };
35
+ }
36
+ }
37
+ };
38
+
39
+ constructor() {
40
+ super();
41
+ this.#internals = this.attachInternals();
42
+ this.attachShadow({ mode: 'open' });
43
+ this.shadowRoot.innerHTML = `
44
+ <style>
45
+ :host { display: inline-block; }
46
+ input { font: inherit; }
47
+ </style>
48
+ <input type="text" part="input">
49
+ `;
50
+ }
51
+
52
+ connectedCallback() {
53
+ const input = this.shadowRoot.querySelector('input');
54
+ input.addEventListener('input', () => {
55
+ this.propagator.dispatchEvent(
56
+ new CustomEvent('value', { detail: input.value })
57
+ );
58
+ });
59
+ }
60
+
61
+ // Expose name for form submission
62
+ get name() { return this.getAttribute('name'); }
63
+ get type() { return this.localName; }
64
+ }
65
+
66
+ // Inject the FaceUp feature (onAssigned sets static formAssociated = true)
67
+ await customElements.assignFeatures(TestInput, {
68
+ faceUp: { spawn: FaceUp }
69
+ });
70
+
71
+ customElements.define('test-input', TestInput);
72
+ </script>
73
+ </head>
74
+ <body>
75
+ <h1>FaceUp Feature Test</h1>
76
+
77
+ <form id="test-form">
78
+ <fieldset>
79
+ <legend>Form-Associated Custom Element</legend>
80
+
81
+ <label>
82
+ Name:
83
+ <test-input name="username"></test-input>
84
+ </label>
85
+
86
+ <br><br>
87
+
88
+ <label>
89
+ Required field:
90
+ <test-input name="required-field" required></test-input>
91
+ </label>
92
+
93
+ <br><br>
94
+
95
+ <button type="submit">Submit</button>
96
+ <button type="reset">Reset</button>
97
+ </fieldset>
98
+ </form>
99
+
100
+ <h2>Form Data Output</h2>
101
+ <pre id="output"></pre>
102
+
103
+ <script type=module>
104
+ const form = document.getElementById('test-form');
105
+ const output = document.getElementById('output');
106
+
107
+ form.addEventListener('submit', (e) => {
108
+ e.preventDefault();
109
+ const formData = new FormData(form);
110
+ const entries = [...formData.entries()];
111
+ output.textContent = JSON.stringify(Object.fromEntries(entries), null, 2);
112
+ });
113
+ </script>
114
+ </body>
115
+ </html>