@wrrdev/fenix-ui 1.1.13 → 1.1.15
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/README.md +6 -5
- package/dist/components/autocomplete/autocomplete.d.ts +11 -5
- package/dist/components/autocomplete/autocomplete.d.ts.map +1 -1
- package/dist/components/autocomplete/autocomplete.js +18 -2
- package/dist/components/autocomplete/autocomplete.js.map +1 -1
- package/dist/components/button/button.d.ts +15 -0
- package/dist/components/button/button.d.ts.map +1 -1
- package/dist/components/button/button.js +20 -1
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/checkbox/checkbox.d.ts +20 -4
- package/dist/components/checkbox/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox/checkbox.js +39 -2
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/datepicker/datepicker.d.ts +16 -2
- package/dist/components/datepicker/datepicker.d.ts.map +1 -1
- package/dist/components/datepicker/datepicker.js +46 -2
- package/dist/components/datepicker/datepicker.js.map +1 -1
- package/dist/components/editor/commands.d.ts +27 -0
- package/dist/components/editor/commands.d.ts.map +1 -0
- package/dist/components/editor/commands.js +235 -0
- package/dist/components/editor/commands.js.map +1 -0
- package/dist/components/editor/editor.d.ts +83 -0
- package/dist/components/editor/editor.d.ts.map +1 -0
- package/dist/components/editor/editor.js +1951 -0
- package/dist/components/editor/editor.js.map +1 -0
- package/dist/components/editor/index.d.ts +12 -0
- package/dist/components/editor/index.d.ts.map +1 -0
- package/dist/components/editor/index.js +7 -0
- package/dist/components/editor/index.js.map +1 -0
- package/dist/components/editor/normalize-html.d.ts +33 -0
- package/dist/components/editor/normalize-html.d.ts.map +1 -0
- package/dist/components/editor/normalize-html.js +316 -0
- package/dist/components/editor/normalize-html.js.map +1 -0
- package/dist/components/editor/sanitize-html.d.ts +37 -0
- package/dist/components/editor/sanitize-html.d.ts.map +1 -0
- package/dist/components/editor/sanitize-html.js +247 -0
- package/dist/components/editor/sanitize-html.js.map +1 -0
- package/dist/components/editor/schema.d.ts +36 -0
- package/dist/components/editor/schema.d.ts.map +1 -0
- package/dist/components/editor/schema.js +232 -0
- package/dist/components/editor/schema.js.map +1 -0
- package/dist/components/editor/selection.d.ts +22 -0
- package/dist/components/editor/selection.d.ts.map +1 -0
- package/dist/components/editor/selection.js +97 -0
- package/dist/components/editor/selection.js.map +1 -0
- package/dist/components/editor/styles.d.ts +10 -0
- package/dist/components/editor/styles.d.ts.map +1 -0
- package/dist/components/editor/styles.js +446 -0
- package/dist/components/editor/styles.js.map +1 -0
- package/dist/components/editor/table-utils.d.ts +33 -0
- package/dist/components/editor/table-utils.d.ts.map +1 -0
- package/dist/components/editor/table-utils.js +150 -0
- package/dist/components/editor/table-utils.js.map +1 -0
- package/dist/components/fileupload/fileupload.d.ts +10 -2
- package/dist/components/fileupload/fileupload.d.ts.map +1 -1
- package/dist/components/fileupload/fileupload.js +17 -2
- package/dist/components/fileupload/fileupload.js.map +1 -1
- package/dist/components/floatlabel/floatlabel.d.ts +9 -3
- package/dist/components/floatlabel/floatlabel.d.ts.map +1 -1
- package/dist/components/floatlabel/floatlabel.js +22 -7
- package/dist/components/floatlabel/floatlabel.js.map +1 -1
- package/dist/components/input/input.d.ts +18 -7
- package/dist/components/input/input.d.ts.map +1 -1
- package/dist/components/input/input.js +33 -3
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/knob/knob.d.ts +7 -3
- package/dist/components/knob/knob.d.ts.map +1 -1
- package/dist/components/knob/knob.js +4 -2
- package/dist/components/knob/knob.js.map +1 -1
- package/dist/components/multiselect/multiselect.d.ts +13 -3
- package/dist/components/multiselect/multiselect.d.ts.map +1 -1
- package/dist/components/multiselect/multiselect.js +27 -2
- package/dist/components/multiselect/multiselect.js.map +1 -1
- package/dist/components/radio/radio.d.ts +27 -4
- package/dist/components/radio/radio.d.ts.map +1 -1
- package/dist/components/radio/radio.js +67 -8
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/rating/rating.d.ts +8 -4
- package/dist/components/rating/rating.d.ts.map +1 -1
- package/dist/components/rating/rating.js +5 -2
- package/dist/components/rating/rating.js.map +1 -1
- package/dist/components/select/select.d.ts +9 -3
- package/dist/components/select/select.d.ts.map +1 -1
- package/dist/components/select/select.js +11 -2
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/slider/slider.d.ts +12 -6
- package/dist/components/slider/slider.d.ts.map +1 -1
- package/dist/components/slider/slider.js +10 -2
- package/dist/components/slider/slider.js.map +1 -1
- package/dist/components/switch/switch.d.ts +20 -4
- package/dist/components/switch/switch.d.ts.map +1 -1
- package/dist/components/switch/switch.js +40 -2
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/textarea/textarea.d.ts +12 -5
- package/dist/components/textarea/textarea.d.ts.map +1 -1
- package/dist/components/textarea/textarea.js +12 -2
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/toast/toast.d.ts +27 -15
- package/dist/components/toast/toast.d.ts.map +1 -1
- package/dist/components/toast/toast.js +205 -32
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/toggle-button-group/toggle-button-group.d.ts +13 -5
- package/dist/components/toggle-button-group/toggle-button-group.d.ts.map +1 -1
- package/dist/components/toggle-button-group/toggle-button-group.js +19 -2
- package/dist/components/toggle-button-group/toggle-button-group.js.map +1 -1
- package/dist/core/base.d.ts +7 -0
- package/dist/core/base.d.ts.map +1 -1
- package/dist/core/base.js +4 -1
- package/dist/core/base.js.map +1 -1
- package/dist/core/form-element.d.ts +81 -0
- package/dist/core/form-element.d.ts.map +1 -0
- package/dist/core/form-element.js +129 -0
- package/dist/core/form-element.js.map +1 -0
- package/dist/core/jsx.d.ts +25 -1
- package/dist/core/jsx.d.ts.map +1 -1
- package/dist/core/jsx.js.map +1 -1
- package/dist/core/presets.d.ts +8 -3
- package/dist/core/presets.d.ts.map +1 -1
- package/dist/core/presets.js +6 -17
- package/dist/core/presets.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/vue.d.ts +3 -1
- package/dist/core/vue.d.ts.map +1 -1
- package/dist/core/vue.js.map +1 -1
- package/dist/docs/app.d.ts.map +1 -1
- package/dist/docs/componentes/autocomplete.doc.d.ts.map +1 -1
- package/dist/docs/componentes/checkbox.doc.d.ts.map +1 -1
- package/dist/docs/componentes/datepicker.doc.d.ts.map +1 -1
- package/dist/docs/componentes/editor.doc.d.ts +6 -0
- package/dist/docs/componentes/editor.doc.d.ts.map +1 -0
- package/dist/docs/componentes/fileupload.doc.d.ts.map +1 -1
- package/dist/docs/componentes/index.d.ts.map +1 -1
- package/dist/docs/componentes/input.doc.d.ts.map +1 -1
- package/dist/docs/componentes/knob.doc.d.ts.map +1 -1
- package/dist/docs/componentes/multiselect.doc.d.ts.map +1 -1
- package/dist/docs/componentes/rating.doc.d.ts.map +1 -1
- package/dist/docs/componentes/select.doc.d.ts.map +1 -1
- package/dist/docs/componentes/slider.doc.d.ts.map +1 -1
- package/dist/docs/componentes/switch.doc.d.ts.map +1 -1
- package/dist/docs/componentes/textarea.doc.d.ts.map +1 -1
- package/dist/docs/componentes/toast.doc.d.ts.map +1 -1
- package/dist/docs/componentes/toggle-button-group.doc.d.ts.map +1 -1
- package/dist/docs/frameworks.d.ts +75 -0
- package/dist/docs/frameworks.d.ts.map +1 -0
- package/dist/docs/ui/code.d.ts +4 -0
- package/dist/docs/ui/code.d.ts.map +1 -1
- package/dist/fenix-ui.umd.min.js +786 -254
- package/dist/fenix-ui2.esm.js.map +1 -1
- package/dist/fenix-ui4.esm.js +4396 -172
- package/dist/fenix-ui4.esm.js.map +1 -1
- package/dist/fenix-ui5.esm.js +1 -0
- package/dist/fenix-ui5.esm.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/auto-import.d.ts.map +1 -1
- package/dist/plugins/auto-import.js +1 -0
- package/dist/plugins/auto-import.js.map +1 -1
- package/package.json +5 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from "../../core/form-element.js";
|
|
2
2
|
import { css } from "../../core/css.js";
|
|
3
3
|
import { defineElement } from "../../core/define.js";
|
|
4
|
-
const _FxRadio = class _FxRadio extends
|
|
4
|
+
const _FxRadio = class _FxRadio extends FxFormElement {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this._initialChecked = false;
|
|
8
|
+
this._initialCheckedCaptured = false;
|
|
9
|
+
}
|
|
5
10
|
static get observedAttributes() {
|
|
6
11
|
return ["checked", "disabled"];
|
|
7
12
|
}
|
|
@@ -17,6 +22,25 @@ const _FxRadio = class _FxRadio extends FxElement {
|
|
|
17
22
|
set disabled(value) {
|
|
18
23
|
this.toggleAttr("disabled", Boolean(value));
|
|
19
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Grupo = radios com o mesmo `name` NA MESMA árvore. `getRootNode()` devolve
|
|
27
|
+
* o document OU o ShadowRoot do app consumidor — grupos com o mesmo name
|
|
28
|
+
* em shadow DOMs diferentes ficam isolados (o `document` global cruzaria
|
|
29
|
+
* fronteiras indevidamente).
|
|
30
|
+
*/
|
|
31
|
+
_group() {
|
|
32
|
+
const name = this.getAttr("name");
|
|
33
|
+
if (!name) return [this];
|
|
34
|
+
const root = this.getRootNode();
|
|
35
|
+
const safeName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
36
|
+
return Array.from(
|
|
37
|
+
root.querySelectorAll(`fx-radio[name="${safeName}"]`)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
/** No FormData: só o radio MARCADO envia `value` (ou 'on'). */
|
|
41
|
+
getFormValue() {
|
|
42
|
+
return this.checked ? this.getAttr("value") || "on" : null;
|
|
43
|
+
}
|
|
20
44
|
/** Tamanho do controle. Padrão: `'md'`. */
|
|
21
45
|
get size() {
|
|
22
46
|
const s = this.getAttr("size", "md");
|
|
@@ -41,19 +65,35 @@ const _FxRadio = class _FxRadio extends FxElement {
|
|
|
41
65
|
if (!box) return;
|
|
42
66
|
box.addEventListener("click", () => this.select());
|
|
43
67
|
box.addEventListener("keydown", (e) => {
|
|
44
|
-
|
|
68
|
+
const key = e.key;
|
|
69
|
+
if (key === " " || key === "Enter") {
|
|
45
70
|
e.preventDefault();
|
|
46
71
|
this.select();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key === "ArrowDown" || key === "ArrowRight" || key === "ArrowUp" || key === "ArrowLeft") {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
this._moveFocus(key === "ArrowDown" || key === "ArrowRight" ? 1 : -1);
|
|
47
77
|
}
|
|
48
78
|
});
|
|
79
|
+
this.updateFormValue();
|
|
80
|
+
}
|
|
81
|
+
/** Move o foco/marcação para o próximo (ou anterior) radio do grupo. */
|
|
82
|
+
_moveFocus(dir) {
|
|
83
|
+
const group = this._group().filter((r) => !r.disabled);
|
|
84
|
+
if (group.length < 2) return;
|
|
85
|
+
const i = group.indexOf(this);
|
|
86
|
+
const next = group[(i + dir + group.length) % group.length];
|
|
87
|
+
next.focus();
|
|
88
|
+
if (!next.root.activeElement) {
|
|
89
|
+
next.root.querySelector(".box")?.focus();
|
|
90
|
+
}
|
|
91
|
+
if (!next.checked) next.select();
|
|
49
92
|
}
|
|
50
93
|
select() {
|
|
51
94
|
if (this.disabled || this.checked) return;
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
document.querySelectorAll(`fx-radio[name="${CSS.escape(name)}"]`).forEach((r) => {
|
|
55
|
-
r.checked = false;
|
|
56
|
-
});
|
|
95
|
+
for (const r of this._group()) {
|
|
96
|
+
if (r !== this) r.checked = false;
|
|
57
97
|
}
|
|
58
98
|
this.checked = true;
|
|
59
99
|
this.dispatchEvent(
|
|
@@ -63,6 +103,25 @@ const _FxRadio = class _FxRadio extends FxElement {
|
|
|
63
103
|
detail: { checked: true, value: this.getAttr("value") }
|
|
64
104
|
})
|
|
65
105
|
);
|
|
106
|
+
this.dispatchEvent(
|
|
107
|
+
new CustomEvent("checkedChange", {
|
|
108
|
+
bubbles: true,
|
|
109
|
+
composed: true,
|
|
110
|
+
detail: { checked: true, value: this.getAttr("value") }
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
connectedCallback() {
|
|
115
|
+
if (!this._initialCheckedCaptured) {
|
|
116
|
+
this._initialCheckedCaptured = true;
|
|
117
|
+
this._initialChecked = this.checked;
|
|
118
|
+
}
|
|
119
|
+
super.connectedCallback();
|
|
120
|
+
}
|
|
121
|
+
/** form.reset(): restaura o estado marcado inicial do grupo. */
|
|
122
|
+
formResetCallback() {
|
|
123
|
+
this.checked = this._initialChecked;
|
|
124
|
+
this.updateFormValue();
|
|
66
125
|
}
|
|
67
126
|
};
|
|
68
127
|
_FxRadio.styles = css`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio.js","sources":["../../../src/components/radio/radio.ts"],"sourcesContent":["import { FxElement } from '../../core/base';\nimport { css } from '../../core/css';\nimport { defineElement } from '../../core/define';\n\n/**\n * <fx-radio> — Botão de opção. Radios com o mesmo `name` formam um grupo\n * (seleção exclusiva gerenciada pelo próprio componente).\n *\n * Atributos: checked, disabled, value, name, size (sm|md|lg).\n * Evento: `change` (composed, detail: { checked, value }).\n */\nexport class FxRadio extends FxElement {\n static override styles = css`\n :host {\n display: inline-flex;\n vertical-align: middle;\n font-family: var(--fx-font-family);\n font-size: var(--fx-font-size);\n }\n .box {\n display: inline-flex;\n align-items: center;\n gap: var(--fx-space-sm);\n cursor: pointer;\n user-select: none;\n color: var(--fx-text-default);\n -webkit-tap-highlight-color: transparent;\n }\n .control {\n width: var(--fx-size-radio, 18px);\n height: var(--fx-size-radio, 18px);\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 2px solid var(--fx-border-hover);\n border-radius: var(--fx-radius-full);\n background: var(--fx-surface-background);\n transition:\n border-color var(--fx-motion-duration-fast) var(--fx-motion-easing),\n box-shadow var(--fx-motion-duration-fast) var(--fx-motion-easing);\n }\n .box:hover .control { border-color: var(--fx-color-primary); }\n .box:focus-visible .control {\n outline: none;\n box-shadow: var(--fx-effect-focus-ring, none);\n }\n .dot {\n width: calc(100% - 8px);\n height: calc(100% - 8px);\n border-radius: var(--fx-radius-full);\n background: var(--fx-color-primary);\n transform: scale(0);\n transition: transform var(--fx-motion-duration-fast) var(--fx-motion-easing);\n }\n :host([checked]) .control { border-color: var(--fx-color-primary); }\n :host([checked]) .dot { transform: scale(1); }\n /* Validação */\n :host([error]) .control,\n :host([invalid]) .control { border-color: var(--fx-color-danger, #dc2626); }\n :host([success]) .control,\n :host([valid]) .control { border-color: var(--fx-color-success, #16a34a); }\n .label { display: inline-flex; align-items: center; }\n :host([disabled]) .box {\n opacity: 0.55;\n cursor: not-allowed;\n pointer-events: none;\n }\n `;\n\n static override get observedAttributes(): string[] {\n return ['checked', 'disabled'];\n }\n\n get checked(): boolean {\n return this.hasAttr('checked');\n }\n set checked(value: boolean) {\n this.toggleAttr('checked', Boolean(value));\n }\n\n get disabled(): boolean {\n return this.hasAttr('disabled');\n }\n set disabled(value: boolean) {\n this.toggleAttr('disabled', Boolean(value));\n }\n\n /** Tamanho do controle. Padrão: `'md'`. */\n get size(): string {\n const s = this.getAttr('size', 'md');\n return s === 'sm' || s === 'lg' ? s : 'md';\n }\n set size(value: string) {\n this.setAttribute('size', value);\n }\n\n protected override render(): void {\n const px = this.size === 'sm' ? '14px' : this.size === 'lg' ? '22px' : '18px';\n if (!this.style.getPropertyValue('--fx-size-radio')) {\n this.style.setProperty('--fx-size-radio', px);\n }\n\n this.setTemplate(`\n <span class=\"box\" part=\"box\" role=\"radio\" tabindex=\"0\"\n aria-checked=\"${this.checked}\" aria-disabled=\"${this.disabled}\">\n <span class=\"control\" part=\"control\"><span class=\"dot\" part=\"dot\"></span></span>\n <span class=\"label\" part=\"label\"><slot></slot></span>\n </span>\n `);\n\n const box = this.root.querySelector<HTMLElement>('.box');\n if (!box) return;\n box.addEventListener('click', () => this.select());\n box.addEventListener('keydown', (e) => {\n if ((e as KeyboardEvent).key === ' ' || (e as KeyboardEvent).key === 'Enter') {\n e.preventDefault();\n this.select();\n }\n });\n }\n\n private select(): void {\n if (this.disabled || this.checked) return;\n // Desmarca os outros radios do mesmo grupo (mesmo name).\n const name = this.getAttr('name');\n if (name) {\n document.querySelectorAll(`fx-radio[name=\"${CSS.escape(name)}\"]`).forEach((r) => {\n (r as FxRadio).checked = false;\n });\n }\n this.checked = true;\n this.dispatchEvent(\n new CustomEvent('change', {\n bubbles: true,\n composed: true,\n detail: { checked: true, value: this.getAttr('value') },\n }),\n );\n }\n}\n\nexport function defineFxRadio(): typeof FxRadio {\n return defineElement('fx-radio', FxRadio);\n}\n"],"names":[],"mappings":";;;AAWO,MAAM,WAAN,MAAM,iBAAgB,UAAU;AAAA,EA2DrC,WAAoB,qBAA+B;AACjD,WAAO,CAAC,WAAW,UAAU;AAAA,EAC/B;AAAA,EAEA,IAAI,UAAmB;AACrB,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EACA,IAAI,QAAQ,OAAgB;AAC1B,SAAK,WAAW,WAAW,QAAQ,KAAK,CAAC;AAAA,EAC3C;AAAA,EAEA,IAAI,WAAoB;AACtB,WAAO,KAAK,QAAQ,UAAU;AAAA,EAChC;AAAA,EACA,IAAI,SAAS,OAAgB;AAC3B,SAAK,WAAW,YAAY,QAAQ,KAAK,CAAC;AAAA,EAC5C;AAAA;AAAA,EAGA,IAAI,OAAe;AACjB,UAAM,IAAI,KAAK,QAAQ,QAAQ,IAAI;AACnC,WAAO,MAAM,QAAQ,MAAM,OAAO,IAAI;AAAA,EACxC;AAAA,EACA,IAAI,KAAK,OAAe;AACtB,SAAK,aAAa,QAAQ,KAAK;AAAA,EACjC;AAAA,EAEmB,SAAe;AAChC,UAAM,KAAK,KAAK,SAAS,OAAO,SAAS,KAAK,SAAS,OAAO,SAAS;AACvE,QAAI,CAAC,KAAK,MAAM,iBAAiB,iBAAiB,GAAG;AACnD,WAAK,MAAM,YAAY,mBAAmB,EAAE;AAAA,IAC9C;AAEA,SAAK,YAAY;AAAA;AAAA,wBAEG,KAAK,OAAO,oBAAoB,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA,KAIhE;AAED,UAAM,MAAM,KAAK,KAAK,cAA2B,MAAM;AACvD,QAAI,CAAC,IAAK;AACV,QAAI,iBAAiB,SAAS,MAAM,KAAK,QAAQ;AACjD,QAAI,iBAAiB,WAAW,CAAC,MAAM;AACrC,UAAK,EAAoB,QAAQ,OAAQ,EAAoB,QAAQ,SAAS;AAC5E,UAAE,eAAA;AACF,aAAK,OAAA;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,SAAe;AACrB,QAAI,KAAK,YAAY,KAAK,QAAS;AAEnC,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAI,MAAM;AACR,eAAS,iBAAiB,kBAAkB,IAAI,OAAO,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM;AAC9E,UAAc,UAAU;AAAA,MAC3B,CAAC;AAAA,IACH;AACA,SAAK,UAAU;AACf,SAAK;AAAA,MACH,IAAI,YAAY,UAAU;AAAA,QACxB,SAAS;AAAA,QACT,UAAU;AAAA,QACV,QAAQ,EAAE,SAAS,MAAM,OAAO,KAAK,QAAQ,OAAO,EAAA;AAAA,MAAE,CACvD;AAAA,IAAA;AAAA,EAEL;AACF;AAhIE,SAAgB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AADpB,IAAM,UAAN;AAmIA,SAAS,gBAAgC;AAC9C,SAAO,cAAc,YAAY,OAAO;AAC1C;"}
|
|
1
|
+
{"version":3,"file":"radio.js","sources":["../../../src/components/radio/radio.ts"],"sourcesContent":["import { FxFormElement } from '../../core/form-element';\nimport { css } from '../../core/css';\nimport { defineElement } from '../../core/define';\n\n/**\n * <fx-radio> — Botão de opção. Radios com o mesmo `name` formam um grupo\n * (seleção exclusiva gerenciada pelo próprio componente, escopado à árvore\n * em que o radio vive — document OU o shadow DOM do app consumidor).\n *\n * Form-Associated Custom Element: dentro de um <form> só o radio MARCADO\n * entra no FormData (via atributo `name`, com `value` ou 'on'), igual ao\n * radio nativo. Recebe reset/disable do form.\n *\n * Atributos: checked, disabled, value, name, size (sm|md|lg).\n * Eventos: `change` e `checkedChange` (composed, detail: { checked, value }) —\n * `checkedChange` é o two-way binding do Angular [(checked)].\n */\nexport class FxRadio extends FxFormElement {\n static override styles = css`\n :host {\n display: inline-flex;\n vertical-align: middle;\n font-family: var(--fx-font-family);\n font-size: var(--fx-font-size);\n }\n .box {\n display: inline-flex;\n align-items: center;\n gap: var(--fx-space-sm);\n cursor: pointer;\n user-select: none;\n color: var(--fx-text-default);\n -webkit-tap-highlight-color: transparent;\n }\n .control {\n width: var(--fx-size-radio, 18px);\n height: var(--fx-size-radio, 18px);\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 2px solid var(--fx-border-hover);\n border-radius: var(--fx-radius-full);\n background: var(--fx-surface-background);\n transition:\n border-color var(--fx-motion-duration-fast) var(--fx-motion-easing),\n box-shadow var(--fx-motion-duration-fast) var(--fx-motion-easing);\n }\n .box:hover .control { border-color: var(--fx-color-primary); }\n .box:focus-visible .control {\n outline: none;\n box-shadow: var(--fx-effect-focus-ring, none);\n }\n .dot {\n width: calc(100% - 8px);\n height: calc(100% - 8px);\n border-radius: var(--fx-radius-full);\n background: var(--fx-color-primary);\n transform: scale(0);\n transition: transform var(--fx-motion-duration-fast) var(--fx-motion-easing);\n }\n :host([checked]) .control { border-color: var(--fx-color-primary); }\n :host([checked]) .dot { transform: scale(1); }\n /* Validação */\n :host([error]) .control,\n :host([invalid]) .control { border-color: var(--fx-color-danger, #dc2626); }\n :host([success]) .control,\n :host([valid]) .control { border-color: var(--fx-color-success, #16a34a); }\n .label { display: inline-flex; align-items: center; }\n :host([disabled]) .box {\n opacity: 0.55;\n cursor: not-allowed;\n pointer-events: none;\n }\n `;\n\n static override get observedAttributes(): string[] {\n return ['checked', 'disabled'];\n }\n\n get checked(): boolean {\n return this.hasAttr('checked');\n }\n set checked(value: boolean) {\n this.toggleAttr('checked', Boolean(value));\n }\n\n get disabled(): boolean {\n return this.hasAttr('disabled');\n }\n set disabled(value: boolean) {\n this.toggleAttr('disabled', Boolean(value));\n }\n\n /**\n * Grupo = radios com o mesmo `name` NA MESMA árvore. `getRootNode()` devolve\n * o document OU o ShadowRoot do app consumidor — grupos com o mesmo name\n * em shadow DOMs diferentes ficam isolados (o `document` global cruzaria\n * fronteiras indevidamente).\n */\n private _group(): FxRadio[] {\n const name = this.getAttr('name');\n if (!name) return [this];\n const root = this.getRootNode() as ParentNode;\n // Escape manual para seletor de atributo com aspas duplas (o global `CSS`\n // não existe em todos os ambientes — ex.: jsdom).\n const safeName = name.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n return Array.from(\n root.querySelectorAll(`fx-radio[name=\"${safeName}\"]`),\n ) as FxRadio[];\n }\n\n /** No FormData: só o radio MARCADO envia `value` (ou 'on'). */\n protected override getFormValue(): string | FormData | null {\n return this.checked ? (this.getAttr('value') || 'on') : null;\n }\n\n /** Tamanho do controle. Padrão: `'md'`. */\n get size(): string {\n const s = this.getAttr('size', 'md');\n return s === 'sm' || s === 'lg' ? s : 'md';\n }\n set size(value: string) {\n this.setAttribute('size', value);\n }\n\n protected override render(): void {\n const px = this.size === 'sm' ? '14px' : this.size === 'lg' ? '22px' : '18px';\n if (!this.style.getPropertyValue('--fx-size-radio')) {\n this.style.setProperty('--fx-size-radio', px);\n }\n\n this.setTemplate(`\n <span class=\"box\" part=\"box\" role=\"radio\" tabindex=\"0\"\n aria-checked=\"${this.checked}\" aria-disabled=\"${this.disabled}\">\n <span class=\"control\" part=\"control\"><span class=\"dot\" part=\"dot\"></span></span>\n <span class=\"label\" part=\"label\"><slot></slot></span>\n </span>\n `);\n\n const box = this.root.querySelector<HTMLElement>('.box');\n if (!box) return;\n box.addEventListener('click', () => this.select());\n box.addEventListener('keydown', (e) => {\n const key = (e as KeyboardEvent).key;\n if (key === ' ' || key === 'Enter') {\n e.preventDefault();\n this.select();\n return;\n }\n // Setas: movem o foco (e marcam) entre os radios do grupo — igual ao\n // <input type=\"radio\"> nativo (roam pelo grupo, sem sair dele).\n if (key === 'ArrowDown' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowLeft') {\n e.preventDefault();\n this._moveFocus(key === 'ArrowDown' || key === 'ArrowRight' ? 1 : -1);\n }\n });\n\n // Publica name/value no form (ElementInternals) a cada render.\n this.updateFormValue();\n }\n\n /** Move o foco/marcação para o próximo (ou anterior) radio do grupo. */\n private _moveFocus(dir: 1 | -1): void {\n const group = this._group().filter((r) => !r.disabled);\n if (group.length < 2) return;\n const i = group.indexOf(this);\n const next = group[(i + dir + group.length) % group.length]!;\n next.focus(); // delegatesFocus leva o foco ao .box interno\n // Fallback (ex.: jsdom sem delegatesFocus): foca o .box interno direto.\n if (!next.root.activeElement) {\n next.root.querySelector<HTMLElement>('.box')?.focus();\n }\n if (!next.checked) next.select();\n }\n\n private select(): void {\n if (this.disabled || this.checked) return;\n // Desmarca os outros radios do mesmo grupo (mesmo name, mesma árvore).\n for (const r of this._group()) {\n if (r !== this) r.checked = false;\n }\n this.checked = true;\n this.dispatchEvent(\n new CustomEvent('change', {\n bubbles: true,\n composed: true,\n detail: { checked: true, value: this.getAttr('value') },\n }),\n );\n // Two-way binding do Angular: [(checked)] = [checked] + (checkedChange).\n this.dispatchEvent(\n new CustomEvent('checkedChange', {\n bubbles: true,\n composed: true,\n detail: { checked: true, value: this.getAttr('value') },\n }),\n );\n }\n\n /** Estado marcado inicial (para form.reset() restaurar o grupo). */\n private _initialChecked = false;\n private _initialCheckedCaptured = false;\n\n protected override connectedCallback(): void {\n if (!this._initialCheckedCaptured) {\n this._initialCheckedCaptured = true;\n this._initialChecked = this.checked;\n }\n super.connectedCallback();\n }\n\n /** form.reset(): restaura o estado marcado inicial do grupo. */\n override formResetCallback(): void {\n this.checked = this._initialChecked;\n this.updateFormValue();\n }\n}\n\nexport function defineFxRadio(): typeof FxRadio {\n return defineElement('fx-radio', FxRadio);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from '../../core/form-element';
|
|
2
2
|
/**
|
|
3
3
|
* <fx-rating> — Evaluación por estrellas.
|
|
4
4
|
*
|
|
5
|
+
* Form-Associated Custom Element: dentro de un <form> entra en el FormData
|
|
6
|
+
* (via atributo `name`) y recibe reset/disable del form.
|
|
7
|
+
*
|
|
5
8
|
* Atributos: value (0 a max), max (número de estrellas, default 5),
|
|
6
|
-
* readonly, disabled, allow-half (permite medias estrellas),
|
|
9
|
+
* name, readonly, disabled, allow-half (permite medias estrellas),
|
|
7
10
|
* size (sm|md|lg).
|
|
8
|
-
* Eventos (composed): `change` (detail: { value })
|
|
11
|
+
* Eventos (composed): `change`, `input` e `valueChange` (detail: { value }) —
|
|
12
|
+
* `input` aciona o v-model do Vue e `valueChange` o [(value)] do Angular.
|
|
9
13
|
*/
|
|
10
|
-
export declare class FxRating extends
|
|
14
|
+
export declare class FxRating extends FxFormElement {
|
|
11
15
|
static styles: string;
|
|
12
16
|
static get observedAttributes(): string[];
|
|
13
17
|
get value(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rating.d.ts","sourceRoot":"","sources":["../../../src/components/rating/rating.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"rating.d.ts","sourceRoot":"","sources":["../../../src/components/rating/rating.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAIxD;;;;;;;;;;;GAWG;AACH,qBAAa,QAAS,SAAQ,aAAa;IACzC,OAAgB,MAAM,SAmCpB;IAEF,WAAoB,kBAAkB,IAAI,MAAM,EAAE,CAEjD;IAED,IAAI,KAAK,IAAI,MAAM,CAGlB;IACD,IAAI,KAAK,CAAC,CAAC,EAAE,MAAM,EAKlB;IAED,IAAI,GAAG,IAAI,MAAM,CAIhB;IACD,IAAI,GAAG,CAAC,CAAC,EAAE,MAAM,EAA0C;cAExC,MAAM,IAAI,IAAI;IAkBjC,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,UAAU;IAuBlB,OAAO,CAAC,MAAM;CAMf;AAED,wBAAgB,cAAc,IAAI,OAAO,QAAQ,CAEhD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from "../../core/form-element.js";
|
|
2
2
|
import { css } from "../../core/css.js";
|
|
3
3
|
import { defineElement } from "../../core/define.js";
|
|
4
|
-
const _FxRating = class _FxRating extends
|
|
4
|
+
const _FxRating = class _FxRating extends FxFormElement {
|
|
5
5
|
static get observedAttributes() {
|
|
6
6
|
return ["value", "max", "readonly", "disabled", "allow-half", "size"];
|
|
7
7
|
}
|
|
@@ -12,6 +12,7 @@ const _FxRating = class _FxRating extends FxElement {
|
|
|
12
12
|
set value(v) {
|
|
13
13
|
this.setAttribute("value", String(Math.min(this.max, Math.max(0, v))));
|
|
14
14
|
this._syncVisual();
|
|
15
|
+
this.updateFormValue();
|
|
15
16
|
}
|
|
16
17
|
get max() {
|
|
17
18
|
const n = Number(this.getAttr("max", "5"));
|
|
@@ -31,6 +32,7 @@ const _FxRating = class _FxRating extends FxElement {
|
|
|
31
32
|
`);
|
|
32
33
|
this._bindStars();
|
|
33
34
|
this._syncVisual();
|
|
35
|
+
this.updateFormValue();
|
|
34
36
|
}
|
|
35
37
|
_starNodes() {
|
|
36
38
|
return Array.from(this.root.querySelectorAll(".star"));
|
|
@@ -54,6 +56,7 @@ const _FxRating = class _FxRating extends FxElement {
|
|
|
54
56
|
let next = n;
|
|
55
57
|
if (this.hasAttr("allow-half")) next += -0.5;
|
|
56
58
|
this.value = next;
|
|
59
|
+
this.emitValueEvents();
|
|
57
60
|
this.dispatchEvent(
|
|
58
61
|
new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this.value } })
|
|
59
62
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rating.js","sources":["../../../src/components/rating/rating.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"rating.js","sources":["../../../src/components/rating/rating.ts"],"sourcesContent":["import { FxFormElement } from '../../core/form-element';\nimport { css } from '../../core/css';\nimport { defineElement } from '../../core/define';\n\n/**\n * <fx-rating> — Evaluación por estrellas.\n *\n * Form-Associated Custom Element: dentro de un <form> entra en el FormData\n * (via atributo `name`) y recibe reset/disable del form.\n *\n * Atributos: value (0 a max), max (número de estrellas, default 5),\n * name, readonly, disabled, allow-half (permite medias estrellas),\n * size (sm|md|lg).\n * Eventos (composed): `change`, `input` e `valueChange` (detail: { value }) —\n * `input` aciona o v-model do Vue e `valueChange` o [(value)] do Angular.\n */\nexport class FxRating extends FxFormElement {\n static override styles = css`\n :host {\n display: inline-flex;\n font-family: var(--fx-font-family);\n font-size: var(--fx-font-size);\n --_star: calc(var(--fx-font-size) + 8px);\n }\n :host([size='sm']) { --_star: calc(var(--fx-font-size) + 4px); }\n :host([size='lg']) { --_star: calc(var(--fx-font-size) + 14px); }\n :host([disabled]) { opacity: 0.55; }\n :host([disabled]) .stars { pointer-events: none; }\n :host([readonly]) .stars { pointer-events: none; }\n\n .stars {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n cursor: pointer;\n }\n .star {\n position: relative;\n display: inline-flex;\n width: var(--_star);\n height: var(--_star);\n font-size: var(--_star);\n line-height: 1;\n color: var(--fx-text-muted);\n cursor: pointer;\n user-select: none;\n transition: transform var(--fx-motion-duration-fast) var(--fx-motion-easing), color var(--fx-motion-duration-fast) var(--fx-motion-easing);\n }\n .star:hover { transform: scale(1.1); }\n .star[hidden] { display: none; }\n .star.filled { color: var(--fx-color-warning); }\n .star.hovered { color: var(--fx-color-warning); }\n `;\n\n static override get observedAttributes(): string[] {\n return ['value', 'max', 'readonly', 'disabled', 'allow-half', 'size'];\n }\n\n get value(): number {\n const n = Number(this.getAttr('value', '0'));\n return Number.isFinite(n) ? Math.min(this.max, Math.max(0, n)) : 0;\n }\n set value(v: number) {\n this.setAttribute('value', String(Math.min(this.max, Math.max(0, v))));\n this._syncVisual();\n // `value` é observado (re-render), mas o setter cobre o caminho sem render.\n this.updateFormValue();\n }\n\n get max(): number {\n const n = Number(this.getAttr('max', '5'));\n const m = Number.isFinite(n) ? Math.round(n) : 5;\n return Math.min(10, Math.max(1, m));\n }\n set max(v: number) { this.setAttribute('max', String(v)); }\n\n protected override render(): void {\n const stars = this.max;\n const starsHtml = Array.from({ length: stars }, (_, i) => i + 1)\n .map((n) => `<span class=\"star\" data-n=\"${n}\" part=\"star\" title=\"${n} ${this._label(n)}\">★</span>`)\n .join('');\n\n this.setTemplate(`\n <span class=\"stars\" part=\"stars\" role=\"radiogroup\">\n ${starsHtml}\n </span>\n `);\n\n this._bindStars();\n this._syncVisual();\n // Publica name/value no form (ElementInternals) a cada render.\n this.updateFormValue();\n }\n\n private _starNodes(): HTMLElement[] {\n return Array.from(this.root.querySelectorAll<HTMLElement>('.star'));\n }\n\n private _label(n: number): string {\n return n === 1 ? 'estrella' : 'estrellas';\n }\n\n private _syncVisual(): void {\n const v = this.value;\n this._starNodes().forEach((star) => {\n const n = Number(star.dataset.n);\n star.classList.toggle('filled', v >= n);\n });\n }\n\n private _bindStars(): void {\n const stars = this._starNodes();\n stars.forEach((star) => {\n const n = Number(star.dataset.n);\n star.addEventListener('click', () => {\n if (this.hasAttr('readonly') || this.hasAttr('disabled')) return;\n let next = n;\n if (this.hasAttr('allow-half')) next += -0.5; // para simplificar, valor entero\n this.value = next;\n // input (v-model do Vue) + valueChange ([(value)] do Angular).\n this.emitValueEvents();\n this.dispatchEvent(\n new CustomEvent('change', { bubbles: true, composed: true, detail: { value: this.value } }),\n );\n });\n star.addEventListener('mouseenter', () => {\n if (this.hasAttr('readonly') || this.hasAttr('disabled')) return;\n this._hover(n);\n });\n star.addEventListener('mouseleave', () => this._syncVisual());\n });\n }\n\n private _hover(n: number): void {\n this._starNodes().forEach((star) => {\n const nn = Number(star.dataset.n);\n star.classList.toggle('hovered', nn <= n);\n });\n }\n}\n\nexport function defineFxRating(): typeof FxRating {\n return defineElement('fx-rating', FxRating);\n}"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from '../../core/form-element';
|
|
2
2
|
/**
|
|
3
3
|
* <fx-select> — Campo de seleção com dropdown customizado.
|
|
4
4
|
*
|
|
5
|
+
* Form-Associated Custom Element: dentro de um <form> entra no FormData
|
|
6
|
+
* (via atributo `name`) e recebe reset/disable do form.
|
|
7
|
+
*
|
|
5
8
|
* Escreva os filhos como `<option>` nativos no light DOM; eles são
|
|
6
9
|
* espelhados automaticamente (MutationObserver):
|
|
7
10
|
*
|
|
@@ -14,9 +17,10 @@ import { FxElement } from '../../core/base';
|
|
|
14
17
|
* searchable, clearable, search-placeholder, no-results,
|
|
15
18
|
* placement (auto|top|bottom — direção do painel; `auto` inverte quando
|
|
16
19
|
* não cabe na viewport).
|
|
17
|
-
*
|
|
20
|
+
* Eventos: `change`, `input` e `valueChange` (composed, detail: { value }) —
|
|
21
|
+
* `input` aciona o v-model do Vue e `valueChange` o [(value)] do Angular.
|
|
18
22
|
*/
|
|
19
|
-
export declare class FxSelect extends
|
|
23
|
+
export declare class FxSelect extends FxFormElement {
|
|
20
24
|
static styles: string;
|
|
21
25
|
static get observedAttributes(): string[];
|
|
22
26
|
/** Espaço (px) entre o campo e o painel — espelha `--fx-select-panel-offset`. */
|
|
@@ -61,6 +65,8 @@ export declare class FxSelect extends FxElement {
|
|
|
61
65
|
/** Navegação por teclado entre as opções do listbox (WCAG 2.1.1). */
|
|
62
66
|
private _navigateOptions;
|
|
63
67
|
protected render(): void;
|
|
68
|
+
/** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */
|
|
69
|
+
formResetCallback(): void;
|
|
64
70
|
}
|
|
65
71
|
export declare function defineFxSelect(): typeof FxSelect;
|
|
66
72
|
//# sourceMappingURL=select.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/components/select/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/components/select/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAKxD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,QAAS,SAAQ,aAAa;IACzC,OAAgB,MAAM,SA+KpB;IAIF,WAAoB,kBAAkB,IAAI,MAAM,EAAE,CAIjD;IAED,iFAAiF;IACjF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAK;IAChC,kEAAkE;IAClE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAK;IACnC,sEAAsE;IACtE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAM;IAExC,OAAO,CAAC,QAAQ,CAAC,CAAmB;IACpC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,CAAa;IAE9B,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAGjB;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,EAErB;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAMtB;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAE1B;IAED,OAAO,KAAK,OAAO,GAKlB;cAEkB,iBAAiB,IAAI,IAAI;cAezB,oBAAoB,IAAI,IAAI;IAO/C,8EAA8E;IAC9E,OAAO,CAAC,MAAM;IAKd,+DAA+D;IAC/D,OAAO,CAAC,OAAO;IAQf,qEAAqE;IACrE,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,aAAa;IAOrB;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,IAAI;IAuChC,0EAA0E;IAC1E,OAAO,CAAC,MAAM,CAAC,UAAU;IAMzB,OAAO,CAAC,MAAM;IAUd,qEAAqE;IACrE,OAAO,CAAC,gBAAgB;cAiBL,MAAM,IAAI,IAAI;IA6GjC,sEAAsE;IAC7D,iBAAiB,IAAI,IAAI;CAInC;AAED,wBAAgB,cAAc,IAAI,OAAO,QAAQ,CAEhD"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from "../../core/form-element.js";
|
|
2
2
|
import { css } from "../../core/css.js";
|
|
3
3
|
import { defineElement } from "../../core/define.js";
|
|
4
4
|
import { esc } from "../../core/sanitize.js";
|
|
5
|
-
const _FxSelect = class _FxSelect extends
|
|
5
|
+
const _FxSelect = class _FxSelect extends FxFormElement {
|
|
6
6
|
// `value` fica FORA da observação: refleti-lo no change não pode
|
|
7
7
|
// re-renderizar o template e fechar o dropdown.
|
|
8
8
|
static get observedAttributes() {
|
|
@@ -21,6 +21,7 @@ const _FxSelect = class _FxSelect extends FxElement {
|
|
|
21
21
|
}
|
|
22
22
|
set value(value) {
|
|
23
23
|
this.setAttribute("value", value);
|
|
24
|
+
this.updateFormValue();
|
|
24
25
|
}
|
|
25
26
|
get disabled() {
|
|
26
27
|
return this.hasAttr("disabled");
|
|
@@ -122,6 +123,7 @@ const _FxSelect = class _FxSelect extends FxElement {
|
|
|
122
123
|
this.value = value;
|
|
123
124
|
this._fechar();
|
|
124
125
|
this.render();
|
|
126
|
+
this.emitValueEvents();
|
|
125
127
|
this.dispatchEvent(
|
|
126
128
|
new CustomEvent("change", { bubbles: true, composed: true, detail: { value } })
|
|
127
129
|
);
|
|
@@ -224,6 +226,7 @@ const _FxSelect = class _FxSelect extends FxElement {
|
|
|
224
226
|
this.value = "";
|
|
225
227
|
this._fechar();
|
|
226
228
|
this.render();
|
|
229
|
+
this.emitValueEvents();
|
|
227
230
|
this.dispatchEvent(
|
|
228
231
|
new CustomEvent("change", { bubbles: true, composed: true, detail: { value: "" } })
|
|
229
232
|
);
|
|
@@ -231,6 +234,12 @@ const _FxSelect = class _FxSelect extends FxElement {
|
|
|
231
234
|
this.root.querySelectorAll(".opt").forEach((opt) => {
|
|
232
235
|
opt.addEventListener("click", () => this.select(opt.dataset.value));
|
|
233
236
|
});
|
|
237
|
+
this.updateFormValue();
|
|
238
|
+
}
|
|
239
|
+
/** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */
|
|
240
|
+
formResetCallback() {
|
|
241
|
+
super.formResetCallback();
|
|
242
|
+
this.render();
|
|
234
243
|
}
|
|
235
244
|
};
|
|
236
245
|
_FxSelect.styles = css`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.js","sources":["../../../src/components/select/select.ts"],"sourcesContent":["import { FxElement } from '../../core/base';\nimport { css } from '../../core/css';\nimport { defineElement } from '../../core/define';\nimport { esc } from '../../core/sanitize';\n\n/**\n * <fx-select> — Campo de seleção com dropdown customizado.\n *\n * Escreva os filhos como `<option>` nativos no light DOM; eles são\n * espelhados automaticamente (MutationObserver):\n *\n * <fx-select value=\"b\" searchable clearable>\n * <option value=\"a\">Opção A</option>\n * <option value=\"b\">Opção B</option>\n * </fx-select>\n *\n * Atributos: value, size (sm|md|lg), disabled, placeholder,\n * searchable, clearable, search-placeholder, no-results,\n * placement (auto|top|bottom — direção do painel; `auto` inverte quando\n * não cabe na viewport).\n * Evento: `change` (composed, detail: { value }).\n */\nexport class FxSelect extends FxElement {\n static override styles = css`\n :host {\n display: inline-block;\n vertical-align: middle;\n font-family: var(--fx-font-family);\n font-size: var(--fx-font-size);\n position: relative;\n /* Largura no HOST (padrão: conteúdo com mín. 200px). CSS externo, classes\n e style inline no elemento definem a largura sem precisar do full. */\n width: var(--fx-select-width, max-content);\n min-width: var(--fx-select-min-width, 200px);\n /* Espaço entre o campo e o painel + altura máxima do painel. Expostos\n como custom property porque ::part() do lado do consumidor tem\n prioridade MENOR que a folha do shadow root e não vence o top. */\n --fx-select-panel-offset: 4px;\n --fx-select-panel-max-height: 260px;\n }\n .trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--fx-space-sm);\n box-sizing: border-box;\n min-height: var(--fx-size-md);\n /* Acompanha a largura definida no :host. */\n width: 100%;\n font: inherit;\n /* Altura/linha determinísticas: a caixa fica no token --fx-size-*\n (o line-height fixo impede que a herança da página estoure o min-height). */\n line-height: var(--fx-font-line-height);\n font-weight: var(--fx-font-weight);\n color: var(--fx-text-default);\n text-align: left;\n background-color: var(--fx-surface-background);\n border: 1px solid var(--fx-border-default);\n border-radius: var(--fx-radius-md);\n padding: var(--fx-space-xs) var(--fx-space-md);\n cursor: pointer;\n transition:\n border-color var(--fx-motion-duration-normal) var(--fx-motion-easing),\n box-shadow var(--fx-motion-duration-normal) var(--fx-motion-easing);\n }\n .trigger:hover { border-color: var(--fx-border-hover); }\n .trigger:focus-visible,\n :host([open]) .trigger {\n outline: none;\n border-color: var(--fx-color-primary);\n box-shadow: var(--fx-effect-focus-ring, none);\n }\n :host([size='sm']) { min-width: var(--fx-select-min-width-sm, 180px); }\n :host([size='sm']) .trigger { min-height: var(--fx-size-sm); }\n :host([size='lg']) .trigger { min-height: var(--fx-size-lg); font-size: calc(var(--fx-font-size) + 4px); }\n /* Full width: o host estica até o pai e o trigger acompanha. */\n :host([full]) { display: block; width: 100%; }\n :host([full]) { min-width: 0; }\n :host([full]) .trigger { width: 100%; min-width: 0; }\n :host([full]) .panel { width: 100%; }\n /* Validação — borda e brilho JÁ EM REPOUSO (igual ao fx-input); o\n brilho segue --fx-effect-error-ring, que acompanha effect.focus-ring. */\n :host([error]) .trigger,\n :host([invalid]) .trigger {\n border-color: var(--fx-color-danger, #dc2626);\n box-shadow: var(--fx-effect-error-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-danger, #dc2626) 18%, transparent));\n }\n :host([error]) .trigger:focus-visible,\n :host([invalid]) .trigger:focus-visible,\n :host([error][open]) .trigger,\n :host([invalid][open]) .trigger {\n border-color: var(--fx-color-danger, #dc2626);\n box-shadow: var(--fx-effect-error-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-danger, #dc2626) 18%, transparent));\n }\n :host([success]) .trigger,\n :host([valid]) .trigger {\n border-color: var(--fx-color-success, #16a34a);\n box-shadow: var(--fx-effect-success-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-success, #16a34a) 18%, transparent));\n }\n :host([success]) .trigger:focus-visible,\n :host([valid]) .trigger:focus-visible,\n :host([success][open]) .trigger,\n :host([valid][open]) .trigger {\n border-color: var(--fx-color-success, #16a34a);\n box-shadow: var(--fx-effect-success-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-success, #16a34a) 18%, transparent));\n }\n :host([disabled]) .trigger,\n .trigger[aria-disabled='true'] { opacity: 0.55; cursor: not-allowed; background-color: var(--fx-surface-surface-hover); }\n /* O rótulo encolhe (ellipsis) e os ícones NUNCA saem do campo. */\n .label,\n .placeholder {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .placeholder { color: var(--fx-text-muted); }\n .actions {\n display: inline-flex;\n align-items: center;\n gap: var(--fx-space-xs, 8px);\n flex-shrink: 0;\n }\n .caret { font-size: calc(var(--fx-font-size) - 3px); color: var(--fx-text-muted); pointer-events: none; }\n\n /* Painel do dropdown — a POSIÇÃO vertical é definida pelas regras\n [data-placement] abaixo (o JS de ajustarPosicao decide), por isso\n top/bottom não ficam aqui. */\n .panel {\n position: absolute;\n left: 0;\n z-index: var(--fx-z-dropdown, 1000);\n width: max(100%, 220px);\n max-height: var(--fx-select-panel-max-height, 260px);\n overflow-y: auto;\n display: none;\n background: var(--fx-surface-background);\n border: 1px solid var(--fx-border-default);\n border-radius: var(--fx-radius-md);\n box-shadow: var(--fx-shadow-lg);\n }\n /* Padrão: abre para baixo. */\n :host([open]:not([data-placement='top'])) .panel {\n top: calc(100% + var(--fx-select-panel-offset, 4px));\n }\n /* placement=\"top\" explícito ou auto-flip: abre para cima. */\n :host([open][placement='top']) .panel,\n :host([open][data-placement='top']) .panel {\n top: auto;\n bottom: calc(100% + var(--fx-select-panel-offset, 4px));\n }\n :host([open]) .panel { display: block; }\n .search {\n position: sticky;\n top: 0;\n width: 100%;\n box-sizing: border-box;\n font: inherit;\n color: var(--fx-text-default);\n background: var(--fx-surface-background);\n border: none;\n border-bottom: 1px solid var(--fx-border-default);\n padding: var(--fx-space-sm) var(--fx-space-md);\n outline: none;\n }\n .opt {\n display: block;\n width: 100%;\n text-align: left;\n font: inherit;\n color: var(--fx-text-default);\n background: none;\n border: none;\n padding: var(--fx-space-sm) var(--fx-space-md);\n cursor: pointer;\n }\n /* Hover e selecionado com a cor primária do tema (igual ao multiselect). */\n .opt:hover { background: color-mix(in srgb, var(--fx-color-primary) 12%, transparent); }\n .opt[aria-selected='true'] {\n background: color-mix(in srgb, var(--fx-color-primary) 18%, transparent);\n color: var(--fx-color-primary);\n font-weight: var(--fx-font-weight);\n }\n .empty { padding: var(--fx-space-sm) var(--fx-space-md); color: var(--fx-text-muted); }\n\n /* Clearable */\n .clear {\n border: none;\n background: transparent;\n color: var(--fx-text-muted);\n font-size: calc(var(--fx-font-size) + 2px);\n line-height: 1;\n cursor: pointer;\n padding: 0;\n }\n .clear:hover { color: var(--fx-color-danger); }\n .clear[hidden] { display: none; }\n `;\n\n // `value` fica FORA da observação: refleti-lo no change não pode\n // re-renderizar o template e fechar o dropdown.\n static override get observedAttributes(): string[] {\n // `invalid`/`valid` são aliases aceitos no CSS; sem observá-los o\n // setAttribute no submit não re-renderiza.\n return ['size', 'disabled', 'placeholder', 'searchable', 'clearable', 'error', 'invalid', 'success', 'valid', 'placement'];\n }\n\n /** Espaço (px) entre o campo e o painel — espelha `--fx-select-panel-offset`. */\n private static readonly GAP = 4;\n /** Margem (px) mantida entre o painel e as bordas da viewport. */\n private static readonly MARGEM = 8;\n /** Altura mínima do painel ao ser limitado pelo espaço disponível. */\n private static readonly MIN_ALTURA = 80;\n\n private observer?: MutationObserver;\n private docListener?: (e: Event) => void;\n /** Reposiciona o painel enquanto ele estiver aberto (bound em `_abrir`). */\n private onReflow?: () => void;\n\n /** Tamanho do campo. Padrão: `'md'`. */\n get size(): string {\n const s = this.getAttr('size', 'md');\n return s === 'sm' || s === 'lg' ? s : 'md';\n }\n set size(value: string) {\n this.setAttribute('size', value);\n }\n\n get value(): string {\n return this.getAttr('value');\n }\n set value(value: string) {\n this.setAttribute('value', value);\n }\n\n get disabled(): boolean {\n return this.hasAttr('disabled');\n }\n set disabled(value: boolean) {\n this.toggleAttr('disabled', Boolean(value));\n }\n\n private get options(): { value: string; label: string }[] {\n return [...this.querySelectorAll('option')].map((o) => ({\n value: o.getAttribute('value') ?? o.textContent?.trim() ?? '',\n label: o.textContent?.trim() ?? '',\n }));\n }\n\n protected override connectedCallback(): void {\n super.connectedCallback();\n // Espelha mudanças nas <option> do light DOM.\n this.observer = new MutationObserver(() => this.render());\n this.observer.observe(this, { childList: true, subtree: true, characterData: true });\n // Fecha o dropdown ao clicar fora.\n this.docListener = (e: Event) => {\n if (!this.hasAttr('open')) return;\n if (e.composedPath().includes(this)) return;\n this._fechar();\n this.render();\n };\n document.addEventListener('click', this.docListener);\n }\n\n protected override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.observer?.disconnect();\n if (this.docListener) document.removeEventListener('click', this.docListener);\n this._unbindReflow();\n }\n\n /** Abre o dropdown, posiciona o painel e passa a reposicioná-lo no reflow. */\n private _abrir(): void {\n this.setAttribute('open', '');\n this._bindReflow();\n }\n\n /** Fecha o dropdown e libera os listeners de resize/scroll. */\n private _fechar(): void {\n this.removeAttribute('open');\n // Sem isto o `data-placement` fica preso no host e a próxima abertura\n // mostraria o painel na posição invertida antes do ajuste rodar.\n this.removeAttribute('data-placement');\n this._unbindReflow();\n }\n\n /** Liga o reposicionamento (resize + scroll interno, em capture). */\n private _bindReflow(): void {\n if (this.onReflow) return;\n this.onReflow = () => {\n if (this.hasAttr('open')) this.ajustarPosicao();\n };\n window.addEventListener('resize', this.onReflow);\n window.addEventListener('scroll', this.onReflow, true);\n }\n\n private _unbindReflow(): void {\n if (!this.onReflow) return;\n window.removeEventListener('resize', this.onReflow);\n window.removeEventListener('scroll', this.onReflow, true);\n this.onReflow = undefined;\n }\n\n /**\n * Decide a posição vertical do painel medindo o espaço real da viewport.\n *\n * `placement=\"top\"`/`\"bottom\"` são respeitados literalmente; o padrão\n * (`auto`) só inverte quando o painel não cabe embaixo E couber melhor\n * acima. Também limita a altura ao espaço disponível e evita overflow\n * horizontal próximo à borda direita.\n */\n protected ajustarPosicao(): void {\n const painel = this.root.querySelector<HTMLElement>('.panel');\n const trigger = this.root.querySelector<HTMLElement>('.trigger');\n if (!painel || !trigger) return;\n\n const r = trigger.getBoundingClientRect();\n const abaixo = window.innerHeight - r.bottom; // espaço livre embaixo\n const acima = r.top; // espaço livre acima\n const desejado = painel.scrollHeight; // altura já com a busca\n const maxAltura = FxSelect._maxAltura(this);\n\n const pedido = this.getAttr('placement', 'auto');\n const paraCima =\n pedido === 'top' ||\n (pedido !== 'bottom' && abaixo < Math.min(desejado, maxAltura) && acima > abaixo);\n\n // O atributo placement do host tem precedência no CSS; data-placement\n // é o canal interno do auto-flip e usa a mesma regra de estilo.\n if (paraCima) this.setAttribute('data-placement', 'top');\n else this.removeAttribute('data-placement');\n\n // Limita a altura ao espaço real disponível (evita o painel vazar da tela).\n const disponivel = Math.max(\n FxSelect.MIN_ALTURA,\n (paraCima ? acima : abaixo) - FxSelect.GAP,\n );\n painel.style.maxHeight = `${Math.min(desejado || disponivel, disponivel, maxAltura)}px`;\n\n // Reset do deslocamento horizontal antes de remedir (o painel é recriado\n // a cada render, mas o ajuste pode rodar várias vezes com o mesmo nó).\n painel.style.left = '';\n const largura = painel.offsetWidth;\n const excesso = r.left + largura - (window.innerWidth - FxSelect.MARGEM);\n if (excesso > 0) {\n painel.style.left = `${-excesso}px`;\n painel.style.right = 'auto';\n }\n }\n\n /** Altura máxima do painel a partir de `--fx-select-panel-max-height`. */\n private static _maxAltura(host: HTMLElement): number {\n const raw = getComputedStyle(host).getPropertyValue('--fx-select-panel-max-height');\n const n = parseInt(raw, 10);\n return Number.isFinite(n) && n > 0 ? n : 260;\n }\n\n private select(value: string): void {\n this.value = value;\n this._fechar();\n this.render();\n this.dispatchEvent(\n new CustomEvent('change', { bubbles: true, composed: true, detail: { value } }),\n );\n }\n\n /** Navegação por teclado entre as opções do listbox (WCAG 2.1.1). */\n private _navigateOptions(e: KeyboardEvent): void {\n e.preventDefault();\n if (!this.hasAttr('open')) {\n this._abrir();\n this.render();\n }\n const opts = Array.from(this.root.querySelectorAll<HTMLButtonElement>('.opt'));\n if (!opts.length) return;\n const current = opts.indexOf(this.root.activeElement as HTMLButtonElement);\n let next = 0;\n if (e.key === 'ArrowDown') next = current === -1 ? 0 : Math.min(current + 1, opts.length - 1);\n else if (e.key === 'ArrowUp') next = current === -1 ? opts.length - 1 : Math.max(current - 1, 0);\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = opts.length - 1;\n opts[next]?.focus();\n }\n\n protected override render(): void {\n const prevOpen = this.hasAttr('open');\n const prevSearch = this.root.querySelector<HTMLInputElement>('.search');\n // Estado do campo de busca antes de re-renderizar (valor, foco e cursor).\n const search = prevSearch?.value ?? '';\n const searchFocused =\n prevSearch != null && this.root.activeElement === prevSearch;\n const caretPos = searchFocused ? prevSearch!.selectionStart : null;\n\n const opts = this.options;\n let current = this.getAttr('value');\n if (!current) {\n const explicit = this.querySelector('option[selected]');\n current = explicit?.getAttribute('value') ?? explicit?.textContent?.trim() ?? '';\n }\n const selectedLabel = opts.find((o) => o.value === current)?.label ?? '';\n const placeholder = this.getAttr('placeholder', 'Selecione…');\n\n const filtered = search\n ? opts.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))\n : opts;\n\n this.setTemplate(`\n <span class=\"trigger\" part=\"trigger\" role=\"button\" tabindex=\"${this.disabled ? -1 : 0}\" aria-haspopup=\"listbox\" aria-expanded=\"${prevOpen}\">\n <span class=\"${selectedLabel ? 'label' : 'placeholder'}\">${esc(selectedLabel) || esc(placeholder)}</span>\n <span class=\"actions\">\n ${this.hasAttr('clearable') && current ? '<button type=\"button\" class=\"clear\" part=\"clear\" aria-label=\"Limpar\">×</button>' : ''}\n <span class=\"caret\">▼</span>\n </span>\n </span>\n <div class=\"panel\" part=\"panel\" role=\"listbox\">\n ${this.hasAttr('searchable') ? `<input class=\"search\" part=\"search\" type=\"text\" placeholder=\"${esc(this.getAttr('search-placeholder', 'Pesquisar…'))}\">` : ''}\n ${filtered.map((o) => `\n <button type=\"button\" class=\"opt\" role=\"option\" data-value=\"${esc(o.value)}\"\n aria-selected=\"${o.value === current}\">${esc(o.label)}</button>`).join('')}\n ${filtered.length === 0 ? `<div class=\"empty\">${esc(this.getAttr('no-results', 'Nenhum resultado'))}</div>` : ''}\n </div>\n `);\n\n if (prevOpen) this.setAttribute('open', '');\n // A medição só é válida com o painel já `display: block` (open aplicado).\n if (this.hasAttr('open')) this.ajustarPosicao();\n const searchInput = this.root.querySelector<HTMLInputElement>('.search');\n if (searchInput) {\n searchInput.value = search;\n searchInput.addEventListener('input', () => this.render());\n searchInput.addEventListener('click', (e) => e.stopPropagation());\n // Re-render não pode roubar o foco nem perder a posição do cursor.\n if (searchFocused) {\n searchInput.focus();\n try { searchInput.setSelectionRange(caretPos!, caretPos!); } catch { /* type=text sempre suporta */ }\n }\n }\n\n const trigger = this.root.querySelector<HTMLElement>('.trigger');\n if (!trigger) return;\n if (this.disabled) trigger.setAttribute('aria-disabled', 'true');\n\n trigger.addEventListener('click', (e) => {\n if (this.disabled) return;\n if ((e.target as HTMLElement).closest('.clear')) return;\n if (this.hasAttr('open')) this._fechar();\n else this._abrir();\n this.render();\n this.root.querySelector<HTMLInputElement>('.search')?.focus();\n });\n\n // Teclado: Enter/Espaço abrem o dropdown; setas/Home/End/Escape navegam.\n trigger.addEventListener('keydown', (e) => {\n if (this.disabled) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n trigger.click();\n return;\n }\n if (e.key === 'Escape') {\n if (this.hasAttr('open')) {\n this._fechar();\n this.render();\n this.root.querySelector<HTMLElement>('.trigger')?.focus();\n }\n return;\n }\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {\n this._navigateOptions(e);\n }\n });\n\n // Limpar.\n this.root.querySelector('.clear')?.addEventListener('click', (e) => {\n e.stopPropagation();\n this.value = '';\n this._fechar();\n this.render();\n this.dispatchEvent(\n new CustomEvent('change', { bubbles: true, composed: true, detail: { value: '' } }),\n );\n });\n\n // Escolher opção.\n this.root.querySelectorAll<HTMLElement>('.opt').forEach((opt) => {\n opt.addEventListener('click', () => this.select(opt.dataset.value!));\n });\n }\n}\n\nexport function defineFxSelect(): typeof FxSelect {\n return defineElement('fx-select', FxSelect);\n}\n\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"select.js","sources":["../../../src/components/select/select.ts"],"sourcesContent":["import { FxFormElement } from '../../core/form-element';\nimport { css } from '../../core/css';\nimport { defineElement } from '../../core/define';\nimport { esc } from '../../core/sanitize';\n\n/**\n * <fx-select> — Campo de seleção com dropdown customizado.\n *\n * Form-Associated Custom Element: dentro de um <form> entra no FormData\n * (via atributo `name`) e recebe reset/disable do form.\n *\n * Escreva os filhos como `<option>` nativos no light DOM; eles são\n * espelhados automaticamente (MutationObserver):\n *\n * <fx-select value=\"b\" searchable clearable>\n * <option value=\"a\">Opção A</option>\n * <option value=\"b\">Opção B</option>\n * </fx-select>\n *\n * Atributos: value, size (sm|md|lg), disabled, placeholder,\n * searchable, clearable, search-placeholder, no-results,\n * placement (auto|top|bottom — direção do painel; `auto` inverte quando\n * não cabe na viewport).\n * Eventos: `change`, `input` e `valueChange` (composed, detail: { value }) —\n * `input` aciona o v-model do Vue e `valueChange` o [(value)] do Angular.\n */\nexport class FxSelect extends FxFormElement {\n static override styles = css`\n :host {\n display: inline-block;\n vertical-align: middle;\n font-family: var(--fx-font-family);\n font-size: var(--fx-font-size);\n position: relative;\n /* Largura no HOST (padrão: conteúdo com mín. 200px). CSS externo, classes\n e style inline no elemento definem a largura sem precisar do full. */\n width: var(--fx-select-width, max-content);\n min-width: var(--fx-select-min-width, 200px);\n /* Espaço entre o campo e o painel + altura máxima do painel. Expostos\n como custom property porque ::part() do lado do consumidor tem\n prioridade MENOR que a folha do shadow root e não vence o top. */\n --fx-select-panel-offset: 4px;\n --fx-select-panel-max-height: 260px;\n }\n .trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--fx-space-sm);\n box-sizing: border-box;\n min-height: var(--fx-size-md);\n /* Acompanha a largura definida no :host. */\n width: 100%;\n font: inherit;\n /* Altura/linha determinísticas: a caixa fica no token --fx-size-*\n (o line-height fixo impede que a herança da página estoure o min-height). */\n line-height: var(--fx-font-line-height);\n font-weight: var(--fx-font-weight);\n color: var(--fx-text-default);\n text-align: left;\n background-color: var(--fx-surface-background);\n border: 1px solid var(--fx-border-default);\n border-radius: var(--fx-radius-md);\n padding: var(--fx-space-xs) var(--fx-space-md);\n cursor: pointer;\n transition:\n border-color var(--fx-motion-duration-normal) var(--fx-motion-easing),\n box-shadow var(--fx-motion-duration-normal) var(--fx-motion-easing);\n }\n .trigger:hover { border-color: var(--fx-border-hover); }\n .trigger:focus-visible,\n :host([open]) .trigger {\n outline: none;\n border-color: var(--fx-color-primary);\n box-shadow: var(--fx-effect-focus-ring, none);\n }\n :host([size='sm']) { min-width: var(--fx-select-min-width-sm, 180px); }\n :host([size='sm']) .trigger { min-height: var(--fx-size-sm); }\n :host([size='lg']) .trigger { min-height: var(--fx-size-lg); font-size: calc(var(--fx-font-size) + 4px); }\n /* Full width: o host estica até o pai e o trigger acompanha. */\n :host([full]) { display: block; width: 100%; }\n :host([full]) { min-width: 0; }\n :host([full]) .trigger { width: 100%; min-width: 0; }\n :host([full]) .panel { width: 100%; }\n /* Validação — borda e brilho JÁ EM REPOUSO (igual ao fx-input); o\n brilho segue --fx-effect-error-ring, que acompanha effect.focus-ring. */\n :host([error]) .trigger,\n :host([invalid]) .trigger {\n border-color: var(--fx-color-danger, #dc2626);\n box-shadow: var(--fx-effect-error-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-danger, #dc2626) 18%, transparent));\n }\n :host([error]) .trigger:focus-visible,\n :host([invalid]) .trigger:focus-visible,\n :host([error][open]) .trigger,\n :host([invalid][open]) .trigger {\n border-color: var(--fx-color-danger, #dc2626);\n box-shadow: var(--fx-effect-error-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-danger, #dc2626) 18%, transparent));\n }\n :host([success]) .trigger,\n :host([valid]) .trigger {\n border-color: var(--fx-color-success, #16a34a);\n box-shadow: var(--fx-effect-success-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-success, #16a34a) 18%, transparent));\n }\n :host([success]) .trigger:focus-visible,\n :host([valid]) .trigger:focus-visible,\n :host([success][open]) .trigger,\n :host([valid][open]) .trigger {\n border-color: var(--fx-color-success, #16a34a);\n box-shadow: var(--fx-effect-success-ring, 0 0 0 3px color-mix(in srgb, var(--fx-color-success, #16a34a) 18%, transparent));\n }\n :host([disabled]) .trigger,\n .trigger[aria-disabled='true'] { opacity: 0.55; cursor: not-allowed; background-color: var(--fx-surface-surface-hover); }\n /* O rótulo encolhe (ellipsis) e os ícones NUNCA saem do campo. */\n .label,\n .placeholder {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .placeholder { color: var(--fx-text-muted); }\n .actions {\n display: inline-flex;\n align-items: center;\n gap: var(--fx-space-xs, 8px);\n flex-shrink: 0;\n }\n .caret { font-size: calc(var(--fx-font-size) - 3px); color: var(--fx-text-muted); pointer-events: none; }\n\n /* Painel do dropdown — a POSIÇÃO vertical é definida pelas regras\n [data-placement] abaixo (o JS de ajustarPosicao decide), por isso\n top/bottom não ficam aqui. */\n .panel {\n position: absolute;\n left: 0;\n z-index: var(--fx-z-dropdown, 1000);\n width: max(100%, 220px);\n max-height: var(--fx-select-panel-max-height, 260px);\n overflow-y: auto;\n display: none;\n background: var(--fx-surface-background);\n border: 1px solid var(--fx-border-default);\n border-radius: var(--fx-radius-md);\n box-shadow: var(--fx-shadow-lg);\n }\n /* Padrão: abre para baixo. */\n :host([open]:not([data-placement='top'])) .panel {\n top: calc(100% + var(--fx-select-panel-offset, 4px));\n }\n /* placement=\"top\" explícito ou auto-flip: abre para cima. */\n :host([open][placement='top']) .panel,\n :host([open][data-placement='top']) .panel {\n top: auto;\n bottom: calc(100% + var(--fx-select-panel-offset, 4px));\n }\n :host([open]) .panel { display: block; }\n .search {\n position: sticky;\n top: 0;\n width: 100%;\n box-sizing: border-box;\n font: inherit;\n color: var(--fx-text-default);\n background: var(--fx-surface-background);\n border: none;\n border-bottom: 1px solid var(--fx-border-default);\n padding: var(--fx-space-sm) var(--fx-space-md);\n outline: none;\n }\n .opt {\n display: block;\n width: 100%;\n text-align: left;\n font: inherit;\n color: var(--fx-text-default);\n background: none;\n border: none;\n padding: var(--fx-space-sm) var(--fx-space-md);\n cursor: pointer;\n }\n /* Hover e selecionado com a cor primária do tema (igual ao multiselect). */\n .opt:hover { background: color-mix(in srgb, var(--fx-color-primary) 12%, transparent); }\n .opt[aria-selected='true'] {\n background: color-mix(in srgb, var(--fx-color-primary) 18%, transparent);\n color: var(--fx-color-primary);\n font-weight: var(--fx-font-weight);\n }\n .empty { padding: var(--fx-space-sm) var(--fx-space-md); color: var(--fx-text-muted); }\n\n /* Clearable */\n .clear {\n border: none;\n background: transparent;\n color: var(--fx-text-muted);\n font-size: calc(var(--fx-font-size) + 2px);\n line-height: 1;\n cursor: pointer;\n padding: 0;\n }\n .clear:hover { color: var(--fx-color-danger); }\n .clear[hidden] { display: none; }\n `;\n\n // `value` fica FORA da observação: refleti-lo no change não pode\n // re-renderizar o template e fechar o dropdown.\n static override get observedAttributes(): string[] {\n // `invalid`/`valid` são aliases aceitos no CSS; sem observá-los o\n // setAttribute no submit não re-renderiza.\n return ['size', 'disabled', 'placeholder', 'searchable', 'clearable', 'error', 'invalid', 'success', 'valid', 'placement'];\n }\n\n /** Espaço (px) entre o campo e o painel — espelha `--fx-select-panel-offset`. */\n private static readonly GAP = 4;\n /** Margem (px) mantida entre o painel e as bordas da viewport. */\n private static readonly MARGEM = 8;\n /** Altura mínima do painel ao ser limitado pelo espaço disponível. */\n private static readonly MIN_ALTURA = 80;\n\n private observer?: MutationObserver;\n private docListener?: (e: Event) => void;\n /** Reposiciona o painel enquanto ele estiver aberto (bound em `_abrir`). */\n private onReflow?: () => void;\n\n /** Tamanho do campo. Padrão: `'md'`. */\n get size(): string {\n const s = this.getAttr('size', 'md');\n return s === 'sm' || s === 'lg' ? s : 'md';\n }\n set size(value: string) {\n this.setAttribute('size', value);\n }\n\n get value(): string {\n return this.getAttr('value');\n }\n set value(value: string) {\n this.setAttribute('value', value);\n // `value` não é observado (não pode re-renderizar/fechar o dropdown no\n // change): publica no form aqui; o rótulo do trigger é re-renderizado\n // pelos caminhos que já chamam render().\n this.updateFormValue();\n }\n\n get disabled(): boolean {\n return this.hasAttr('disabled');\n }\n set disabled(value: boolean) {\n this.toggleAttr('disabled', Boolean(value));\n }\n\n private get options(): { value: string; label: string }[] {\n return [...this.querySelectorAll('option')].map((o) => ({\n value: o.getAttribute('value') ?? o.textContent?.trim() ?? '',\n label: o.textContent?.trim() ?? '',\n }));\n }\n\n protected override connectedCallback(): void {\n super.connectedCallback();\n // Espelha mudanças nas <option> do light DOM.\n this.observer = new MutationObserver(() => this.render());\n this.observer.observe(this, { childList: true, subtree: true, characterData: true });\n // Fecha o dropdown ao clicar fora.\n this.docListener = (e: Event) => {\n if (!this.hasAttr('open')) return;\n if (e.composedPath().includes(this)) return;\n this._fechar();\n this.render();\n };\n document.addEventListener('click', this.docListener);\n }\n\n protected override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.observer?.disconnect();\n if (this.docListener) document.removeEventListener('click', this.docListener);\n this._unbindReflow();\n }\n\n /** Abre o dropdown, posiciona o painel e passa a reposicioná-lo no reflow. */\n private _abrir(): void {\n this.setAttribute('open', '');\n this._bindReflow();\n }\n\n /** Fecha o dropdown e libera os listeners de resize/scroll. */\n private _fechar(): void {\n this.removeAttribute('open');\n // Sem isto o `data-placement` fica preso no host e a próxima abertura\n // mostraria o painel na posição invertida antes do ajuste rodar.\n this.removeAttribute('data-placement');\n this._unbindReflow();\n }\n\n /** Liga o reposicionamento (resize + scroll interno, em capture). */\n private _bindReflow(): void {\n if (this.onReflow) return;\n this.onReflow = () => {\n if (this.hasAttr('open')) this.ajustarPosicao();\n };\n window.addEventListener('resize', this.onReflow);\n window.addEventListener('scroll', this.onReflow, true);\n }\n\n private _unbindReflow(): void {\n if (!this.onReflow) return;\n window.removeEventListener('resize', this.onReflow);\n window.removeEventListener('scroll', this.onReflow, true);\n this.onReflow = undefined;\n }\n\n /**\n * Decide a posição vertical do painel medindo o espaço real da viewport.\n *\n * `placement=\"top\"`/`\"bottom\"` são respeitados literalmente; o padrão\n * (`auto`) só inverte quando o painel não cabe embaixo E couber melhor\n * acima. Também limita a altura ao espaço disponível e evita overflow\n * horizontal próximo à borda direita.\n */\n protected ajustarPosicao(): void {\n const painel = this.root.querySelector<HTMLElement>('.panel');\n const trigger = this.root.querySelector<HTMLElement>('.trigger');\n if (!painel || !trigger) return;\n\n const r = trigger.getBoundingClientRect();\n const abaixo = window.innerHeight - r.bottom; // espaço livre embaixo\n const acima = r.top; // espaço livre acima\n const desejado = painel.scrollHeight; // altura já com a busca\n const maxAltura = FxSelect._maxAltura(this);\n\n const pedido = this.getAttr('placement', 'auto');\n const paraCima =\n pedido === 'top' ||\n (pedido !== 'bottom' && abaixo < Math.min(desejado, maxAltura) && acima > abaixo);\n\n // O atributo placement do host tem precedência no CSS; data-placement\n // é o canal interno do auto-flip e usa a mesma regra de estilo.\n if (paraCima) this.setAttribute('data-placement', 'top');\n else this.removeAttribute('data-placement');\n\n // Limita a altura ao espaço real disponível (evita o painel vazar da tela).\n const disponivel = Math.max(\n FxSelect.MIN_ALTURA,\n (paraCima ? acima : abaixo) - FxSelect.GAP,\n );\n painel.style.maxHeight = `${Math.min(desejado || disponivel, disponivel, maxAltura)}px`;\n\n // Reset do deslocamento horizontal antes de remedir (o painel é recriado\n // a cada render, mas o ajuste pode rodar várias vezes com o mesmo nó).\n painel.style.left = '';\n const largura = painel.offsetWidth;\n const excesso = r.left + largura - (window.innerWidth - FxSelect.MARGEM);\n if (excesso > 0) {\n painel.style.left = `${-excesso}px`;\n painel.style.right = 'auto';\n }\n }\n\n /** Altura máxima do painel a partir de `--fx-select-panel-max-height`. */\n private static _maxAltura(host: HTMLElement): number {\n const raw = getComputedStyle(host).getPropertyValue('--fx-select-panel-max-height');\n const n = parseInt(raw, 10);\n return Number.isFinite(n) && n > 0 ? n : 260;\n }\n\n private select(value: string): void {\n this.value = value;\n this._fechar();\n this.render();\n this.emitValueEvents();\n this.dispatchEvent(\n new CustomEvent('change', { bubbles: true, composed: true, detail: { value } }),\n );\n }\n\n /** Navegação por teclado entre as opções do listbox (WCAG 2.1.1). */\n private _navigateOptions(e: KeyboardEvent): void {\n e.preventDefault();\n if (!this.hasAttr('open')) {\n this._abrir();\n this.render();\n }\n const opts = Array.from(this.root.querySelectorAll<HTMLButtonElement>('.opt'));\n if (!opts.length) return;\n const current = opts.indexOf(this.root.activeElement as HTMLButtonElement);\n let next = 0;\n if (e.key === 'ArrowDown') next = current === -1 ? 0 : Math.min(current + 1, opts.length - 1);\n else if (e.key === 'ArrowUp') next = current === -1 ? opts.length - 1 : Math.max(current - 1, 0);\n else if (e.key === 'Home') next = 0;\n else if (e.key === 'End') next = opts.length - 1;\n opts[next]?.focus();\n }\n\n protected override render(): void {\n const prevOpen = this.hasAttr('open');\n const prevSearch = this.root.querySelector<HTMLInputElement>('.search');\n // Estado do campo de busca antes de re-renderizar (valor, foco e cursor).\n const search = prevSearch?.value ?? '';\n const searchFocused =\n prevSearch != null && this.root.activeElement === prevSearch;\n const caretPos = searchFocused ? prevSearch!.selectionStart : null;\n\n const opts = this.options;\n let current = this.getAttr('value');\n if (!current) {\n const explicit = this.querySelector('option[selected]');\n current = explicit?.getAttribute('value') ?? explicit?.textContent?.trim() ?? '';\n }\n const selectedLabel = opts.find((o) => o.value === current)?.label ?? '';\n const placeholder = this.getAttr('placeholder', 'Selecione…');\n\n const filtered = search\n ? opts.filter((o) => o.label.toLowerCase().includes(search.toLowerCase()))\n : opts;\n\n this.setTemplate(`\n <span class=\"trigger\" part=\"trigger\" role=\"button\" tabindex=\"${this.disabled ? -1 : 0}\" aria-haspopup=\"listbox\" aria-expanded=\"${prevOpen}\">\n <span class=\"${selectedLabel ? 'label' : 'placeholder'}\">${esc(selectedLabel) || esc(placeholder)}</span>\n <span class=\"actions\">\n ${this.hasAttr('clearable') && current ? '<button type=\"button\" class=\"clear\" part=\"clear\" aria-label=\"Limpar\">×</button>' : ''}\n <span class=\"caret\">▼</span>\n </span>\n </span>\n <div class=\"panel\" part=\"panel\" role=\"listbox\">\n ${this.hasAttr('searchable') ? `<input class=\"search\" part=\"search\" type=\"text\" placeholder=\"${esc(this.getAttr('search-placeholder', 'Pesquisar…'))}\">` : ''}\n ${filtered.map((o) => `\n <button type=\"button\" class=\"opt\" role=\"option\" data-value=\"${esc(o.value)}\"\n aria-selected=\"${o.value === current}\">${esc(o.label)}</button>`).join('')}\n ${filtered.length === 0 ? `<div class=\"empty\">${esc(this.getAttr('no-results', 'Nenhum resultado'))}</div>` : ''}\n </div>\n `);\n\n if (prevOpen) this.setAttribute('open', '');\n // A medição só é válida com o painel já `display: block` (open aplicado).\n if (this.hasAttr('open')) this.ajustarPosicao();\n const searchInput = this.root.querySelector<HTMLInputElement>('.search');\n if (searchInput) {\n searchInput.value = search;\n searchInput.addEventListener('input', () => this.render());\n searchInput.addEventListener('click', (e) => e.stopPropagation());\n // Re-render não pode roubar o foco nem perder a posição do cursor.\n if (searchFocused) {\n searchInput.focus();\n try { searchInput.setSelectionRange(caretPos!, caretPos!); } catch { /* type=text sempre suporta */ }\n }\n }\n\n const trigger = this.root.querySelector<HTMLElement>('.trigger');\n if (!trigger) return;\n if (this.disabled) trigger.setAttribute('aria-disabled', 'true');\n\n trigger.addEventListener('click', (e) => {\n if (this.disabled) return;\n if ((e.target as HTMLElement).closest('.clear')) return;\n if (this.hasAttr('open')) this._fechar();\n else this._abrir();\n this.render();\n this.root.querySelector<HTMLInputElement>('.search')?.focus();\n });\n\n // Teclado: Enter/Espaço abrem o dropdown; setas/Home/End/Escape navegam.\n trigger.addEventListener('keydown', (e) => {\n if (this.disabled) return;\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n trigger.click();\n return;\n }\n if (e.key === 'Escape') {\n if (this.hasAttr('open')) {\n this._fechar();\n this.render();\n this.root.querySelector<HTMLElement>('.trigger')?.focus();\n }\n return;\n }\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Home' || e.key === 'End') {\n this._navigateOptions(e);\n }\n });\n\n // Limpar.\n this.root.querySelector('.clear')?.addEventListener('click', (e) => {\n e.stopPropagation();\n this.value = '';\n this._fechar();\n this.render();\n this.emitValueEvents();\n this.dispatchEvent(\n new CustomEvent('change', { bubbles: true, composed: true, detail: { value: '' } }),\n );\n });\n\n // Escolher opção.\n this.root.querySelectorAll<HTMLElement>('.opt').forEach((opt) => {\n opt.addEventListener('click', () => this.select(opt.dataset.value!));\n });\n\n // Publica name/value no form (ElementInternals) a cada render.\n this.updateFormValue();\n }\n\n /** form.reset(): restaura o valor inicial e re-renderiza o rótulo. */\n override formResetCallback(): void {\n super.formResetCallback();\n this.render();\n }\n}\n\nexport function defineFxSelect(): typeof FxSelect {\n return defineElement('fx-select', FxSelect);\n}\n\n"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from '../../core/form-element';
|
|
2
2
|
/**
|
|
3
3
|
* <fx-slider> — Controle deslizante para seleção de valor numérico em um intervalo.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Form-Associated Custom Element: dentro de um <form> entra no FormData
|
|
6
|
+
* (via atributo `name`) e recebe reset/disable do form.
|
|
7
|
+
*
|
|
8
|
+
* Atributos: min, max, step, value, name, size (sm|md|lg), disabled,
|
|
9
|
+
* show-value, label (rótulo acima do trilho).
|
|
10
|
+
* Eventos (composed): `input` (durante o arraste) e `change` (ao soltar),
|
|
11
|
+
* ambos com `detail: { value: number }`; `change`/solta também emitem
|
|
12
|
+
* `valueChange` ([(value)] do Angular) e o `input` aciona o v-model do Vue.
|
|
9
13
|
*/
|
|
10
|
-
export declare class FxSlider extends
|
|
14
|
+
export declare class FxSlider extends FxFormElement {
|
|
11
15
|
static styles: string;
|
|
12
16
|
static get observedAttributes(): string[];
|
|
13
17
|
get min(): number;
|
|
@@ -31,6 +35,8 @@ export declare class FxSlider extends FxElement {
|
|
|
31
35
|
private _bindKeyboard;
|
|
32
36
|
/** Atualiza fill/thumb/tooltip/aria sem re-renderizar o template (evita perder o drag). */
|
|
33
37
|
private _syncVisual;
|
|
38
|
+
/** form.reset(): restaura o valor inicial e o visual sem re-render. */
|
|
39
|
+
formResetCallback(): void;
|
|
34
40
|
}
|
|
35
41
|
export declare function defineFxSlider(): typeof FxSlider;
|
|
36
42
|
//# sourceMappingURL=slider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/slider/slider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"slider.d.ts","sourceRoot":"","sources":["../../../src/components/slider/slider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAIxD;;;;;;;;;;;GAWG;AACH,qBAAa,QAAS,SAAQ,aAAa;IACzC,OAAgB,MAAM,SA8EpB;IAEF,WAAoB,kBAAkB,IAAI,MAAM,EAAE,CAEjD;IAED,IAAI,GAAG,IAAI,MAAM,CAAgC;IACjD,IAAI,GAAG,IAAI,MAAM,CAAkC;IACnD,IAAI,IAAI,IAAI,MAAM,CAAiC;IAEnD,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,IAAI,KAAK,CAAC,CAAC,EAAE,MAAM,EAOlB;IAED,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,MAAM;IAId,4EAA4E;IAC5E,OAAO,CAAC,KAAK;IAMb,6DAA6D;IAC7D,OAAO,CAAC,IAAI;cAKO,MAAM,IAAI,IAAI;IAoBjC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAC,CAA4B;IAC5C,OAAO,CAAC,KAAK,CAAC,CAA4B;cAEvB,oBAAoB,IAAI,IAAI;IAU/C,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,YAAY;IAkDpB,OAAO,CAAC,aAAa;IA8BrB,2FAA2F;IAC3F,OAAO,CAAC,WAAW;IAkBnB,uEAAuE;IAC9D,iBAAiB,IAAI,IAAI;CAInC;AAED,wBAAgB,cAAc,IAAI,OAAO,QAAQ,CAEhD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FxFormElement } from "../../core/form-element.js";
|
|
2
2
|
import { css } from "../../core/css.js";
|
|
3
3
|
import { defineElement } from "../../core/define.js";
|
|
4
|
-
const _FxSlider = class _FxSlider extends
|
|
4
|
+
const _FxSlider = class _FxSlider extends FxFormElement {
|
|
5
5
|
constructor() {
|
|
6
6
|
super(...arguments);
|
|
7
7
|
this._dragging = false;
|
|
@@ -25,6 +25,7 @@ const _FxSlider = class _FxSlider extends FxElement {
|
|
|
25
25
|
const snapped = this._snap(v);
|
|
26
26
|
this.setAttribute("value", String(snapped));
|
|
27
27
|
if (this.isConnected) this._syncVisual();
|
|
28
|
+
this.updateFormValue();
|
|
28
29
|
}
|
|
29
30
|
_num(attr, fallback) {
|
|
30
31
|
const raw = this.getAttribute(attr);
|
|
@@ -99,6 +100,7 @@ const _FxSlider = class _FxSlider extends FxElement {
|
|
|
99
100
|
window.removeEventListener("pointerup", up);
|
|
100
101
|
this.value = valueFromEvent(e);
|
|
101
102
|
this._syncVisual();
|
|
103
|
+
this.emitValueEvents();
|
|
102
104
|
this.dispatchEvent(
|
|
103
105
|
new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this.value } })
|
|
104
106
|
);
|
|
@@ -153,6 +155,7 @@ const _FxSlider = class _FxSlider extends FxElement {
|
|
|
153
155
|
ev.preventDefault();
|
|
154
156
|
this.value = next;
|
|
155
157
|
this._syncVisual();
|
|
158
|
+
this.emitValueEvents();
|
|
156
159
|
this.dispatchEvent(
|
|
157
160
|
new CustomEvent("input", { bubbles: true, composed: true, detail: { value: this.value } })
|
|
158
161
|
);
|
|
@@ -179,6 +182,11 @@ const _FxSlider = class _FxSlider extends FxElement {
|
|
|
179
182
|
const labelPct = this.root.querySelector(".label span:last-child");
|
|
180
183
|
if (labelPct) labelPct.textContent = String(this.value);
|
|
181
184
|
}
|
|
185
|
+
/** form.reset(): restaura o valor inicial e o visual sem re-render. */
|
|
186
|
+
formResetCallback() {
|
|
187
|
+
super.formResetCallback();
|
|
188
|
+
this._syncVisual();
|
|
189
|
+
}
|
|
182
190
|
};
|
|
183
191
|
_FxSlider.styles = css`
|
|
184
192
|
:host {
|