@charpente-ui/vue 2.0.0 → 2.1.0

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 CHANGED
@@ -51,25 +51,36 @@ npm run dev
51
51
  ```vue
52
52
  <script setup>
53
53
  import { ref } from 'vue';
54
- import { CInput, CButton, CLabel } from '@charpente-ui/vue';
54
+ import { CForm, CField, CLabel, CInput, CSupportingText, CButton } from '@charpente-ui/vue';
55
55
 
56
56
  const email = ref('');
57
+
58
+ function onSubmit() {
59
+ // Only called once the form passes native validation.
60
+ }
57
61
  </script>
58
62
 
59
63
  <template>
60
- <div class="form-group">
61
- <CLabel for="email-field">Email Address</CLabel>
64
+ <CForm validate @submit="onSubmit">
65
+ <CField class="form-group">
66
+ <CLabel>Email Address</CLabel>
67
+
68
+ <CInput v-model="email" type="email" placeholder="hello@world.com" required
69
+ class="my-custom-input-style"/>
62
70
 
63
- <CInput id="email-field" v-model="email" type="email" placeholder="hello@world.com"
64
- class="my-custom-input-style"/>
71
+ <CSupportingText validation>We never share your email.</CSupportingText>
72
+ </CField>
65
73
 
66
- <CButton @click="submit" class="btn-primary">
74
+ <CButton type="submit" class="btn-primary">
67
75
  Subscribe
68
76
  </CButton>
69
- </div>
77
+ </CForm>
70
78
  </template>
71
79
  ```
72
80
 
81
+ No `for`/`id` wiring, no validation library: the label, the hint and the browser-localized error messages are linked
82
+ and accessible automatically — and every class lands on the native element, ready for your CSS.
83
+
73
84
  ## Component Reference
74
85
 
75
86
  1. **Form Inputs** **(CInput, CTextarea, CSelect)**
@@ -169,9 +180,36 @@ avoid accidental form submissions. Pass `type="submit"` explicitly for submit bu
169
180
  </CField>
170
181
  ```
171
182
 
172
- An explicit `id` on the input or `for` on the label always wins over the field id. A `CField` wrapping a whole
173
- group is ignored by the items (a single id must not land on every input); wrap each item in its own `CField`
174
- instead:
183
+ `CSupportingText` renders a field's hint or error text inside a `CField`: the input automatically gets an
184
+ `aria-describedby` pointing to it, and the attribute is removed when the text unmounts (e.g. behind a `v-if`).
185
+
186
+ ```vue
187
+ <CField>
188
+ <CLabel>Email</CLabel>
189
+ <CInput v-model="email" type="email"/>
190
+ <CSupportingText v-if="error">{{ error }}</CSupportingText>
191
+ </CField>
192
+ ```
193
+
194
+ An explicit `aria-describedby` on the input always wins, and a standalone `CSupportingText` (outside a field)
195
+ simply renders its content with an id. Likewise, an explicit `id` on the input or `for` on the label always wins
196
+ over the field id.
197
+
198
+ Passing `id` to `CField` itself names that pairing instead of the wrapper `<div>` — reusing it on both would put
199
+ the same id on two different DOM elements:
200
+
201
+ ```vue
202
+ <CField id="email-field">
203
+ <CLabel>Email</CLabel>
204
+ <CInput v-model="email" type="email"/>
205
+ </CField>
206
+ ```
207
+
208
+ Renders `<label for="email-field">` and `<input id="email-field">`; the `<div>` itself gets no `id`. Use `class`
209
+ to target the wrapper.
210
+
211
+ A `CField` wrapping a whole group is ignored by the items (a single id must not land on every input); wrap each
212
+ item in its own `CField` instead:
175
213
 
176
214
  ```vue
177
215
  <CRadioGroup v-model="selected">
@@ -187,6 +225,29 @@ instead:
187
225
  </CRadioGroup>
188
226
  ```
189
227
 
228
+ 6. **Native Validation** _(CForm + CField + CSupportingText)_
229
+
230
+ Browsers already validate forms (`required`, `type="email"`, `minlength`, `pattern`…) and localize their error
231
+ messages for free. Charpente UI exposes that instead of reinventing it — opt in with the `validate` prop:
232
+
233
+ ```vue
234
+ <CForm validate @submit="onSubmit">
235
+ <CField>
236
+ <CLabel>Email</CLabel>
237
+ <CInput v-model="email" type="email" required/>
238
+ <CSupportingText validation>We never share your email.</CSupportingText>
239
+ </CField>
240
+ </CForm>
241
+ ```
242
+
243
+ - `CForm validate` suppresses the native bubbles (`novalidate`), blocks `submit` until the form is valid, and
244
+ focuses the first invalid control.
245
+ - Errors appear after the first submit attempt, then update live as the user fixes the value.
246
+ - `CSupportingText validation` shows the browser's localized `validationMessage` while invalid, and falls back to
247
+ its slot content otherwise. The control also gets `aria-invalid` automatically.
248
+ - Without `validate`, nothing changes — bring your own validation library if you need cross-field or async rules.
249
+ Native escapes still work: `formnovalidate` on a submit button skips validation for that button.
250
+
190
251
  ## Components
191
252
 
192
253
  | Name | Core Logic | Tag | Status |
@@ -196,12 +257,13 @@ instead:
196
257
  | CheckboxGroup | **Group:** Shared v-model and name across checkboxes inside a fieldset. | `CCheckboxGroup` | Ready |
197
258
  | Field | **Wrapper:** Auto-links a label and an input via a shared generated id. | `CField` | Ready |
198
259
  | File | **File Input:** Reactive file selection with `v-model` support. | `CFile` | Ready |
199
- | Form | **Auto-Submit:** Integrated `preventDefault` and event handling. | `CForm` | Ready |
260
+ | Form | **Auto-Submit:** `preventDefault` handling and opt-in native validation. | `CForm` | Ready |
200
261
  | Input | **Auto-ID:** Auto-links to labels via `useId()` and full attributes inheritance. | `CInput` | Ready |
201
262
  | Label | **Context-Aware:** Simple, accessible binding for any input. | `CLabel` | Ready |
202
263
  | Radio | **Selection:** Minimalist wrapper for native radio input. | `CRadio` | Ready |
203
264
  | RadioGroup | **Group:** Shared v-model and name across radios inside a fieldset. | `CRadioGroup` | Ready |
204
265
  | Select | **Native Wrapper:** Single and multiple selection support. | `CSelect` | Ready |
266
+ | SupportingText | **Field Text:** Hint or error text wired to its input via `aria-describedby`. | `CSupportingText` | Ready |
205
267
  | Textarea | **Flexible Binding:** Auto-ID and reactive model management. | `CTextarea` | Ready |
206
268
 
207
269
  ## Wrapping Components
@@ -211,7 +273,7 @@ When wrapping a Charpente UI component inside your own, you must forward `$attrs
211
273
 
212
274
  ```vue
213
275
  <script setup>
214
- import { CSelect } from '@charpente-ui/vue';
276
+ import { CField, CLabel, CSelect, CSupportingText } from '@charpente-ui/vue';
215
277
 
216
278
  defineOptions({
217
279
  inheritAttrs: false
@@ -226,20 +288,21 @@ const model = defineModel();
226
288
  </script>
227
289
 
228
290
  <template>
229
- <div>
230
- <label>{{ label }}</label>
291
+ <CField>
292
+ <CLabel>{{ label }}</CLabel>
231
293
 
232
294
  <CSelect v-bind="$attrs" v-model="model">
233
295
  <slot/>
234
296
  </CSelect>
235
297
 
236
- <span v-if="error">{{ error }}</span>
237
- </div>
298
+ <CSupportingText v-if="error">{{ error }}</CSupportingText>
299
+ </CField>
238
300
  </template>
239
301
  ```
240
302
 
241
303
  **Why this matters:** Without `inheritAttrs: false`, Vue applies fallthrough attributes to the wrapper's root element
242
- (`<div>`) instead of the inner component. Adding `v-bind="$attrs"` on the Charpente component ensures attributes like
243
- `id`, `class`, `required`, or `disabled` pass all the way through to the native HTML element.
304
+ instead of the inner component. Adding `v-bind="$attrs"` on the Charpente component ensures attributes like `id`,
305
+ `class`, `required`, or `disabled` pass all the way through to the native HTML element. And since `CField` works by
306
+ provide/inject, the label, select and error stay linked across your wrapper's boundary — accessibility included.
244
307
 
245
308
  This pattern works the same way for all Charpente components (`CInput`, `CCheckbox`, `CRadio`, etc.).
@@ -1,11 +1,14 @@
1
- import { checkboxGroupKey as e, fieldKey as t } from "./internal/keys.js";
2
- import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeModels as s, mergeProps as c, openBlock as l, unref as u, useAttrs as d, useId as f, useModel as p, useTemplateRef as m, vModelCheckbox as h, watchPostEffect as g, withDirectives as _ } from "vue";
1
+ import { checkboxGroupKey as e } from "./internal/keys.js";
2
+ import { useFieldControl as t } from "./internal/field.js";
3
+ import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeModels as s, mergeProps as c, openBlock as l, unref as u, useAttrs as d, useModel as f, useTemplateRef as p, vModelCheckbox as m, watchPostEffect as h, withDirectives as g } from "vue";
3
4
  //#region src/components/BaseCheckbox.vue?vue&type=script&setup=true&lang.ts
4
- var v = [
5
+ var _ = [
5
6
  "id",
7
+ "aria-describedby",
8
+ "aria-invalid",
6
9
  "name",
7
10
  "value"
8
- ], y = /* @__PURE__ */ i({
11
+ ], v = /* @__PURE__ */ i({
9
12
  inheritAttrs: !1,
10
13
  __name: "BaseCheckbox",
11
14
  props: /* @__PURE__ */ s({
@@ -17,19 +20,21 @@ var v = [
17
20
  }),
18
21
  emits: ["update:modelValue"],
19
22
  setup(i) {
20
- let s = i, y = p(i, "modelValue"), b = a(e, null), x = b ? b.model : y, S = d(), C = f(), w = m("input"), T = a(t, null), E = n(() => typeof S.id == "string" ? S.id : T?.id.value ?? C), D = n(() => typeof S.name == "string" ? S.name : b?.name.value);
21
- return g(() => {
23
+ let s = i, v = f(i, "modelValue"), y = a(e, null), b = y ? y.model : v, x = d(), S = p("input"), { controlId: C, describedBy: w, ariaInvalid: T } = t(), E = n(() => typeof x.name == "string" ? x.name : y?.name.value);
24
+ return h(() => {
22
25
  /* v8 ignore next 2 */
23
- w.value && (w.value.indeterminate = !!s.indeterminate);
24
- }), (e, t) => _((l(), r("input", c(e.$attrs, {
25
- id: E.value,
26
+ S.value && (S.value.indeterminate = !!s.indeterminate);
27
+ }), (e, t) => g((l(), r("input", c(e.$attrs, {
28
+ id: u(C),
26
29
  ref: "input",
27
- "onUpdate:modelValue": t[0] ||= (e) => o(x) ? x.value = e : null,
28
- name: D.value,
30
+ "onUpdate:modelValue": t[0] ||= (e) => o(b) ? b.value = e : null,
31
+ "aria-describedby": u(w),
32
+ "aria-invalid": u(T),
33
+ name: E.value,
29
34
  type: "checkbox",
30
35
  value: i.value
31
- }), null, 16, v)), [[h, u(x)]]);
36
+ }), null, 16, _)), [[m, u(b)]]);
32
37
  }
33
38
  });
34
39
  //#endregion
35
- export { y as default };
40
+ export { v as default };
@@ -1,13 +1,31 @@
1
1
  import { fieldKey as e } from "./internal/keys.js";
2
- import { computed as t, createElementBlock as n, defineComponent as r, guardReactiveProps as i, normalizeProps as a, openBlock as o, provide as s, renderSlot as c, useId as l } from "vue";
2
+ import { computed as t, createElementBlock as n, defineComponent as r, mergeProps as i, openBlock as a, provide as o, ref as s, renderSlot as c, useAttrs as l, useId as u } from "vue";
3
3
  //#region src/components/BaseField.vue?vue&type=script&setup=true&lang.ts
4
- var u = /* @__PURE__ */ r({
4
+ var d = /* @__PURE__ */ r({
5
5
  inheritAttrs: !1,
6
6
  __name: "BaseField",
7
7
  setup(r) {
8
- let u = l();
9
- return s(e, { id: t(() => u) }), (e, t) => (o(), n("div", a(i(e.$attrs)), [c(e.$slots, "default")], 16));
8
+ let d = l(), f = u(), p = s(), m = s(""), h = s(!1), g = t(() => typeof d.id == "string" ? d.id : f), _ = t(() => {
9
+ let e = { ...d };
10
+ return delete e.id, e;
11
+ });
12
+ o(e, {
13
+ id: g,
14
+ supportingTextId: p,
15
+ validationMessage: m
16
+ });
17
+ function v(e) {
18
+ h.value = !0, m.value = e.target.validationMessage;
19
+ }
20
+ function y(e) {
21
+ h.value && (m.value = e.target.validationMessage);
22
+ }
23
+ return (e, t) => (a(), n("div", i(_.value, {
24
+ onInvalidCapture: v,
25
+ onInputCapture: y,
26
+ onChangeCapture: y
27
+ }), [c(e.$slots, "default")], 16));
10
28
  }
11
29
  });
12
30
  //#endregion
13
- export { u as default };
31
+ export { d as default };
@@ -1,7 +1,11 @@
1
- import { fieldKey as e } from "./internal/keys.js";
2
- import { computed as t, createElementBlock as n, defineComponent as r, inject as i, mergeProps as a, openBlock as o, useAttrs as s, useId as c, useModel as l, useTemplateRef as u, watch as d } from "vue";
1
+ import { useFieldControl as e } from "./internal/field.js";
2
+ import { createElementBlock as t, defineComponent as n, mergeProps as r, openBlock as i, unref as a, useModel as o, useTemplateRef as s, watch as c } from "vue";
3
3
  //#region src/components/BaseFile.vue?vue&type=script&setup=true&lang.ts
4
- var f = ["id"], p = /* @__PURE__ */ r({
4
+ var l = [
5
+ "id",
6
+ "aria-describedby",
7
+ "aria-invalid"
8
+ ], u = /* @__PURE__ */ n({
5
9
  inheritAttrs: !1,
6
10
  __name: "BaseFile",
7
11
  props: {
@@ -9,20 +13,22 @@ var f = ["id"], p = /* @__PURE__ */ r({
9
13
  modelModifiers: {}
10
14
  },
11
15
  emits: ["update:modelValue"],
12
- setup(r) {
13
- let p = l(r, "modelValue"), m = s(), h = c(), g = u("input"), _ = i(e, null), v = t(() => typeof m.id == "string" ? m.id : _?.id.value ?? h);
14
- function y(e) {
15
- p.value = e.target.files;
16
+ setup(n) {
17
+ let u = o(n, "modelValue"), d = s("input"), { controlId: f, describedBy: p, ariaInvalid: m } = e();
18
+ function h(e) {
19
+ u.value = e.target.files;
16
20
  }
17
- return d(p, (e) => {
18
- !e && g.value && (g.value.value = "");
19
- }), (e, t) => (o(), n("input", a(e.$attrs, {
20
- id: v.value,
21
+ return c(u, (e) => {
22
+ !e && d.value && (d.value.value = "");
23
+ }), (e, n) => (i(), t("input", r(e.$attrs, {
24
+ id: a(f),
21
25
  ref: "input",
26
+ "aria-describedby": a(p),
27
+ "aria-invalid": a(m),
22
28
  type: "file",
23
- onChange: y
24
- }), null, 16, f));
29
+ onChange: h
30
+ }), null, 16, l));
25
31
  }
26
32
  });
27
33
  //#endregion
28
- export { p as default };
34
+ export { u as default };
@@ -1,3 +1,6 @@
1
+ type __VLS_Props = {
2
+ validate?: boolean;
3
+ };
1
4
  declare function __VLS_template(): {
2
5
  attrs: Partial<{}>;
3
6
  slots: {
@@ -7,11 +10,11 @@ declare function __VLS_template(): {
7
10
  rootEl: any;
8
11
  };
9
12
  type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
10
- declare const __VLS_component: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
13
+ declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
11
14
  submit: (event: SubmitEvent) => any;
12
- }, string, import('vue').PublicProps, Readonly<{}> & Readonly<{
15
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
13
16
  onSubmit?: ((event: SubmitEvent) => any) | undefined;
14
- }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
17
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
15
18
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
16
19
  export default _default;
17
20
  type __VLS_WithTemplateSlots<T, S> = T & {
@@ -1,14 +1,33 @@
1
1
  import { computed as e, createElementBlock as t, defineComponent as n, mergeProps as r, openBlock as i, renderSlot as a, useAttrs as o, useId as s, withModifiers as c } from "vue";
2
2
  //#region src/components/BaseForm.vue?vue&type=script&setup=true&lang.ts
3
- var l = ["id"], u = /* @__PURE__ */ n({
3
+ var l = ["id", "novalidate"], u = /* @__PURE__ */ n({
4
4
  inheritAttrs: !1,
5
5
  __name: "BaseForm",
6
+ props: { validate: { type: Boolean } },
6
7
  emits: ["submit"],
7
8
  setup(n, { emit: u }) {
8
- let d = o(), f = s(), p = u, m = e(() => typeof d.id == "string" ? d.id : f);
9
- return (e, n) => (i(), t("form", r(e.$attrs, {
10
- id: m.value,
11
- onSubmit: n[0] ||= c((e) => p("submit", e), ["prevent"])
9
+ let d = n, f = o(), p = s(), m = u, h = e(() => typeof f.id == "string" ? f.id : p);
10
+ function g(e) {
11
+ let t = e.target;
12
+ if (d.validate && !t.checkValidity()) {
13
+ _(t);
14
+ return;
15
+ }
16
+ m("submit", e);
17
+ }
18
+ function _(e) {
19
+ for (let t of Array.from(e.elements)) {
20
+ let e = t;
21
+ if (e.willValidate && !e.validity.valid) {
22
+ e.focus();
23
+ break;
24
+ }
25
+ }
26
+ }
27
+ return (e, o) => (i(), t("form", r(e.$attrs, {
28
+ id: h.value,
29
+ novalidate: n.validate || void 0,
30
+ onSubmit: c(g, ["prevent"])
12
31
  }), [a(e.$slots, "default")], 16, l));
13
32
  }
14
33
  });
@@ -1,8 +1,16 @@
1
- import { fieldKey as e } from "./internal/keys.js";
1
+ import { useFieldControl as e } from "./internal/field.js";
2
2
  import { applyModelModifiers as t } from "./internal/modifiers.js";
3
- import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeProps as s, openBlock as c, unref as l, useAttrs as u, useId as d, useModel as f, vModelDynamic as p, withDirectives as m } from "vue";
3
+ import { createElementBlock as n, defineComponent as r, isRef as i, mergeProps as a, openBlock as o, unref as s, useModel as c, vModelDynamic as l, withDirectives as u } from "vue";
4
4
  //#region src/components/BaseInput.vue?vue&type=script&setup=true&lang.ts
5
- var h = ["id"], g = ["id"], _ = /* @__PURE__ */ i({
5
+ var d = [
6
+ "id",
7
+ "aria-describedby",
8
+ "aria-invalid"
9
+ ], f = [
10
+ "id",
11
+ "aria-describedby",
12
+ "aria-invalid"
13
+ ], p = /* @__PURE__ */ r({
6
14
  inheritAttrs: !1,
7
15
  __name: "BaseInput",
8
16
  props: {
@@ -10,21 +18,25 @@ var h = ["id"], g = ["id"], _ = /* @__PURE__ */ i({
10
18
  modelModifiers: {}
11
19
  },
12
20
  emits: ["update:modelValue"],
13
- setup(i) {
14
- let [_, v] = f(i, "modelValue", { set: (e) => t(e, v) }), y = u(), b = d(), x = a(e, null), S = n(() => typeof y.id == "string" ? y.id : x?.id.value ?? b);
15
- return (e, t) => l(v).lazy ? m((c(), r("input", s({ key: 0 }, e.$attrs, {
16
- id: S.value,
17
- "onUpdate:modelValue": t[0] ||= (e) => o(_) ? _.value = e : null
18
- }), null, 16, h)), [[
19
- p,
20
- l(_),
21
+ setup(r) {
22
+ let [p, m] = c(r, "modelValue", { set: (e) => t(e, m) }), { controlId: h, describedBy: g, ariaInvalid: _ } = e();
23
+ return (e, t) => s(m).lazy ? u((o(), n("input", a({ key: 0 }, e.$attrs, {
24
+ id: s(h),
25
+ "onUpdate:modelValue": t[0] ||= (e) => i(p) ? p.value = e : null,
26
+ "aria-describedby": s(g),
27
+ "aria-invalid": s(_)
28
+ }), null, 16, d)), [[
29
+ l,
30
+ s(p),
21
31
  void 0,
22
32
  { lazy: !0 }
23
- ]]) : m((c(), r("input", s({ key: 1 }, e.$attrs, {
24
- id: S.value,
25
- "onUpdate:modelValue": t[1] ||= (e) => o(_) ? _.value = e : null
26
- }), null, 16, g)), [[p, l(_)]]);
33
+ ]]) : u((o(), n("input", a({ key: 1 }, e.$attrs, {
34
+ id: s(h),
35
+ "onUpdate:modelValue": t[1] ||= (e) => i(p) ? p.value = e : null,
36
+ "aria-describedby": s(g),
37
+ "aria-invalid": s(_)
38
+ }), null, 16, f)), [[l, s(p)]]);
27
39
  }
28
40
  });
29
41
  //#endregion
30
- export { _ as default };
42
+ export { p as default };
@@ -1,11 +1,14 @@
1
- import { fieldKey as e, radioGroupKey as t } from "./internal/keys.js";
2
- import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeModels as s, mergeProps as c, openBlock as l, unref as u, useAttrs as d, useId as f, useModel as p, vModelRadio as m, withDirectives as h } from "vue";
1
+ import { radioGroupKey as e } from "./internal/keys.js";
2
+ import { useFieldControl as t } from "./internal/field.js";
3
+ import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeModels as s, mergeProps as c, openBlock as l, unref as u, useAttrs as d, useModel as f, vModelRadio as p, withDirectives as m } from "vue";
3
4
  //#region src/components/BaseRadio.vue?vue&type=script&setup=true&lang.ts
4
- var g = [
5
+ var h = [
5
6
  "id",
7
+ "aria-describedby",
8
+ "aria-invalid",
6
9
  "name",
7
10
  "value"
8
- ], _ = /* @__PURE__ */ i({
11
+ ], g = /* @__PURE__ */ i({
9
12
  inheritAttrs: !1,
10
13
  __name: "BaseRadio",
11
14
  props: /* @__PURE__ */ s({ value: {} }, {
@@ -14,15 +17,17 @@ var g = [
14
17
  }),
15
18
  emits: ["update:modelValue"],
16
19
  setup(i) {
17
- let s = p(i, "modelValue"), _ = a(t, null), v = _ ? _.model : s, y = d(), b = f(), x = a(e, null), S = n(() => typeof y.id == "string" ? y.id : x?.id.value ?? b), C = n(() => typeof y.name == "string" ? y.name : _?.name.value);
18
- return (e, t) => h((l(), r("input", c(e.$attrs, {
19
- id: S.value,
20
- "onUpdate:modelValue": t[0] ||= (e) => o(v) ? v.value = e : null,
21
- name: C.value,
20
+ let s = f(i, "modelValue"), g = a(e, null), _ = g ? g.model : s, v = d(), { controlId: y, describedBy: b, ariaInvalid: x } = t(), S = n(() => typeof v.name == "string" ? v.name : g?.name.value);
21
+ return (e, t) => m((l(), r("input", c(e.$attrs, {
22
+ id: u(y),
23
+ "onUpdate:modelValue": t[0] ||= (e) => o(_) ? _.value = e : null,
24
+ "aria-describedby": u(b),
25
+ "aria-invalid": u(x),
26
+ name: S.value,
22
27
  value: i.value,
23
28
  type: "radio"
24
- }), null, 16, g)), [[m, u(v)]]);
29
+ }), null, 16, h)), [[p, u(_)]]);
25
30
  }
26
31
  });
27
32
  //#endregion
28
- export { _ as default };
33
+ export { g as default };
@@ -1,7 +1,11 @@
1
- import { fieldKey as e } from "./internal/keys.js";
2
- import { computed as t, createElementBlock as n, defineComponent as r, inject as i, mergeProps as a, openBlock as o, renderSlot as s, useAttrs as c, useId as l, useModel as u, vModelSelect as d, withDirectives as f } from "vue";
1
+ import { useFieldControl as e } from "./internal/field.js";
2
+ import { createElementBlock as t, defineComponent as n, mergeProps as r, openBlock as i, renderSlot as a, unref as o, useModel as s, vModelSelect as c, withDirectives as l } from "vue";
3
3
  //#region src/components/BaseSelect.vue?vue&type=script&setup=true&lang.ts
4
- var p = ["id"], m = /* @__PURE__ */ r({
4
+ var u = [
5
+ "id",
6
+ "aria-describedby",
7
+ "aria-invalid"
8
+ ], d = /* @__PURE__ */ n({
5
9
  inheritAttrs: !1,
6
10
  __name: "BaseSelect",
7
11
  props: {
@@ -9,13 +13,15 @@ var p = ["id"], m = /* @__PURE__ */ r({
9
13
  modelModifiers: {}
10
14
  },
11
15
  emits: ["update:modelValue"],
12
- setup(r) {
13
- let m = u(r, "modelValue"), h = c(), g = l(), _ = i(e, null), v = t(() => typeof h.id == "string" ? h.id : _?.id.value ?? g);
14
- return (e, t) => f((o(), n("select", a(e.$attrs, {
15
- id: v.value,
16
- "onUpdate:modelValue": t[0] ||= (e) => m.value = e
17
- }), [s(e.$slots, "default")], 16, p)), [[d, m.value]]);
16
+ setup(n) {
17
+ let d = s(n, "modelValue"), { controlId: f, describedBy: p, ariaInvalid: m } = e();
18
+ return (e, n) => l((i(), t("select", r(e.$attrs, {
19
+ id: o(f),
20
+ "onUpdate:modelValue": n[0] ||= (e) => d.value = e,
21
+ "aria-describedby": o(p),
22
+ "aria-invalid": o(m)
23
+ }), [a(e.$slots, "default")], 16, u)), [[c, d.value]]);
18
24
  }
19
25
  });
20
26
  //#endregion
21
- export { m as default };
27
+ export { d as default };
@@ -0,0 +1,20 @@
1
+ type __VLS_Props = {
2
+ validation?: boolean;
3
+ };
4
+ declare function __VLS_template(): {
5
+ attrs: Partial<{}>;
6
+ slots: {
7
+ default?(_: {}): any;
8
+ };
9
+ refs: {};
10
+ rootEl: any;
11
+ };
12
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
13
+ declare const __VLS_component: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
15
+ export default _default;
16
+ type __VLS_WithTemplateSlots<T, S> = T & {
17
+ new (): {
18
+ $slots: S;
19
+ };
20
+ };
@@ -0,0 +1,5 @@
1
+ import e from "./BaseSupportingText.vue?vue&type=script&setup=true&lang.js";
2
+ //#region src/components/BaseSupportingText.vue
3
+ var t = e;
4
+ //#endregion
5
+ export { t as default };
@@ -0,0 +1,18 @@
1
+ import { fieldKey as e } from "./internal/keys.js";
2
+ import { Fragment as t, computed as n, createElementBlock as r, createTextVNode as i, defineComponent as a, inject as o, mergeProps as s, onBeforeUnmount as c, openBlock as l, renderSlot as u, toDisplayString as d, useAttrs as f, useId as p, watchEffect as m } from "vue";
3
+ //#region src/components/BaseSupportingText.vue?vue&type=script&setup=true&lang.ts
4
+ var h = ["id"], g = /* @__PURE__ */ a({
5
+ inheritAttrs: !1,
6
+ __name: "BaseSupportingText",
7
+ props: { validation: { type: Boolean } },
8
+ setup(a) {
9
+ let g = a, _ = f(), v = p(), y = o(e, null), b = n(() => typeof _.id == "string" ? _.id : v), x = n(() => g.validation ? y?.validationMessage.value ?? "" : "");
10
+ return m(() => {
11
+ y && (y.supportingTextId.value = b.value);
12
+ }), c(() => {
13
+ y && (y.supportingTextId.value = void 0);
14
+ }), (e, n) => (l(), r("p", s(e.$attrs, { id: b.value }), [x.value ? (l(), r(t, { key: 0 }, [i(d(x.value), 1)], 64)) : u(e.$slots, "default", { key: 1 })], 16, h));
15
+ }
16
+ });
17
+ //#endregion
18
+ export { g as default };
@@ -1,8 +1,16 @@
1
- import { fieldKey as e } from "./internal/keys.js";
1
+ import { useFieldControl as e } from "./internal/field.js";
2
2
  import { applyModelModifiers as t } from "./internal/modifiers.js";
3
- import { computed as n, createElementBlock as r, defineComponent as i, inject as a, isRef as o, mergeProps as s, openBlock as c, unref as l, useAttrs as u, useId as d, useModel as f, vModelText as p, withDirectives as m } from "vue";
3
+ import { createElementBlock as n, defineComponent as r, isRef as i, mergeProps as a, openBlock as o, unref as s, useModel as c, vModelText as l, withDirectives as u } from "vue";
4
4
  //#region src/components/BaseTextarea.vue?vue&type=script&setup=true&lang.ts
5
- var h = ["id"], g = ["id"], _ = /* @__PURE__ */ i({
5
+ var d = [
6
+ "id",
7
+ "aria-describedby",
8
+ "aria-invalid"
9
+ ], f = [
10
+ "id",
11
+ "aria-describedby",
12
+ "aria-invalid"
13
+ ], p = /* @__PURE__ */ r({
6
14
  inheritAttrs: !1,
7
15
  __name: "BaseTextarea",
8
16
  props: {
@@ -10,21 +18,25 @@ var h = ["id"], g = ["id"], _ = /* @__PURE__ */ i({
10
18
  modelModifiers: {}
11
19
  },
12
20
  emits: ["update:modelValue"],
13
- setup(i) {
14
- let [_, v] = f(i, "modelValue", { set: (e) => t(e, v) }), y = u(), b = d(), x = a(e, null), S = n(() => typeof y.id == "string" ? y.id : x?.id.value ?? b);
15
- return (e, t) => l(v).lazy ? m((c(), r("textarea", s({ key: 0 }, e.$attrs, {
16
- id: S.value,
17
- "onUpdate:modelValue": t[0] ||= (e) => o(_) ? _.value = e : null
18
- }), null, 16, h)), [[
19
- p,
20
- l(_),
21
+ setup(r) {
22
+ let [p, m] = c(r, "modelValue", { set: (e) => t(e, m) }), { controlId: h, describedBy: g, ariaInvalid: _ } = e();
23
+ return (e, t) => s(m).lazy ? u((o(), n("textarea", a({ key: 0 }, e.$attrs, {
24
+ id: s(h),
25
+ "onUpdate:modelValue": t[0] ||= (e) => i(p) ? p.value = e : null,
26
+ "aria-describedby": s(g),
27
+ "aria-invalid": s(_)
28
+ }), null, 16, d)), [[
29
+ l,
30
+ s(p),
21
31
  void 0,
22
32
  { lazy: !0 }
23
- ]]) : m((c(), r("textarea", s({ key: 1 }, e.$attrs, {
24
- id: S.value,
25
- "onUpdate:modelValue": t[1] ||= (e) => o(_) ? _.value = e : null
26
- }), null, 16, g)), [[p, l(_)]]);
33
+ ]]) : u((o(), n("textarea", a({ key: 1 }, e.$attrs, {
34
+ id: s(h),
35
+ "onUpdate:modelValue": t[1] ||= (e) => i(p) ? p.value = e : null,
36
+ "aria-describedby": s(g),
37
+ "aria-invalid": s(_)
38
+ }), null, 16, f)), [[l, s(p)]]);
27
39
  }
28
40
  });
29
41
  //#endregion
30
- export { _ as default };
42
+ export { p as default };
@@ -0,0 +1,5 @@
1
+ export declare function useFieldControl(): {
2
+ controlId: import('vue').ComputedRef<string>;
3
+ describedBy: import('vue').ComputedRef<string | undefined>;
4
+ ariaInvalid: import('vue').ComputedRef<unknown>;
5
+ };
@@ -0,0 +1,13 @@
1
+ import { fieldKey as e } from "./keys.js";
2
+ import { computed as t, inject as n, useAttrs as r, useId as i } from "vue";
3
+ //#region src/components/internal/field.ts
4
+ function a() {
5
+ let a = r(), o = i(), s = n(e, null);
6
+ return {
7
+ controlId: t(() => typeof a.id == "string" ? a.id : s?.id.value ?? o),
8
+ describedBy: t(() => typeof a["aria-describedby"] == "string" ? a["aria-describedby"] : s?.supportingTextId.value),
9
+ ariaInvalid: t(() => "aria-invalid" in a ? a["aria-invalid"] : s?.validationMessage.value ? "true" : void 0)
10
+ };
11
+ }
12
+ //#endregion
13
+ export { a as useFieldControl };
@@ -1,4 +1,4 @@
1
- import { InjectionKey, ModelRef, ComputedRef } from 'vue';
1
+ import { InjectionKey, ModelRef, ComputedRef, Ref } from 'vue';
2
2
  export interface RadioGroupContext {
3
3
  model: ModelRef<string | number | undefined>;
4
4
  name: ComputedRef<string>;
@@ -11,5 +11,7 @@ export interface CheckboxGroupContext {
11
11
  export declare const checkboxGroupKey: InjectionKey<CheckboxGroupContext>;
12
12
  export interface FieldContext {
13
13
  id: ComputedRef<string>;
14
+ supportingTextId: Ref<string | undefined>;
15
+ validationMessage: Ref<string>;
14
16
  }
15
17
  export declare const fieldKey: InjectionKey<FieldContext | null>;
package/dist/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export { default as CLabel } from './components/BaseLabel';
9
9
  export { default as CRadio } from './components/BaseRadio';
10
10
  export { default as CRadioGroup } from './components/BaseRadioGroup';
11
11
  export { default as CSelect } from './components/BaseSelect';
12
+ export { default as CSupportingText } from './components/BaseSupportingText';
12
13
  export { default as CTextarea } from './components/BaseTextarea';
package/dist/index.js CHANGED
@@ -9,5 +9,6 @@ import s from "./components/BaseLabel.js";
9
9
  import c from "./components/BaseRadio.js";
10
10
  import l from "./components/BaseRadioGroup.js";
11
11
  import u from "./components/BaseSelect.js";
12
- import d from "./components/BaseTextarea.js";
13
- export { e as CButton, t as CCheckbox, n as CCheckboxGroup, r as CField, i as CFile, a as CForm, o as CInput, s as CLabel, c as CRadio, l as CRadioGroup, u as CSelect, d as CTextarea };
12
+ import d from "./components/BaseSupportingText.js";
13
+ import f from "./components/BaseTextarea.js";
14
+ export { e as CButton, t as CCheckbox, n as CCheckboxGroup, r as CField, i as CFile, a as CForm, o as CInput, s as CLabel, c as CRadio, l as CRadioGroup, u as CSelect, d as CSupportingText, f as CTextarea };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charpente-ui/vue",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Charpente UI is a headless Vue component library.",
5
5
  "type": "module",
6
6
  "scripts": {