@westpac/ui 1.4.0 → 1.6.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/icons/filled/flag-filled.svg +3 -0
  3. package/assets/icons/outlined/flag-outlined.svg +3 -0
  4. package/dist/component-type.json +1 -1
  5. package/dist/components/autocomplete/autocomplete.component.js +6 -3
  6. package/dist/components/error-message/error-message.component.d.ts +1 -1
  7. package/dist/components/error-message/error-message.component.js +26 -3
  8. package/dist/components/error-message/error-message.styles.d.ts +18 -0
  9. package/dist/components/error-message/error-message.styles.js +4 -1
  10. package/dist/components/error-message/error-message.types.d.ts +4 -0
  11. package/dist/components/field/field.component.d.ts +1 -1
  12. package/dist/components/field/field.component.js +2 -1
  13. package/dist/components/field/field.types.d.ts +4 -0
  14. package/dist/components/icon/components/flag-icon.d.ts +2 -0
  15. package/dist/components/icon/components/flag-icon.js +15 -0
  16. package/dist/components/icon/index.d.ts +1 -0
  17. package/dist/components/icon/index.js +1 -0
  18. package/dist/components/input-group/input-group.component.d.ts +1 -1
  19. package/dist/components/input-group/input-group.component.js +2 -1
  20. package/dist/components/input-group/input-group.types.d.ts +4 -0
  21. package/dist/components/selector/components/selector-button-group/selector-button-group.component.d.ts +1 -1
  22. package/dist/components/selector/components/selector-button-group/selector-button-group.component.js +13 -4
  23. package/dist/components/selector/components/selector-button-group/selector-button-group.types.d.ts +6 -1
  24. package/package.json +3 -3
  25. package/src/components/autocomplete/autocomplete.component.tsx +4 -11
  26. package/src/components/error-message/error-message.component.tsx +27 -3
  27. package/src/components/error-message/error-message.styles.ts +3 -0
  28. package/src/components/error-message/error-message.types.ts +4 -0
  29. package/src/components/field/field.component.tsx +2 -1
  30. package/src/components/field/field.types.ts +4 -0
  31. package/src/components/icon/components/flag-icon.tsx +21 -0
  32. package/src/components/icon/index.ts +1 -0
  33. package/src/components/input-group/input-group.component.tsx +2 -1
  34. package/src/components/input-group/input-group.types.ts +4 -0
  35. package/src/components/selector/components/selector-button-group/selector-button-group.component.tsx +11 -4
  36. package/src/components/selector/components/selector-button-group/selector-button-group.types.ts +6 -1
@@ -71,14 +71,17 @@ function Autocomplete({ size = 'medium', invalid = false, isDisabled, footer, po
71
71
  const { buttonProps } = useButton(clearButtonProps, clearButtonRef);
72
72
  const outerRef = React.useRef(null);
73
73
  const isNoOptionPopOverOpen = useMemo(()=>{
74
- return !!(noOptionsMessage && (!state.isOpen && state.isFocused && searchProps.value.length > 0 && state.selectedItems.length === 0 || state.collection.size === 0 && searchProps.value.length > 0));
74
+ var _ref;
75
+ var _searchProps_value;
76
+ const inputLength = (_ref = (_searchProps_value = searchProps.value) === null || _searchProps_value === void 0 ? void 0 : _searchProps_value.length) !== null && _ref !== void 0 ? _ref : 0;
77
+ return !!(noOptionsMessage && (!state.isOpen && state.isFocused && inputLength > 0 && !state.value || state.collection.size === 0 && inputLength > 0));
75
78
  }, [
76
79
  noOptionsMessage,
77
80
  state.isOpen,
78
81
  state.isFocused,
79
- state.selectedItems,
82
+ state.value,
80
83
  state.collection.size,
81
- searchProps.value.length
84
+ searchProps.value
82
85
  ]);
83
86
  return React.createElement("div", {
84
87
  className: styles.base({
@@ -1,2 +1,2 @@
1
1
  import { type ErrorMessageProps } from './error-message.types.js';
2
- export declare function ErrorMessage({ className, tag: Tag, icon: Icon, message, ...props }: ErrorMessageProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function ErrorMessage({ className, tag: Tag, icon: Icon, errorTitle, message, ...props }: ErrorMessageProps): import("react/jsx-runtime").JSX.Element;
@@ -1,10 +1,33 @@
1
1
  import { clsx } from 'clsx';
2
2
  import React from 'react';
3
3
  import { AlertIcon } from '../../components/icon/index.js';
4
+ import { List, ListItem } from '../../components/list/index.js';
4
5
  import { styles as errorMessageStyles } from './error-message.styles.js';
5
- export function ErrorMessage({ className, tag: Tag = 'div', icon: Icon, message, ...props }) {
6
+ export function ErrorMessage({ className, tag: Tag = 'div', icon: Icon, errorTitle, message, ...props }) {
6
7
  const styles = errorMessageStyles({});
7
8
  const FinalIcon = Icon !== null && Icon !== void 0 ? Icon : AlertIcon;
9
+ if (errorTitle && Array.isArray(message)) {
10
+ return React.createElement("div", {
11
+ className: styles.titleWrapper({
12
+ className
13
+ }),
14
+ ...props
15
+ }, React.createElement("span", {
16
+ className: styles.title({})
17
+ }, React.createElement(FinalIcon, {
18
+ color: "danger",
19
+ copyrightYear: "2026",
20
+ className: styles.icon({}),
21
+ size: "xsmall",
22
+ look: "outlined"
23
+ }), errorTitle), React.createElement(List, {
24
+ type: "bullet",
25
+ look: "primary",
26
+ className: styles.bulletList({})
27
+ }, message.map((msg, index)=>React.createElement(ListItem, {
28
+ key: index
29
+ }, msg))));
30
+ }
8
31
  return Array.isArray(message) ? React.createElement("ul", {
9
32
  className: styles.list({}),
10
33
  ...props
@@ -15,7 +38,7 @@ export function ErrorMessage({ className, tag: Tag = 'div', icon: Icon, message,
15
38
  })
16
39
  }, React.createElement(FinalIcon, {
17
40
  color: "danger",
18
- copyrightYear: "2023",
41
+ copyrightYear: "2026",
19
42
  className: styles.icon({}),
20
43
  size: "xsmall",
21
44
  look: "outlined"
@@ -26,7 +49,7 @@ export function ErrorMessage({ className, tag: Tag = 'div', icon: Icon, message,
26
49
  ...props
27
50
  }, React.createElement(FinalIcon, {
28
51
  color: "danger",
29
- copyrightYear: "2023",
52
+ copyrightYear: "2026",
30
53
  className: styles.icon({}),
31
54
  size: "xsmall",
32
55
  look: "outlined"
@@ -2,36 +2,54 @@ export declare const styles: import("tailwind-variants").TVReturnType<{
2
2
  [key: string]: {
3
3
  [key: string]: import("tailwind-merge").ClassNameValue | {
4
4
  base?: import("tailwind-merge").ClassNameValue;
5
+ title?: import("tailwind-merge").ClassNameValue;
5
6
  list?: import("tailwind-merge").ClassNameValue;
6
7
  icon?: import("tailwind-merge").ClassNameValue;
8
+ titleWrapper?: import("tailwind-merge").ClassNameValue;
9
+ bulletList?: import("tailwind-merge").ClassNameValue;
7
10
  };
8
11
  };
9
12
  } | {
10
13
  [x: string]: {
11
14
  [x: string]: import("tailwind-merge").ClassNameValue | {
12
15
  base?: import("tailwind-merge").ClassNameValue;
16
+ title?: import("tailwind-merge").ClassNameValue;
13
17
  list?: import("tailwind-merge").ClassNameValue;
14
18
  icon?: import("tailwind-merge").ClassNameValue;
19
+ titleWrapper?: import("tailwind-merge").ClassNameValue;
20
+ bulletList?: import("tailwind-merge").ClassNameValue;
15
21
  };
16
22
  };
17
23
  } | {}, {
18
24
  base: string;
19
25
  list: string;
20
26
  icon: string;
27
+ titleWrapper: string;
28
+ title: string;
29
+ bulletList: string;
21
30
  }, undefined, {
22
31
  [key: string]: {
23
32
  [key: string]: import("tailwind-merge").ClassNameValue | {
24
33
  base?: import("tailwind-merge").ClassNameValue;
34
+ title?: import("tailwind-merge").ClassNameValue;
25
35
  list?: import("tailwind-merge").ClassNameValue;
26
36
  icon?: import("tailwind-merge").ClassNameValue;
37
+ titleWrapper?: import("tailwind-merge").ClassNameValue;
38
+ bulletList?: import("tailwind-merge").ClassNameValue;
27
39
  };
28
40
  };
29
41
  } | {}, {
30
42
  base: string;
31
43
  list: string;
32
44
  icon: string;
45
+ titleWrapper: string;
46
+ title: string;
47
+ bulletList: string;
33
48
  }, import("tailwind-variants").TVReturnType<unknown, {
34
49
  base: string;
35
50
  list: string;
36
51
  icon: string;
52
+ titleWrapper: string;
53
+ title: string;
54
+ bulletList: string;
37
55
  }, undefined, unknown, unknown, undefined>>;
@@ -3,6 +3,9 @@ export const styles = tv({
3
3
  slots: {
4
4
  base: 'flex items-start typography-body-11 text-text-danger',
5
5
  list: 'mb-2 flex flex-col gap-1',
6
- icon: 'mt-[0.25rem] mr-[0.5em] flex-shrink-0 align-top'
6
+ icon: 'mt-[0.25rem] mr-[0.5em] flex-shrink-0 align-top',
7
+ titleWrapper: 'mb-2 flex flex-col gap-1 text-text-danger',
8
+ title: 'flex items-start typography-body-11',
9
+ bulletList: 'text-text-danger'
7
10
  }
8
11
  });
@@ -5,6 +5,10 @@ export type ErrorMessageProps = {
5
5
  * Icon
6
6
  */
7
7
  icon?: (...args: unknown[]) => JSX.Element;
8
+ /**
9
+ * Title
10
+ */
11
+ errorTitle?: string;
8
12
  /**
9
13
  * Message or messages
10
14
  */
@@ -1,2 +1,2 @@
1
1
  import { type FieldProps } from './field.types.js';
2
- export declare function Field({ className, label, tag: Tag, children, hintMessage, errorMessage, labelElementType, labelSize, ...props }: FieldProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function Field({ className, label, tag: Tag, children, hintMessage, errorTitle, errorMessage, labelElementType, labelSize, ...props }: FieldProps): import("react/jsx-runtime").JSX.Element;
@@ -2,7 +2,7 @@
2
2
  import React, { Children, cloneElement, isValidElement, useCallback } from 'react';
3
3
  import { useField } from 'react-aria';
4
4
  import { ErrorMessage, Hint, Label } from '../index.js';
5
- export function Field({ className, label, tag: Tag = 'div', children, hintMessage, errorMessage, labelElementType, labelSize, ...props }) {
5
+ export function Field({ className, label, tag: Tag = 'div', children, hintMessage, errorTitle, errorMessage, labelElementType, labelSize, ...props }) {
6
6
  const { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField({
7
7
  ...props,
8
8
  description: hintMessage,
@@ -28,6 +28,7 @@ export function Field({ className, label, tag: Tag = 'div', children, hintMessag
28
28
  ...labelProps
29
29
  }, label), hintMessage && React.createElement(Hint, descriptionProps, hintMessage), errorMessage && React.createElement(ErrorMessage, {
30
30
  ...errorMessageProps,
31
+ errorTitle: errorTitle,
31
32
  message: errorMessage
32
33
  }), renderChildren());
33
34
  }
@@ -2,6 +2,10 @@ import { HTMLAttributes } from 'react';
2
2
  import { AriaFieldProps } from 'react-aria';
3
3
  import { HintProps, LabelProps } from '../index.js';
4
4
  export type FieldProps = {
5
+ /**
6
+ * error title
7
+ */
8
+ errorTitle?: string;
5
9
  /**
6
10
  * error message
7
11
  */
@@ -0,0 +1,2 @@
1
+ import { type IconProps } from '../icon.types.js';
2
+ export declare function FlagIcon({ look, 'aria-label': ariaLabel, copyrightYear, ...props }: IconProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { Icon } from '../icon.component.js';
3
+ export function FlagIcon({ look = 'filled', 'aria-label': ariaLabel = 'Flag', copyrightYear = '2026', ...props }) {
4
+ return React.createElement(Icon, {
5
+ "aria-label": ariaLabel,
6
+ copyrightYear: copyrightYear,
7
+ ...props
8
+ }, look === 'filled' ? React.createElement("path", {
9
+ d: "M4 22V2H20L18 7L20 12H6V22H4Z",
10
+ fill: "currentColor"
11
+ }) : React.createElement("path", {
12
+ d: "M4 22V2H20L18 7L20 12H6V22H4ZM5.95 10H17L16 7L17.05 4H6L5.95 10Z",
13
+ fill: "currentColor"
14
+ }));
15
+ }
@@ -98,6 +98,7 @@ export { FilterIcon } from './components/filter-icon.js';
98
98
  export { FingerprintIcon } from './components/fingerprint-icon.js';
99
99
  export { FirstAidCaseIcon } from './components/first-aid-case-icon.js';
100
100
  export { FirstAidIcon } from './components/first-aid-icon.js';
101
+ export { FlagIcon } from './components/flag-icon.js';
101
102
  export { FormatColorIcon } from './components/format-color-icon.js';
102
103
  export { FullscreenExitIcon } from './components/fullscreen-exit-icon.js';
103
104
  export { FullscreenIcon } from './components/fullscreen-icon.js';
@@ -98,6 +98,7 @@ export { FilterIcon } from './components/filter-icon.js';
98
98
  export { FingerprintIcon } from './components/fingerprint-icon.js';
99
99
  export { FirstAidCaseIcon } from './components/first-aid-case-icon.js';
100
100
  export { FirstAidIcon } from './components/first-aid-icon.js';
101
+ export { FlagIcon } from './components/flag-icon.js';
101
102
  export { FormatColorIcon } from './components/format-color-icon.js';
102
103
  export { FullscreenExitIcon } from './components/fullscreen-exit-icon.js';
103
104
  export { FullscreenIcon } from './components/fullscreen-icon.js';
@@ -1,2 +1,2 @@
1
1
  import { type InputGroupProps } from './input-group.types.js';
2
- export declare function InputGroup({ label, hideLabel, size, hint, errorMessage, supportingText, instanceId, after, before, children, tag: Tag, className, width, id: propID, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-label': ariaLabel, ...props }: InputGroupProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function InputGroup({ label, hideLabel, size, hint, errorMessage, errorTitle, supportingText, instanceId, after, before, children, tag: Tag, className, width, id: propID, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-label': ariaLabel, ...props }: InputGroupProps): import("react/jsx-runtime").JSX.Element;
@@ -6,7 +6,7 @@ import { ErrorMessage, Hint, Label } from '../index.js';
6
6
  import { InputGroupSupportingText } from './components/index.js';
7
7
  import { InputGroupAddOn } from './components/input-group-add-ons/input-group-add-ons.component.js';
8
8
  import { styles as inputGroupStyles } from './input-group.styles.js';
9
- export function InputGroup({ label, hideLabel, size = 'medium', hint, errorMessage, supportingText, instanceId, after, before, children, tag: Tag = 'div', className, width = 'full', id: propID, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-label': ariaLabel, ...props }) {
9
+ export function InputGroup({ label, hideLabel, size = 'medium', hint, errorMessage, errorTitle, supportingText, instanceId, after, before, children, tag: Tag = 'div', className, width = 'full', id: propID, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-label': ariaLabel, ...props }) {
10
10
  const _id = useId();
11
11
  const id = useMemo(()=>instanceId || `gel-field-${_id}`, [
12
12
  _id,
@@ -124,6 +124,7 @@ export function InputGroup({ label, hideLabel, size = 'medium', hint, errorMessa
124
124
  id: `${id}-hint`
125
125
  }, hint), errorMessage && React.createElement(ErrorMessage, {
126
126
  id: `${id}-error`,
127
+ errorTitle: errorTitle,
127
128
  message: errorMessage
128
129
  }), React.createElement("div", {
129
130
  className: styles.input()
@@ -37,6 +37,10 @@ export type InputGroupProps = {
37
37
  * Error message text
38
38
  */
39
39
  errorMessage?: string | string[];
40
+ /**
41
+ * Error title
42
+ */
43
+ errorTitle?: string;
40
44
  /**
41
45
  * Visually hide label
42
46
  */
@@ -1,4 +1,4 @@
1
1
  import React from 'react';
2
2
  import { SelectorButtonGroupContextState, SelectorButtonGroupProps } from './selector-button-group.types.js';
3
3
  export declare const SelectorButtonContext: React.Context<SelectorButtonGroupContextState>;
4
- export declare function SelectorButtonGroup({ className, children, label, orientation, errorMessage, description, value, isDisabled, ...props }: SelectorButtonGroupProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function SelectorButtonGroup({ className, children, label, orientation, errorMessage, description, value, onChange, isDisabled, ...props }: SelectorButtonGroupProps): import("react/jsx-runtime").JSX.Element;
@@ -11,25 +11,34 @@ export const SelectorButtonContext = createContext({
11
11
  validationState: 'valid',
12
12
  isDisabled: undefined
13
13
  });
14
- export function SelectorButtonGroup({ className, children, label, orientation = 'vertical', errorMessage, description, value = '', isDisabled, ...props }) {
14
+ export function SelectorButtonGroup({ className, children, label, orientation = 'vertical', errorMessage, description, value = '', onChange, isDisabled, ...props }) {
15
+ const isControlled = onChange !== undefined;
16
+ const onChangeCallback = onChange;
15
17
  const [selected, setSelected] = useState(value);
16
18
  const breakpoint = useBreakpoint();
17
19
  const resolvedOrientation = resolveResponsiveVariant(orientation, breakpoint);
18
20
  const handleChange = useCallback((id)=>{
19
- setSelected(id);
21
+ if (onChangeCallback) {
22
+ onChangeCallback(id);
23
+ } else {
24
+ setSelected(id);
25
+ }
20
26
  }, [
27
+ onChangeCallback,
21
28
  setSelected
22
29
  ]);
23
30
  const state = useMemo(()=>({
24
- value: selected,
31
+ value: isControlled ? value !== null && value !== void 0 ? value : '' : selected,
25
32
  onClick: (id)=>handleChange(id),
26
33
  validationState: errorMessage ? 'invalid' : 'valid',
27
34
  isDisabled
28
35
  }), [
29
36
  errorMessage,
30
37
  handleChange,
38
+ isControlled,
31
39
  isDisabled,
32
- selected
40
+ selected,
41
+ value
33
42
  ]);
34
43
  const { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField({
35
44
  validationState: state.validationState,
@@ -18,9 +18,14 @@ export type SelectorButtonGroupProps = {
18
18
  */
19
19
  tag?: keyof JSX.IntrinsicElements;
20
20
  /**
21
- * Key to set as default value
21
+ * Key to set as default value (uncontrolled) or currently selected value (controlled)
22
22
  */
23
23
  value?: string;
24
+ /**
25
+ * Called when selection changes. Providing this prop makes the component controlled.
26
+ * Pass an empty string to clear the selection.
27
+ */
28
+ onChange?: (value: string) => void;
24
29
  } & AriaFieldProps & Omit<HTMLAttributes<Element>, 'onChange'>;
25
30
  export type SelectorButtonGroupContextState = {
26
31
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westpac/ui",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -253,9 +253,9 @@
253
253
  "typescript": "^5.5.4",
254
254
  "vite": "^7.1.12",
255
255
  "vitest": "^3.2.4",
256
- "@westpac/style-config": "~1.0.2",
257
- "@westpac/test-config": "~0.0.0",
258
256
  "@westpac/eslint-config": "~1.1.0",
257
+ "@westpac/test-config": "~0.0.0",
258
+ "@westpac/style-config": "~1.0.2",
259
259
  "@westpac/ts-config": "~0.0.0"
260
260
  },
261
261
  "dependencies": {
@@ -48,7 +48,6 @@ function Autocomplete<T extends object>(
48
48
  }: AutocompleteProps<T>,
49
49
  ref: ForwardedRef<HTMLInputElement>,
50
50
  ) {
51
- // eslint-disable-next-line @typescript-eslint/unbound-method
52
51
  const { contains } = useFilter({ sensitivity: 'base' });
53
52
  const internalState = useComboBoxState({ isDisabled, ...props, defaultFilter: contains });
54
53
  const state = comboBoxState ?? internalState;
@@ -97,19 +96,13 @@ function Autocomplete<T extends object>(
97
96
  const outerRef = React.useRef(null);
98
97
 
99
98
  const isNoOptionPopOverOpen = useMemo(() => {
99
+ const inputLength = searchProps.value?.length ?? 0;
100
100
  return !!(
101
101
  noOptionsMessage &&
102
- ((!state.isOpen && state.isFocused && searchProps.value.length > 0 && state.selectedItems.length === 0) ||
103
- (state.collection.size === 0 && searchProps.value.length > 0))
102
+ ((!state.isOpen && state.isFocused && inputLength > 0 && !state.value) ||
103
+ (state.collection.size === 0 && inputLength > 0))
104
104
  );
105
- }, [
106
- noOptionsMessage,
107
- state.isOpen,
108
- state.isFocused,
109
- state.selectedItems,
110
- state.collection.size,
111
- searchProps.value.length,
112
- ]);
105
+ }, [noOptionsMessage, state.isOpen, state.isFocused, state.value, state.collection.size, searchProps.value]);
113
106
 
114
107
  return (
115
108
  <div className={styles.base({ className })}>
@@ -2,26 +2,50 @@ import { clsx } from 'clsx';
2
2
  import React, { ReactNode } from 'react';
3
3
 
4
4
  import { AlertIcon } from '../../components/icon/index.js';
5
+ import { List, ListItem } from '../../components/list/index.js';
5
6
 
6
7
  import { styles as errorMessageStyles } from './error-message.styles.js';
7
8
  import { type ErrorMessageProps } from './error-message.types.js';
8
9
 
9
- export function ErrorMessage({ className, tag: Tag = 'div', icon: Icon, message, ...props }: ErrorMessageProps) {
10
+ export function ErrorMessage({
11
+ className,
12
+ tag: Tag = 'div',
13
+ icon: Icon,
14
+ errorTitle,
15
+ message,
16
+ ...props
17
+ }: ErrorMessageProps) {
10
18
  const styles = errorMessageStyles({});
11
19
  const FinalIcon = Icon ?? AlertIcon;
12
20
 
21
+ if (errorTitle && Array.isArray(message)) {
22
+ return (
23
+ <div className={styles.titleWrapper({ className })} {...props}>
24
+ <span className={styles.title({})}>
25
+ <FinalIcon color="danger" copyrightYear="2026" className={styles.icon({})} size="xsmall" look="outlined" />
26
+ {errorTitle}
27
+ </span>
28
+ <List type="bullet" look="primary" className={styles.bulletList({})}>
29
+ {message.map((msg, index) => (
30
+ <ListItem key={index}>{msg}</ListItem>
31
+ ))}
32
+ </List>
33
+ </div>
34
+ );
35
+ }
36
+
13
37
  return Array.isArray(message) ? (
14
38
  <ul className={styles.list({})} {...props}>
15
39
  {message.map((msg, index) => (
16
40
  <li key={index} className={styles.base({ className })}>
17
- <FinalIcon color="danger" copyrightYear="2023" className={styles.icon({})} size="xsmall" look="outlined" />
41
+ <FinalIcon color="danger" copyrightYear="2026" className={styles.icon({})} size="xsmall" look="outlined" />
18
42
  {msg}
19
43
  </li>
20
44
  ))}
21
45
  </ul>
22
46
  ) : (
23
47
  <Tag className={styles.base({ className: clsx(className, 'mb-2') })} {...props}>
24
- <FinalIcon color="danger" copyrightYear="2023" className={styles.icon({})} size="xsmall" look="outlined" />
48
+ <FinalIcon color="danger" copyrightYear="2026" className={styles.icon({})} size="xsmall" look="outlined" />
25
49
  {message as ReactNode}
26
50
  </Tag>
27
51
  );
@@ -6,5 +6,8 @@ export const styles = tv({
6
6
  list: 'mb-2 flex flex-col gap-1',
7
7
  // below should be em rather than rem based on old GEL
8
8
  icon: 'mt-[0.25rem] mr-[0.5em] flex-shrink-0 align-top',
9
+ titleWrapper: 'mb-2 flex flex-col gap-1 text-text-danger',
10
+ title: 'flex items-start typography-body-11',
11
+ bulletList: 'text-text-danger',
9
12
  },
10
13
  });
@@ -6,6 +6,10 @@ export type ErrorMessageProps = {
6
6
  * Icon
7
7
  */
8
8
  icon?: (...args: unknown[]) => JSX.Element;
9
+ /**
10
+ * Title
11
+ */
12
+ errorTitle?: string;
9
13
  /**
10
14
  * Message or messages
11
15
  */
@@ -13,6 +13,7 @@ export function Field({
13
13
  tag: Tag = 'div',
14
14
  children,
15
15
  hintMessage,
16
+ errorTitle,
16
17
  errorMessage,
17
18
  labelElementType,
18
19
  labelSize,
@@ -42,7 +43,7 @@ export function Field({
42
43
  </Label>
43
44
  )}
44
45
  {hintMessage && <Hint {...descriptionProps}>{hintMessage}</Hint>}
45
- {errorMessage && <ErrorMessage {...errorMessageProps} message={errorMessage} />}
46
+ {errorMessage && <ErrorMessage {...errorMessageProps} errorTitle={errorTitle} message={errorMessage} />}
46
47
  {renderChildren()}
47
48
  </Tag>
48
49
  );
@@ -4,6 +4,10 @@ import { AriaFieldProps } from 'react-aria';
4
4
  import { HintProps, LabelProps } from '../index.js';
5
5
 
6
6
  export type FieldProps = {
7
+ /**
8
+ * error title
9
+ */
10
+ errorTitle?: string;
7
11
  /**
8
12
  * error message
9
13
  */
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ import { Icon } from '../icon.component.js';
4
+ import { type IconProps } from '../icon.types.js';
5
+
6
+ export function FlagIcon({
7
+ look = 'filled',
8
+ 'aria-label': ariaLabel = 'Flag',
9
+ copyrightYear = '2026',
10
+ ...props
11
+ }: IconProps) {
12
+ return (
13
+ <Icon aria-label={ariaLabel} copyrightYear={copyrightYear} {...props}>
14
+ {look === 'filled' ? (
15
+ <path d="M4 22V2H20L18 7L20 12H6V22H4Z" fill="currentColor" />
16
+ ) : (
17
+ <path d="M4 22V2H20L18 7L20 12H6V22H4ZM5.95 10H17L16 7L17.05 4H6L5.95 10Z" fill="currentColor" />
18
+ )}
19
+ </Icon>
20
+ );
21
+ }
@@ -98,6 +98,7 @@ export { FilterIcon } from './components/filter-icon.js';
98
98
  export { FingerprintIcon } from './components/fingerprint-icon.js';
99
99
  export { FirstAidCaseIcon } from './components/first-aid-case-icon.js';
100
100
  export { FirstAidIcon } from './components/first-aid-icon.js';
101
+ export { FlagIcon } from './components/flag-icon.js';
101
102
  export { FormatColorIcon } from './components/format-color-icon.js';
102
103
  export { FullscreenExitIcon } from './components/fullscreen-exit-icon.js';
103
104
  export { FullscreenIcon } from './components/fullscreen-icon.js';
@@ -26,6 +26,7 @@ export function InputGroup({
26
26
  size = 'medium',
27
27
  hint,
28
28
  errorMessage,
29
+ errorTitle,
29
30
  supportingText,
30
31
  instanceId,
31
32
  after,
@@ -135,7 +136,7 @@ export function InputGroup({
135
136
  </Label>
136
137
  )}
137
138
  {hint && <Hint id={`${id}-hint`}>{hint}</Hint>}
138
- {errorMessage && <ErrorMessage id={`${id}-error`} message={errorMessage} />}
139
+ {errorMessage && <ErrorMessage id={`${id}-error`} errorTitle={errorTitle} message={errorMessage} />}
139
140
  <div className={styles.input()}>
140
141
  {before && (
141
142
  <InputGroupAddOn position="before" size={resolvedSize} inset={beforeInset} icon={beforeIcon} id={id}>
@@ -34,6 +34,10 @@ export type InputGroupProps = {
34
34
  * Error message text
35
35
  */
36
36
  errorMessage?: string | string[];
37
+ /**
38
+ * Error title
39
+ */
40
+ errorTitle?: string;
37
41
  /**
38
42
  * Visually hide label
39
43
  */
@@ -25,28 +25,35 @@ export function SelectorButtonGroup({
25
25
  errorMessage,
26
26
  description,
27
27
  value = '',
28
+ onChange,
28
29
  isDisabled,
29
30
  ...props
30
31
  }: SelectorButtonGroupProps) {
32
+ const isControlled = onChange !== undefined;
33
+ const onChangeCallback = onChange as ((value: string) => void) | undefined;
31
34
  const [selected, setSelected] = useState(value);
32
35
  const breakpoint = useBreakpoint();
33
36
  const resolvedOrientation = resolveResponsiveVariant(orientation, breakpoint);
34
37
 
35
38
  const handleChange = useCallback(
36
39
  (id: string) => {
37
- setSelected(id);
40
+ if (onChangeCallback) {
41
+ onChangeCallback(id);
42
+ } else {
43
+ setSelected(id);
44
+ }
38
45
  },
39
- [setSelected],
46
+ [onChangeCallback, setSelected],
40
47
  );
41
48
 
42
49
  const state: SelectorButtonGroupContextState = useMemo(
43
50
  () => ({
44
- value: selected,
51
+ value: isControlled ? (value ?? '') : selected,
45
52
  onClick: (id: string) => handleChange(id),
46
53
  validationState: errorMessage ? 'invalid' : 'valid',
47
54
  isDisabled,
48
55
  }),
49
- [errorMessage, handleChange, isDisabled, selected],
56
+ [errorMessage, handleChange, isControlled, isDisabled, selected, value],
50
57
  );
51
58
 
52
59
  const { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField({
@@ -22,9 +22,14 @@ export type SelectorButtonGroupProps = {
22
22
  */
23
23
  tag?: keyof JSX.IntrinsicElements;
24
24
  /**
25
- * Key to set as default value
25
+ * Key to set as default value (uncontrolled) or currently selected value (controlled)
26
26
  */
27
27
  value?: string;
28
+ /**
29
+ * Called when selection changes. Providing this prop makes the component controlled.
30
+ * Pass an empty string to clear the selection.
31
+ */
32
+ onChange?: (value: string) => void;
28
33
  } & AriaFieldProps &
29
34
  Omit<HTMLAttributes<Element>, 'onChange'>;
30
35