@iress-oss/ids-components 6.2.2 → 6.3.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.
@@ -530,6 +530,38 @@ Some use cases:
530
530
 
531
531
  [View "ValidationSummary" example in Storybook →](https://main--691abcc79dfa560a36d0a74f.chromatic.com/?path=/story/components_patterns-form--validation-summary)
532
532
 
533
+ ### Controlling screen reader announcements
534
+
535
+ By default, `IressFormValidationSummary` uses `role="alert"` (an assertive live region), which causes screen readers to interrupt the user and read out the entire error list immediately. When using `mode="onBlur"`, a new error is added every time the user leaves a field — this can be disruptive.
536
+
537
+ You have two options for a less intrusive experience:
538
+
539
+ **Option 1 — Polite announcements via `aria-live`**
540
+
541
+ Pass `aria-live="polite"` and `role="status"` to `IressFormValidationSummary` to switch to a polite live region. Screen readers will wait for a pause in user activity before announcing changes.
542
+
543
+ ```tsx
544
+ <IressForm mode="onBlur">
545
+ <IressFormValidationSummary aria-live="polite" role="status" srOnly />
546
+ {/* fields */}
547
+ </IressForm>
548
+ ```
549
+
550
+ **Option 2 — Only announce on submit via `updateErrorSummaryOnSubmit`**
551
+
552
+ Pass `updateErrorSummaryOnSubmit` to `IressForm` (or `IressHookForm`) to prevent the summary from updating while the user is still filling in the form. The summary will only be populated — and announced — when the form is submitted.
553
+
554
+ ```tsx
555
+ <IressForm mode="onBlur" updateErrorSummaryOnSubmit>
556
+ {/* fields */}
557
+ </IressForm>
558
+ ```
559
+
560
+ | Approach | When to use |
561
+ |---|---|
562
+ | `aria-live="polite"` on `IressFormValidationSummary` | Real-time feedback is still desirable but should not interrupt the user |
563
+ | `updateErrorSummaryOnSubmit` on `IressForm` | Errors should only be announced once the user tries to submit |
564
+
533
565
  ## Migration to version 5 and beyond
534
566
 
535
567
  The previous form components contained a lot of logic to translate the HTML5 validation API to a format that matched the design system’s guidelines. This allowed users to use the default props of input such as `pattern` and `required`, and be assured that the `IressField` would display errors accordingly.
@@ -121,6 +121,7 @@ var x = (e) => typeof e == "object" && !!e && "label" in e, S = (e, t) => {
121
121
  disabled: s,
122
122
  id: c,
123
123
  name: u,
124
+ onBlur: f,
124
125
  onChange: (e, t) => {
125
126
  d?.(e, t?.value ?? null, t), V(t);
126
127
  },
@@ -1,2 +1,2 @@
1
- import { t as e } from "../../Autocomplete-CzdfUGYJ.js";
1
+ import { t as e } from "../../Autocomplete-ZqkibVwt.js";
2
2
  export { e as IressAutocomplete };
@@ -1,4 +1,4 @@
1
1
  import { autoComplete as e } from "./Autocomplete.styles.js";
2
2
  import { t } from "../../useAutocompleteSearch-BYzUn7Ag.js";
3
- import { t as n } from "../../Autocomplete-CzdfUGYJ.js";
3
+ import { t as n } from "../../Autocomplete-ZqkibVwt.js";
4
4
  export { n as IressAutocomplete, e as autoComplete, t as useAutocompleteSearch };
@@ -40,6 +40,12 @@ export type IressFieldProps<E extends 'div' | 'fieldset' = 'div', ELabel extends
40
40
  * Validation errors to be displayed above the field, an array of validation messages to be displayed in `IressValidationSummary`.
41
41
  */
42
42
  errorMessages?: ValidationMessageObj[];
43
+ /**
44
+ * ID for the error container element, used to associate the error message with the form control via `aria-describedby`.
45
+ * When `error` or `errorMessages` are present and `htmlFor` is set, this is automatically derived as `${htmlFor}-error`.
46
+ * Override only if you need a custom error container ID.
47
+ */
48
+ errorId?: string;
43
49
  /**
44
50
  * Renders the group in a read-only state (no asterisk symbol).
45
51
  * Use `'locked'` when the control is read-only due to permissions.
@@ -56,6 +62,6 @@ export type IressFieldProps<E extends 'div' | 'fieldset' = 'div', ELabel extends
56
62
  supplementary?: ReactNode;
57
63
  };
58
64
  export declare const IressField: {
59
- ({ children, className, "data-testid": dataTestId, error, errorMessages, hiddenLabel, hint, horizontal, label, labelWidth, required, readOnly, removeErrorMargin, supplementary, ...restProps }: IressFieldProps): import("react/jsx-runtime").JSX.Element;
65
+ ({ children, className, "data-testid": dataTestId, error, errorId, errorMessages, hiddenLabel, hint, horizontal, label, labelWidth, required, readOnly, removeErrorMargin, supplementary, ...restProps }: IressFieldProps): import("react/jsx-runtime").JSX.Element;
60
66
  displayName: string;
61
67
  };
@@ -12,57 +12,58 @@ import { FieldHint as u } from "./components/FieldHint.js";
12
12
  import { isValidElement as d } from "react";
13
13
  import { jsx as f, jsxs as p } from "react/jsx-runtime";
14
14
  //#region src/components/Field/Field.tsx
15
- var m = ({ children: m, className: h, "data-testid": g, error: _, errorMessages: v, hiddenLabel: y, hint: b, horizontal: x, label: S, labelWidth: C, required: w, readOnly: T, removeErrorMargin: E = !1, supplementary: D, ...O }) => {
16
- let { htmlFor: k } = O, A;
17
- A = k === void 0 && d(m) ? o(m)[0].props.id : k;
18
- let j = c.raw({
19
- hasError: !!_ || !!v?.length,
20
- hasHint: !!b,
21
- hiddenLabel: y,
22
- horizontal: x,
15
+ var m = ({ children: m, className: h, "data-testid": g, error: _, errorId: v, errorMessages: y, hiddenLabel: b, hint: x, horizontal: S, label: C, labelWidth: w, required: T, readOnly: E, removeErrorMargin: D = !1, supplementary: O, ...k }) => {
16
+ let { htmlFor: A } = k, j;
17
+ j = A === void 0 && d(m) ? o(m)[0].props.id : A;
18
+ let M = !!_ || !!y?.length, N = v ?? (M && j ? `${j}-error` : void 0), P = c.raw({
19
+ hasError: M,
20
+ hasHint: !!x,
21
+ hiddenLabel: b,
22
+ horizontal: S,
23
23
  multipleFields: !1,
24
- removeErrorMargin: E
25
- }), [M, N] = n(O), P = !!T, F = () => {
24
+ removeErrorMargin: D
25
+ }), [F, I] = n(k), L = !!E, R = () => {
26
26
  let t = /* @__PURE__ */ f(s, {
27
- className: e(j.label),
27
+ className: e(P.label),
28
28
  "data-testid": a(g, "label"),
29
- hiddenLabel: y,
30
- htmlFor: A,
31
- readOnly: T,
32
- required: !P && !!w,
33
- children: S
29
+ hiddenLabel: b,
30
+ htmlFor: j,
31
+ readOnly: E,
32
+ required: !L && !!T,
33
+ children: C
34
34
  });
35
- return b ? /* @__PURE__ */ f(u, {
36
- hint: b,
37
- horizontal: x,
38
- hiddenLabel: y,
35
+ return x ? /* @__PURE__ */ f(u, {
36
+ hint: x,
37
+ horizontal: S,
38
+ hiddenLabel: b,
39
39
  dataTestId: g,
40
40
  children: t
41
41
  }) : t;
42
42
  };
43
43
  return /* @__PURE__ */ p(r.div, {
44
- className: t(h, e(j.root, M), i.Field),
45
- style: { ...x && C && { gridTemplateColumns: `${C} 1fr` } },
44
+ className: t(h, e(P.root, F), i.Field),
45
+ style: { ...S && w && { gridTemplateColumns: `${w} 1fr` } },
46
46
  "data-testid": g,
47
- ...N,
47
+ ...I,
48
48
  children: [
49
49
  /* @__PURE__ */ f(r.div, {
50
- className: e(j.labelContainer),
51
- children: F()
50
+ className: e(P.labelContainer),
51
+ children: R()
52
52
  }),
53
53
  /* @__PURE__ */ f(r.div, {
54
- className: e(j.fieldContainer),
54
+ className: e(P.fieldContainer),
55
55
  children: /* @__PURE__ */ f("div", {
56
- className: e(j.element),
56
+ className: e(P.element),
57
57
  children: m
58
58
  })
59
59
  }),
60
60
  /* @__PURE__ */ f(l, {
61
- className: e(j.footer),
61
+ className: e(P.footer),
62
62
  "data-testid": a(g, "error"),
63
63
  error: _,
64
- errorMessages: v,
65
- supplementary: D
64
+ errorId: N,
65
+ errorMessages: y,
66
+ supplementary: O
66
67
  })
67
68
  ]
68
69
  });
@@ -6,6 +6,10 @@ export interface FieldFooterProps extends IressStyledProps {
6
6
  * Validation error to be displayed above the field.
7
7
  */
8
8
  error?: ReactNode;
9
+ /**
10
+ * ID for the error container element, used to associate the error message with the form control via `aria-describedby`.
11
+ */
12
+ errorId?: string;
9
13
  /**
10
14
  * Validation errors to be displayed above the field, an array of validation messages to be displayed in `IressValidationSummary`.
11
15
  */
@@ -20,6 +24,6 @@ export interface FieldFooterProps extends IressStyledProps {
20
24
  supplementary?: ReactNode;
21
25
  }
22
26
  export declare const FieldFooter: {
23
- ({ className, "data-testid": dataTestId, error, errorMessages, multipleFields, supplementary, ...restProps }: FieldFooterProps): import("react/jsx-runtime").JSX.Element | null;
27
+ ({ className, "data-testid": dataTestId, error, errorId, errorMessages, multipleFields, supplementary, ...restProps }: FieldFooterProps): import("react/jsx-runtime").JSX.Element | null;
24
28
  displayName: string;
25
29
  };
@@ -12,31 +12,32 @@ import { jsx as d, jsxs as f } from "react/jsx-runtime";
12
12
  var p = (e) => /* @__PURE__ */ d(r, {
13
13
  name: "cancel",
14
14
  ...e
15
- }), m = ({ className: r, "data-testid": m, error: h, errorMessages: g = [], multipleFields: _, supplementary: v, ...y }) => {
16
- let b = a({ multipleFields: _ }), x = u(null), S = u(null), C = l(() => !!h || g.length > 0, [h, g.length]);
15
+ }), m = ({ className: r, "data-testid": m, error: h, errorId: g, errorMessages: _ = [], multipleFields: v, supplementary: y, ...b }) => {
16
+ let x = a({ multipleFields: v }), S = u(null), C = u(null), w = l(() => !!h || _.length > 0, [h, _.length]);
17
17
  return c(() => {
18
- if (!x.current || S.current === C) {
19
- S.current = C;
18
+ if (!S.current || C.current === w) {
19
+ C.current = w;
20
20
  return;
21
21
  }
22
- x.current.classList.remove(e({ animationStyle: "field-footer" })), x.current.offsetHeight, S.current = C, x.current.classList.add(e({ animationStyle: "field-footer" }));
23
- }, [C]), !v && !C ? null : /* @__PURE__ */ f(n.div, {
24
- ...y,
25
- className: t(r, b.footer),
26
- ref: x,
27
- children: [C && /* @__PURE__ */ d(s, {
22
+ S.current.classList.remove(e({ animationStyle: "field-footer" })), S.current.offsetHeight, C.current = w, S.current.classList.add(e({ animationStyle: "field-footer" }));
23
+ }, [w]), !y && !w ? null : /* @__PURE__ */ f(n.div, {
24
+ ...b,
25
+ className: t(r, x.footer),
26
+ ref: S,
27
+ children: [w && /* @__PURE__ */ d(s, {
28
+ id: g,
28
29
  itemStyle: { textStyle: "typography.body.sm" },
29
- messages: o(g, "message"),
30
+ messages: o(_, "message"),
30
31
  "data-testid": m,
31
32
  prefix: /* @__PURE__ */ d(p, {
32
33
  "aria-label": "Error: ",
33
- className: b.footerIcon
34
+ className: x.footerIcon
34
35
  }),
35
36
  visiblePrefix: !0,
36
37
  children: h
37
- }), !C && /* @__PURE__ */ d(i, {
38
- className: b.supplementary,
39
- children: v
38
+ }), !w && /* @__PURE__ */ d(i, {
39
+ className: x.supplementary,
40
+ children: y
40
41
  })]
41
42
  });
42
43
  };
@@ -1,2 +1,2 @@
1
- import { n as e } from "../../Autocomplete-CzdfUGYJ.js";
1
+ import { n as e } from "../../Autocomplete-ZqkibVwt.js";
2
2
  export { e as IressSelect };
@@ -29,7 +29,7 @@ var f = l(({ className: l, "data-testid": f, options: p, onChange: m, placeholde
29
29
  m?.(e, t);
30
30
  },
31
31
  ref: b,
32
- value: o(_?.value ?? _?.label),
32
+ value: o(_?.value ?? _?.label) ?? "",
33
33
  children: [h !== void 0 && /* @__PURE__ */ u("option", {
34
34
  value: "",
35
35
  children: h
@@ -6,7 +6,7 @@ import { IressSelectHeading as i } from "./SelectHeading/SelectHeading.js";
6
6
  import { IressSelectMenu as a } from "./SelectMenu/SelectMenu.js";
7
7
  import { IressSelectSearchInput as o } from "./SelectSearchInput/SelectSearchInput.js";
8
8
  import { nativeSelect as s } from "./components/NativeSelect.styles.js";
9
- import { n as c } from "../../Autocomplete-CzdfUGYJ.js";
9
+ import { n as c } from "../../Autocomplete-ZqkibVwt.js";
10
10
  import { IressSelectBody as l } from "./SelectBody/SelectBody.js";
11
11
  import { IressSelectCreate as u } from "./SelectCreate/SelectCreate.js";
12
12
  export { c as IressSelect, l as IressSelectBody, u as IressSelectCreate, i as IressSelectHeading, t as IressSelectLabel, a as IressSelectMenu, r as IressSelectSearch, o as IressSelectSearchInput, n as IressSelectTags, s as nativeSelect, e as select };
package/dist/main.js CHANGED
@@ -66,7 +66,7 @@ import { IressSelectHeading as Ae } from "./components/Select/SelectHeading/Sele
66
66
  import { IressSelectMenu as je } from "./components/Select/SelectMenu/SelectMenu.js";
67
67
  import { IressSelectSearchInput as Me } from "./components/Select/SelectSearchInput/SelectSearchInput.js";
68
68
  import { nativeSelect as Ne } from "./components/Select/components/NativeSelect.styles.js";
69
- import { n as Pe, t as Fe } from "./Autocomplete-CzdfUGYJ.js";
69
+ import { n as Pe, t as Fe } from "./Autocomplete-ZqkibVwt.js";
70
70
  import { IressSelectBody as Ie } from "./components/Select/SelectBody/SelectBody.js";
71
71
  import { IressSelectCreate as Le } from "./components/Select/SelectCreate/SelectCreate.js";
72
72
  import { avatar as Re } from "./components/Avatar/Avatar.styles.js";
@@ -55,6 +55,15 @@ export interface FormFieldRenderProps<T extends FieldValues> extends ControllerR
55
55
  * ID of the field. It is automatically generated based on the name of the field and its parent form.
56
56
  */
57
57
  id: string;
58
+ /**
59
+ * References the ID of the error message container when the field has a validation error,
60
+ * so that assistive technologies can associate the error with the control.
61
+ */
62
+ 'aria-describedby'?: string;
63
+ /**
64
+ * Set to `true` when the field has a validation error, indicating an invalid state to assistive technologies.
65
+ */
66
+ 'aria-invalid'?: boolean;
58
67
  }
59
68
  /**
60
69
  * Props for the `IressFormField` `render` prop, second argument.
@@ -27,21 +27,26 @@ var f = ({ control: f, defaultValue: p, name: m, render: h, renderSupplementary:
27
27
  m,
28
28
  b
29
29
  ]);
30
- let O = {
31
- ...i(w),
32
- id: `${S.id}__${m}`
33
- }, k = {
30
+ let O = i(w), k = `${S.id}__${m}-error`, A = !!D && !b, j = {
31
+ ...O,
32
+ id: `${S.id}__${m}`,
33
+ ...A ? {
34
+ "aria-describedby": k,
35
+ "aria-invalid": !0
36
+ } : {}
37
+ }, M = {
34
38
  formState: T,
35
39
  fieldState: E
36
40
  };
37
41
  return /* @__PURE__ */ l(t, {
38
- errorMessages: D && !b ? [{ message: D }] : void 0,
42
+ errorMessages: A ? [{ message: D }] : void 0,
39
43
  htmlFor: `${S.id}__${m}`,
40
44
  readOnly: b,
41
45
  required: !!C?.required,
42
- supplementary: /* @__PURE__ */ u(c, { children: [g?.(O, k), y] }),
46
+ supplementary: /* @__PURE__ */ u(c, { children: [g?.(j, M), y] }),
43
47
  ...x,
44
- children: h(O, k)
48
+ errorId: k,
49
+ children: h(j, M)
45
50
  });
46
51
  };
47
52
  f.displayName = "IressFormField";
@@ -13,6 +13,6 @@ export type IressFormValidationSummaryProps = Omit<IressValidationSummaryProps,
13
13
  * Validation summary messages based on the form context from react-hook-form
14
14
  */
15
15
  export declare const IressFormValidationSummary: {
16
- ({ actions, children, "data-testid": testId, footer, heading, itemStyle, icon, srOnly, variant, ...restProps }: IressFormValidationSummaryProps): import("react/jsx-runtime").JSX.Element;
16
+ ({ actions, children, "data-testid": testId, footer, heading, itemStyle, icon, srOnly, variant, role, "aria-live": ariaLive, ...restProps }: IressFormValidationSummaryProps): import("react/jsx-runtime").JSX.Element;
17
17
  displayName: string;
18
18
  };
@@ -13,27 +13,31 @@ var c = e("div", { base: {
13
13
  } }, { defaultProps: {
14
14
  role: "alert",
15
15
  tabIndex: -1
16
- } }), l = ({ actions: e, children: l, "data-testid": u, footer: d, heading: f = /* @__PURE__ */ s("h3", { children: "There was a problem submitting this form" }), itemStyle: p, icon: m, srOnly: h, variant: g, ..._ }) => {
17
- let v = o(r);
18
- if (!v) throw Error("IressFormValidationSummary must be used within a IressForm");
19
- let y = Object.entries(v.errorMessages).filter(([, e]) => !!e);
20
- if (y.length === 0 || l) return /* @__PURE__ */ s(c, {
16
+ } }), l = ({ actions: e, children: l, "data-testid": u, footer: d, heading: f = /* @__PURE__ */ s("h3", { children: "There was a problem submitting this form" }), itemStyle: p, icon: m, srOnly: h, variant: g, role: _, "aria-live": v, ...y }) => {
17
+ let b = o(r);
18
+ if (!b) throw Error("IressFormValidationSummary must be used within a IressForm");
19
+ let x = Object.entries(b.errorMessages).filter(([, e]) => !!e);
20
+ if (x.length === 0 || l) return /* @__PURE__ */ s(c, {
21
21
  "data-testid": u,
22
22
  srOnly: h,
23
- ref: v.setFocusOnError,
23
+ ref: b.setFocusOnError,
24
+ ..._ !== void 0 && { role: _ },
25
+ ...v !== void 0 && { "aria-live": v },
24
26
  children: l
25
27
  });
26
- let b = y.map(([e, t]) => {
27
- let n = `${v?.id}__${e}`, r = document.getElementById(n), i = r?.labels, o = Array.from(i ?? []), s = r?.closest("fieldset")?.querySelector("legend");
28
+ let S = x.map(([e, t]) => {
29
+ let n = `${b?.id}__${e}`, r = document.getElementById(n), i = r?.labels, o = Array.from(i ?? []), s = r?.closest("fieldset")?.querySelector("legend");
28
30
  return {
29
31
  linkToTarget: n,
30
32
  message: `${`${o[0]?.dataset?.name ?? s?.dataset?.name ?? a(e)}: `}${t}`
31
33
  };
32
34
  });
33
35
  return /* @__PURE__ */ s(c, {
34
- ref: v.setFocusOnError,
36
+ ref: b.setFocusOnError,
35
37
  "data-testid": u,
36
38
  srOnly: h,
39
+ ..._ !== void 0 && { role: _ },
40
+ ...v !== void 0 && { "aria-live": v },
37
41
  children: /* @__PURE__ */ s(n, {
38
42
  actions: e,
39
43
  footer: d,
@@ -44,13 +48,13 @@ var c = e("div", { base: {
44
48
  variant: g,
45
49
  multiLine: !0,
46
50
  children: /* @__PURE__ */ s(i, {
47
- ..._,
51
+ ...y,
48
52
  "data-testid": t(u, "messages"),
49
53
  itemStyle: {
50
54
  "data-testid": t(u, "error"),
51
55
  ...p
52
56
  },
53
- messages: b
57
+ messages: S
54
58
  })
55
59
  })
56
60
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iress-oss/ids-components",
3
- "version": "6.2.2",
3
+ "version": "6.3.0",
4
4
  "description": "Iress React Component Library",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",