@bpmn-io/form-js-viewer 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -3,7 +3,7 @@ import { isString, get, isNil, isObject, some, isNumber, set, findIndex, isArray
3
3
  import Big from 'big.js';
4
4
  import classNames from 'classnames';
5
5
  import { jsx, jsxs, Fragment } from 'preact/jsx-runtime';
6
- import { useContext, useMemo, useRef, useEffect, useState, useCallback, useLayoutEffect } from 'preact/hooks';
6
+ import { useContext, useMemo, useRef, useCallback, useEffect, useState, useLayoutEffect } from 'preact/hooks';
7
7
  import { createContext, createElement, Fragment as Fragment$1, render } from 'preact';
8
8
  import isEqual from 'lodash/isEqual';
9
9
  import flatpickr from 'flatpickr';
@@ -772,20 +772,59 @@ function useCondition(condition) {
772
772
  }, [conditionChecker, condition, expressionContextInfo]);
773
773
  }
774
774
 
775
- // parses the options data from the provided form field and form data
776
- function getOptionsData(formField, formData) {
775
+ /**
776
+ * Returns the options data for the provided if they can be simply determined, ignoring expression defined options.
777
+ *
778
+ * @param {object} formField
779
+ * @param {object} formData
780
+ */
781
+ function getSimpleOptionsData(formField, formData) {
777
782
  const {
783
+ valuesExpression: optionsExpression,
778
784
  valuesKey: optionsKey,
779
785
  values: staticOptions
780
786
  } = formField;
787
+ if (optionsExpression) {
788
+ return null;
789
+ }
781
790
  return optionsKey ? get(formData, [optionsKey]) : staticOptions;
782
791
  }
783
792
 
784
- // transforms the provided options into a normalized format, trimming invalid options
793
+ /**
794
+ * Normalizes the provided options data to a format that can be used by the select components.
795
+ * If the options data is not valid, it is filtered out.
796
+ *
797
+ * @param {any[]} optionsData
798
+ *
799
+ * @returns {object[]}
800
+ */
785
801
  function normalizeOptionsData(optionsData) {
786
802
  return optionsData.filter(_isAllowedValue).map(_normalizeOption).filter(o => !isNil(o));
787
803
  }
788
804
 
805
+ /**
806
+ * Creates an options object with default values if no options are provided.
807
+ *
808
+ * @param {object} options
809
+ *
810
+ * @returns {object}
811
+ */
812
+ function createEmptyOptions(options = {}) {
813
+ const defaults = {};
814
+
815
+ // provide default options if valuesKey and valuesExpression are not set
816
+ if (!options.valuesKey && !options.valuesExpression) {
817
+ defaults.values = [{
818
+ label: 'Value',
819
+ value: 'value'
820
+ }];
821
+ }
822
+ return {
823
+ ...defaults,
824
+ ...options
825
+ };
826
+ }
827
+
789
828
  /**
790
829
  * Converts the provided option to a normalized format.
791
830
  * If the option is not valid, null is returned.
@@ -836,21 +875,6 @@ function _isAllowedValue(value) {
836
875
  }
837
876
  return _isAllowedPrimitive(value);
838
877
  }
839
- function createEmptyOptions(options = {}) {
840
- const defaults = {};
841
-
842
- // provide default options if valuesKey and valuesExpression are not set
843
- if (!options.valuesKey && !options.valuesExpression) {
844
- defaults.values = [{
845
- label: 'Value',
846
- value: 'value'
847
- }];
848
- }
849
- return {
850
- ...defaults,
851
- ...options
852
- };
853
- }
854
878
 
855
879
  /**
856
880
  * Evaluate a string reactively based on the expressionLanguage and form data.
@@ -871,29 +895,19 @@ function useExpressionEvaluation(value) {
871
895
  }, [expressionLanguage, expressionContextInfo, value]);
872
896
  }
873
897
 
874
- function usePrevious(value, defaultValue = null) {
875
- const ref = useRef(defaultValue);
876
- useEffect(() => ref.current = value, [value]);
877
- return ref.current;
878
- }
879
-
880
898
  /**
881
899
  * A custom hook to manage state changes with deep comparison.
882
900
  *
883
- * @param {any} value - The current value to manage.
884
- * @param {any} defaultValue - The initial default value for the state.
885
- * @returns {any} - Returns the current state.
901
+ * @template T
902
+ * @param {T} value - The current value to manage.
903
+ * @returns {T} - Returns the current state.
886
904
  */
887
- function useDeepCompareState(value, defaultValue) {
888
- const [state, setState] = useState(defaultValue);
889
- const previous = usePrevious(value, defaultValue);
890
- const changed = !isEqual(previous, value);
891
- useEffect(() => {
892
- if (changed) {
893
- setState(value);
894
- }
895
- }, [changed, value]);
896
- return state;
905
+ function useDeepCompareMemoize(value) {
906
+ const ref = useRef();
907
+ if (!isEqual(value, ref.current)) {
908
+ ref.current = value;
909
+ }
910
+ return ref.current;
897
911
  }
898
912
 
899
913
  /**
@@ -923,15 +937,10 @@ function useOptionsAsync(field) {
923
937
  valuesKey: optionsKey,
924
938
  values: staticOptions
925
939
  } = field;
926
- const [optionsGetter, setOptionsGetter] = useState({
927
- options: [],
928
- error: undefined,
929
- loadState: LOAD_STATES.LOADING
930
- });
931
940
  const initialData = useService('form')._getState().initialData;
932
941
  const expressionEvaluation = useExpressionEvaluation(optionsExpression);
933
- const evaluatedOptions = useDeepCompareState(expressionEvaluation || [], []);
934
- useEffect(() => {
942
+ const evaluatedOptions = useDeepCompareMemoize(expressionEvaluation || []);
943
+ const optionsGetter = useMemo(() => {
935
944
  let options = [];
936
945
 
937
946
  // dynamic options
@@ -946,18 +955,16 @@ function useOptionsAsync(field) {
946
955
  options = Array.isArray(staticOptions) ? staticOptions : [];
947
956
 
948
957
  // expression
949
- } else if (optionsExpression) {
950
- if (evaluatedOptions && Array.isArray(evaluatedOptions)) {
951
- options = evaluatedOptions;
952
- }
958
+ } else if (optionsExpression && evaluatedOptions && Array.isArray(evaluatedOptions)) {
959
+ options = evaluatedOptions;
960
+
961
+ // error case
953
962
  } else {
954
- setOptionsGetter(buildErrorState('No options source defined in the form definition'));
955
- return;
963
+ return buildErrorState('No options source defined in the form definition');
956
964
  }
957
965
 
958
966
  // normalize data to support primitives and partially defined objects
959
- options = normalizeOptionsData(options);
960
- setOptionsGetter(buildLoadedState(options));
967
+ return buildLoadedState(normalizeOptionsData(options));
961
968
  }, [optionsKey, staticOptions, initialData, optionsExpression, evaluatedOptions]);
962
969
  return optionsGetter;
963
970
  }
@@ -1002,14 +1009,14 @@ const getDOMPurifyConfig = sanitizeStyleTags => {
1002
1009
  };
1003
1010
  };
1004
1011
 
1005
- /**
1006
- * A custom hook to build up security attributes from form configuration.
1007
- *
1008
- * @param {Object} security - The security configuration.
1009
- * @returns {Array} - Returns a tuple with sandbox and allow attributes.
1012
+ /**
1013
+ * A custom hook to build up security attributes from form configuration.
1014
+ *
1015
+ * @param {Object} security - The security configuration.
1016
+ * @returns {Array} - Returns a tuple with sandbox and allow attributes.
1010
1017
  */
1011
1018
  function useSecurityAttributesMap(security) {
1012
- const securityMemoized = useDeepCompareState(security);
1019
+ const securityMemoized = useDeepCompareMemoize(security);
1013
1020
  const sandbox = useMemo(() => SECURITY_ATTRIBUTES_DEFINITIONS.filter(({
1014
1021
  attribute
1015
1022
  }) => attribute === SANDBOX_ATTRIBUTE).filter(({
@@ -1181,6 +1188,12 @@ function useReadonly(formField, properties = {}) {
1181
1188
  return readonly || false;
1182
1189
  }
1183
1190
 
1191
+ function usePrevious(value, defaultValue = null) {
1192
+ const ref = useRef(defaultValue);
1193
+ useEffect(() => ref.current = value, [value]);
1194
+ return ref.current;
1195
+ }
1196
+
1184
1197
  function useFlushDebounce(func) {
1185
1198
  const timeoutRef = useRef(null);
1186
1199
  const lastArgsRef = useRef(null);
@@ -1445,8 +1458,16 @@ function sanitizeSingleSelectValue(options) {
1445
1458
  data,
1446
1459
  value
1447
1460
  } = options;
1461
+ const {
1462
+ valuesExpression: optionsExpression
1463
+ } = formField;
1448
1464
  try {
1449
- const validValues = normalizeOptionsData(getOptionsData(formField, data)).map(v => v.value);
1465
+ // if options are expression evaluated, we don't need to sanitize the value against the options
1466
+ // and defer to the field's internal validation
1467
+ if (optionsExpression) {
1468
+ return value;
1469
+ }
1470
+ const validValues = normalizeOptionsData(getSimpleOptionsData(formField, data)).map(v => v.value);
1450
1471
  return hasEqualValue(value, validValues) ? value : null;
1451
1472
  } catch (error) {
1452
1473
  // use default value in case of formatting error
@@ -1460,8 +1481,16 @@ function sanitizeMultiSelectValue(options) {
1460
1481
  data,
1461
1482
  value
1462
1483
  } = options;
1484
+ const {
1485
+ valuesExpression: optionsExpression
1486
+ } = formField;
1463
1487
  try {
1464
- const validValues = normalizeOptionsData(getOptionsData(formField, data)).map(v => v.value);
1488
+ // if options are expression evaluated, we don't need to sanitize the values against the options
1489
+ // and defer to the field's internal validation
1490
+ if (optionsExpression) {
1491
+ return value;
1492
+ }
1493
+ const validValues = normalizeOptionsData(getSimpleOptionsData(formField, data)).map(v => v.value);
1465
1494
  return value.filter(v => hasEqualValue(v, validValues));
1466
1495
  } catch (error) {
1467
1496
  // use default value in case of formatting error
@@ -1549,7 +1578,7 @@ function useCleanupMultiSelectValue(props) {
1549
1578
  onChange,
1550
1579
  values
1551
1580
  } = props;
1552
- const memoizedValues = useDeepCompareState(values, []);
1581
+ const memoizedValues = useDeepCompareMemoize(values || []);
1553
1582
 
1554
1583
  // ensures that the values are always a subset of the possible options
1555
1584
  useEffect(() => {
@@ -1569,7 +1598,8 @@ function useCleanupMultiSelectValue(props) {
1569
1598
 
1570
1599
  function Description(props) {
1571
1600
  const {
1572
- description
1601
+ description,
1602
+ id
1573
1603
  } = props;
1574
1604
  const evaluatedDescription = useSingleLineTemplateEvaluation(description || '', {
1575
1605
  debug: true
@@ -1578,6 +1608,7 @@ function Description(props) {
1578
1608
  return null;
1579
1609
  }
1580
1610
  return jsx("div", {
1611
+ id: id,
1581
1612
  class: "fjs-form-field-description",
1582
1613
  children: evaluatedDescription
1583
1614
  });
@@ -1608,6 +1639,7 @@ function Errors(props) {
1608
1639
  function Label(props) {
1609
1640
  const {
1610
1641
  id,
1642
+ htmlFor,
1611
1643
  label,
1612
1644
  collapseOnEmpty = true,
1613
1645
  required = false
@@ -1616,12 +1648,14 @@ function Label(props) {
1616
1648
  debug: true
1617
1649
  });
1618
1650
  return jsxs("label", {
1619
- for: id,
1651
+ id: id,
1652
+ for: htmlFor,
1620
1653
  class: classNames('fjs-form-field-label', {
1621
1654
  'fjs-incollapsible-label': !collapseOnEmpty
1622
1655
  }, props['class']),
1623
1656
  children: [props.children, evaluatedLabel, required && jsx("span", {
1624
1657
  class: "fjs-asterix",
1658
+ "aria-hidden": true,
1625
1659
  children: "*"
1626
1660
  })]
1627
1661
  });
@@ -1632,7 +1666,6 @@ function Checkbox(props) {
1632
1666
  const {
1633
1667
  disabled,
1634
1668
  errors = [],
1635
- errorMessageId,
1636
1669
  domId,
1637
1670
  onBlur,
1638
1671
  onFocus,
@@ -1656,6 +1689,8 @@ function Checkbox(props) {
1656
1689
  value: target.checked
1657
1690
  });
1658
1691
  };
1692
+ const descriptionId = `${domId}-description`;
1693
+ const errorMessageId = `${domId}-error-message`;
1659
1694
  return jsxs("div", {
1660
1695
  class: classNames(formFieldClasses(type$f, {
1661
1696
  errors,
@@ -1665,7 +1700,7 @@ function Checkbox(props) {
1665
1700
  'fjs-checked': value
1666
1701
  }),
1667
1702
  children: [jsx(Label, {
1668
- id: domId,
1703
+ htmlFor: domId,
1669
1704
  label: label,
1670
1705
  required: required,
1671
1706
  children: jsx("input", {
@@ -1678,13 +1713,16 @@ function Checkbox(props) {
1678
1713
  onChange: onChange,
1679
1714
  onBlur: () => onBlur && onBlur(),
1680
1715
  onFocus: () => onFocus && onFocus(),
1681
- "aria-describedby": errorMessageId
1716
+ required: required,
1717
+ "aria-invalid": errors.length > 0,
1718
+ "aria-describedby": [descriptionId, errorMessageId].join(' ')
1682
1719
  })
1683
1720
  }), jsx(Description, {
1721
+ id: descriptionId,
1684
1722
  description: description
1685
1723
  }), jsx(Errors, {
1686
- errors: errors,
1687
- id: errorMessageId
1724
+ id: errorMessageId,
1725
+ errors: errors
1688
1726
  })]
1689
1727
  });
1690
1728
  }
@@ -1707,7 +1745,6 @@ function Checklist(props) {
1707
1745
  const {
1708
1746
  disabled,
1709
1747
  errors = [],
1710
- errorMessageId,
1711
1748
  domId,
1712
1749
  onBlur,
1713
1750
  onFocus,
@@ -1754,6 +1791,8 @@ function Checklist(props) {
1754
1791
  values,
1755
1792
  onChange: props.onChange
1756
1793
  });
1794
+ const descriptionId = `${domId}-description`;
1795
+ const errorMessageId = `${domId}-error-message`;
1757
1796
  return jsxs("div", {
1758
1797
  class: classNames(formFieldClasses(type$e, {
1759
1798
  errors,
@@ -1768,7 +1807,7 @@ function Checklist(props) {
1768
1807
  const itemDomId = `${domId}-${index}`;
1769
1808
  const isChecked = hasEqualValue(o.value, values);
1770
1809
  return jsx(Label, {
1771
- id: itemDomId,
1810
+ htmlFor: itemDomId,
1772
1811
  label: o.label,
1773
1812
  class: classNames({
1774
1813
  'fjs-checked': isChecked
@@ -1784,14 +1823,17 @@ function Checklist(props) {
1784
1823
  onClick: () => toggleCheckbox(o.value),
1785
1824
  onBlur: onCheckboxBlur,
1786
1825
  onFocus: onCheckboxFocus,
1787
- "aria-describedby": errorMessageId
1826
+ required: required,
1827
+ "aria-invalid": errors.length > 0,
1828
+ "aria-describedby": [descriptionId, errorMessageId].join(' ')
1788
1829
  })
1789
1830
  });
1790
1831
  }), jsx(Description, {
1832
+ id: descriptionId,
1791
1833
  description: description
1792
1834
  }), jsx(Errors, {
1793
- errors: errors,
1794
- id: errorMessageId
1835
+ id: errorMessageId,
1836
+ errors: errors
1795
1837
  })]
1796
1838
  });
1797
1839
  }
@@ -1871,6 +1913,7 @@ function FormField(props) {
1871
1913
  }
1872
1914
  }, [viewerCommands, field, initialValue, initialValidationTrigger, indexes]);
1873
1915
  const onBlur = useCallback(() => {
1916
+ const value = get(data, valuePath);
1874
1917
  if (initialValidationTrigger) {
1875
1918
  setInitialValidationTrigger(false);
1876
1919
  viewerCommands.updateFieldValidation(field, value, indexes);
@@ -1878,7 +1921,7 @@ function FormField(props) {
1878
1921
  eventBus.fire('formField.blur', {
1879
1922
  formField: field
1880
1923
  });
1881
- }, [eventBus, field, indexes, value, viewerCommands, initialValidationTrigger]);
1924
+ }, [eventBus, field, indexes, viewerCommands, initialValidationTrigger, data, valuePath]);
1882
1925
  const onFocus = useCallback(() => {
1883
1926
  eventBus.fire('formField.focus', {
1884
1927
  formField: field
@@ -1902,7 +1945,6 @@ function FormField(props) {
1902
1945
  }
1903
1946
  const domId = `${prefixId(field.id, formId, indexes)}`;
1904
1947
  const fieldErrors = get(errors, [field.id, ...Object.values(indexes || {})]) || [];
1905
- const errorMessageId = errors.length === 0 ? undefined : `${domId}-error-message`;
1906
1948
  return jsx(Column, {
1907
1949
  field: field,
1908
1950
  class: gridColumnClasses(field),
@@ -1913,7 +1955,6 @@ function FormField(props) {
1913
1955
  ...props,
1914
1956
  disabled: disabled,
1915
1957
  errors: fieldErrors,
1916
- errorMessageId: errorMessageId,
1917
1958
  domId: domId,
1918
1959
  onChange: disabled || readonly ? noop$1 : onChangeIndexed,
1919
1960
  onBlur: disabled || readonly ? noop$1 : onBlur,
@@ -2225,7 +2266,7 @@ function Datepicker(props) {
2225
2266
  const [forceFocusCalendar, setForceFocusCalendar] = useState(false);
2226
2267
 
2227
2268
  // ensures we render based on date value instead of reference
2228
- const date = useDeepCompareState(dateObject, null);
2269
+ const date = useDeepCompareMemoize(dateObject);
2229
2270
 
2230
2271
  // shorts the date value back to the source
2231
2272
  useEffect(() => {
@@ -2316,7 +2357,7 @@ function Datepicker(props) {
2316
2357
  return jsxs("div", {
2317
2358
  class: "fjs-datetime-subsection",
2318
2359
  children: [jsx(Label, {
2319
- id: domId,
2360
+ htmlFor: domId,
2320
2361
  label: label,
2321
2362
  collapseOnEmpty: collapseLabelOnEmpty,
2322
2363
  required: required
@@ -2588,7 +2629,7 @@ function Timepicker(props) {
2588
2629
  return jsxs("div", {
2589
2630
  class: "fjs-datetime-subsection",
2590
2631
  children: [jsx(Label, {
2591
- id: domId,
2632
+ htmlFor: domId,
2592
2633
  label: label,
2593
2634
  collapseOnEmpty: collapseLabelOnEmpty,
2594
2635
  required: required
@@ -2775,6 +2816,7 @@ function Datetime(props) {
2775
2816
  });
2776
2817
  }, []);
2777
2818
  const errorMessageId = allErrors.length === 0 ? undefined : `${prefixId(id, formId)}-error-message`;
2819
+ const descriptionId = `${prefixId(id, formId)}-description`;
2778
2820
  const datePickerProps = {
2779
2821
  label: dateLabel,
2780
2822
  collapseLabelOnEmpty: !timeLabel,
@@ -2787,7 +2829,7 @@ function Datetime(props) {
2787
2829
  date: dateTime.date,
2788
2830
  readonly,
2789
2831
  setDate,
2790
- 'aria-describedby': errorMessageId
2832
+ 'aria-describedby': [descriptionId, errorMessageId].join(' ')
2791
2833
  };
2792
2834
  const timePickerProps = {
2793
2835
  label: timeLabel,
@@ -2802,7 +2844,7 @@ function Datetime(props) {
2802
2844
  timeInterval,
2803
2845
  time: dateTime.time,
2804
2846
  setTime,
2805
- 'aria-describedby': errorMessageId
2847
+ 'aria-describedby': [descriptionId, errorMessageId].join(' ')
2806
2848
  };
2807
2849
  return jsxs("div", {
2808
2850
  class: formFieldClasses(type$d, {
@@ -2821,6 +2863,7 @@ function Datetime(props) {
2821
2863
  ...timePickerProps
2822
2864
  })]
2823
2865
  }), jsx(Description, {
2866
+ id: descriptionId,
2824
2867
  description: description
2825
2868
  }), jsx(Errors, {
2826
2869
  errors: allErrors,
@@ -2924,7 +2967,7 @@ function IFrame(props) {
2924
2967
  readonly
2925
2968
  }),
2926
2969
  children: [jsx(Label, {
2927
- id: domId,
2970
+ htmlFor: domId,
2928
2971
  label: evaluatedLabel
2929
2972
  }), !evaluatedUrl && jsx(IFramePlaceholder, {
2930
2973
  text: "No content to show."
@@ -3470,7 +3513,6 @@ function Numberfield(props) {
3470
3513
  const {
3471
3514
  disabled,
3472
3515
  errors = [],
3473
- errorMessageId,
3474
3516
  domId,
3475
3517
  onBlur,
3476
3518
  onFocus,
@@ -3608,6 +3650,8 @@ function Numberfield(props) {
3608
3650
  e.preventDefault();
3609
3651
  }
3610
3652
  };
3653
+ const descriptionId = `${domId}-description`;
3654
+ const errorMessageId = `${domId}-error-message`;
3611
3655
  return jsxs("div", {
3612
3656
  class: formFieldClasses(type$a, {
3613
3657
  errors,
@@ -3615,7 +3659,7 @@ function Numberfield(props) {
3615
3659
  readonly
3616
3660
  }),
3617
3661
  children: [jsx(Label, {
3618
- id: domId,
3662
+ htmlFor: domId,
3619
3663
  label: label,
3620
3664
  required: required
3621
3665
  }), jsx(TemplatedInputAdorner, {
@@ -3649,15 +3693,17 @@ function Numberfield(props) {
3649
3693
  autoComplete: "off",
3650
3694
  step: incrementAmount,
3651
3695
  value: displayValue,
3652
- "aria-describedby": errorMessageId
3696
+ "aria-describedby": [descriptionId, errorMessageId].join(' '),
3697
+ required: required,
3698
+ "aria-invalid": errors.length > 0
3653
3699
  }), jsxs("div", {
3654
3700
  class: classNames('fjs-number-arrow-container', {
3655
3701
  'fjs-disabled': disabled,
3656
3702
  'fjs-readonly': readonly
3657
3703
  }),
3658
3704
  children: [jsx("button", {
3659
- class: "fjs-number-arrow-up",
3660
3705
  type: "button",
3706
+ class: "fjs-number-arrow-up",
3661
3707
  "aria-label": "Increment",
3662
3708
  onClick: () => increment(),
3663
3709
  tabIndex: -1,
@@ -3665,8 +3711,8 @@ function Numberfield(props) {
3665
3711
  }), jsx("div", {
3666
3712
  class: "fjs-number-arrow-separator"
3667
3713
  }), jsx("button", {
3668
- class: "fjs-number-arrow-down",
3669
3714
  type: "button",
3715
+ class: "fjs-number-arrow-down",
3670
3716
  "aria-label": "Decrement",
3671
3717
  onClick: () => decrement(),
3672
3718
  tabIndex: -1,
@@ -3675,10 +3721,11 @@ function Numberfield(props) {
3675
3721
  })]
3676
3722
  })
3677
3723
  }), jsx(Description, {
3724
+ id: descriptionId,
3678
3725
  description: description
3679
3726
  }), jsx(Errors, {
3680
- errors: errors,
3681
- id: errorMessageId
3727
+ id: errorMessageId,
3728
+ errors: errors
3682
3729
  })]
3683
3730
  });
3684
3731
  }
@@ -3708,7 +3755,6 @@ function Radio(props) {
3708
3755
  const {
3709
3756
  disabled,
3710
3757
  errors = [],
3711
- errorMessageId,
3712
3758
  domId,
3713
3759
  onBlur,
3714
3760
  onFocus,
@@ -3754,6 +3800,8 @@ function Radio(props) {
3754
3800
  value,
3755
3801
  onChange: props.onChange
3756
3802
  });
3803
+ const descriptionId = `${domId}-description`;
3804
+ const errorMessageId = `${domId}-error-message`;
3757
3805
  return jsxs("div", {
3758
3806
  class: formFieldClasses(type$9, {
3759
3807
  errors,
@@ -3768,7 +3816,7 @@ function Radio(props) {
3768
3816
  const itemDomId = `${domId}-${index}`;
3769
3817
  const isChecked = isEqual(option.value, value);
3770
3818
  return jsx(Label, {
3771
- id: itemDomId,
3819
+ htmlFor: itemDomId,
3772
3820
  label: option.label,
3773
3821
  class: classNames({
3774
3822
  'fjs-checked': isChecked
@@ -3784,14 +3832,17 @@ function Radio(props) {
3784
3832
  onClick: () => onChange(option.value),
3785
3833
  onBlur: onRadioBlur,
3786
3834
  onFocus: onRadioFocus,
3787
- "aria-describedby": errorMessageId
3835
+ "aria-describedby": [descriptionId, errorMessageId].join(' '),
3836
+ required: required,
3837
+ "aria-invalid": errors.length > 0
3788
3838
  })
3789
3839
  }, index);
3790
3840
  }), jsx(Description, {
3841
+ id: descriptionId,
3791
3842
  description: description
3792
3843
  }), jsx(Errors, {
3793
- errors: errors,
3794
- id: errorMessageId
3844
+ id: errorMessageId,
3845
+ errors: errors
3795
3846
  })]
3796
3847
  });
3797
3848
  }
@@ -3856,7 +3907,7 @@ function SearchableSelect(props) {
3856
3907
 
3857
3908
  // whenever we change the underlying value, set the label to it
3858
3909
  useEffect(() => {
3859
- setFilter(label);
3910
+ setFilter(label || '');
3860
3911
  }, [label]);
3861
3912
  const filteredOptions = useMemo(() => {
3862
3913
  if (loadState !== LOAD_STATES.LOADED) {
@@ -3936,7 +3987,7 @@ function SearchableSelect(props) {
3936
3987
  }, [onFocus]);
3937
3988
  const onInputBlur = useCallback(() => {
3938
3989
  setIsDropdownExpanded(false);
3939
- setFilter(label);
3990
+ setFilter(label || '');
3940
3991
  onBlur && onBlur();
3941
3992
  }, [onBlur, label]);
3942
3993
  return jsxs(Fragment, {
@@ -4032,6 +4083,9 @@ function SimpleSelect(props) {
4032
4083
  }, [disabled, isDropdownExpanded, loadState, readonly, value]);
4033
4084
  const onMouseDown = useCallback(e => {
4034
4085
  const input = inputRef.current;
4086
+ if (disabled || !input) {
4087
+ return;
4088
+ }
4035
4089
  setIsDropdownExpanded(!isDropdownExpanded);
4036
4090
  if (isDropdownExpanded) {
4037
4091
  input.blur();
@@ -4039,7 +4093,7 @@ function SimpleSelect(props) {
4039
4093
  input.focus();
4040
4094
  }
4041
4095
  e.preventDefault();
4042
- }, [isDropdownExpanded]);
4096
+ }, [disabled, isDropdownExpanded]);
4043
4097
  const initialFocusIndex = useMemo(() => value && findIndex(options, o => o.value === value) || 0, [options, value]);
4044
4098
  const onInputFocus = useCallback(() => {
4045
4099
  if (!readonly) {
@@ -4111,7 +4165,6 @@ function Select(props) {
4111
4165
  const {
4112
4166
  disabled,
4113
4167
  errors = [],
4114
- errorMessageId,
4115
4168
  domId,
4116
4169
  onBlur,
4117
4170
  onFocus,
@@ -4129,6 +4182,8 @@ function Select(props) {
4129
4182
  const {
4130
4183
  required
4131
4184
  } = validate;
4185
+ const descriptionId = `${domId}-description`;
4186
+ const errorMessageId = `${domId}-error-message`;
4132
4187
  const selectProps = {
4133
4188
  domId,
4134
4189
  disabled,
@@ -4139,7 +4194,9 @@ function Select(props) {
4139
4194
  value,
4140
4195
  onChange,
4141
4196
  readonly,
4142
- 'aria-describedby': errorMessageId
4197
+ required,
4198
+ 'aria-invalid': errors.length > 0,
4199
+ 'aria-describedby': [descriptionId, errorMessageId].join(' ')
4143
4200
  };
4144
4201
  return jsxs("div", {
4145
4202
  class: formFieldClasses(type$8, {
@@ -4154,7 +4211,7 @@ function Select(props) {
4154
4211
  }
4155
4212
  },
4156
4213
  children: [jsx(Label, {
4157
- id: domId,
4214
+ htmlFor: domId,
4158
4215
  label: label,
4159
4216
  required: required
4160
4217
  }), searchable ? jsx(SearchableSelect, {
@@ -4162,10 +4219,11 @@ function Select(props) {
4162
4219
  }) : jsx(SimpleSelect, {
4163
4220
  ...selectProps
4164
4221
  }), jsx(Description, {
4222
+ id: descriptionId,
4165
4223
  description: description
4166
4224
  }), jsx(Errors, {
4167
- errors: errors,
4168
- id: errorMessageId
4225
+ id: errorMessageId,
4226
+ errors: errors
4169
4227
  })]
4170
4228
  });
4171
4229
  }
@@ -4298,7 +4356,6 @@ function Taglist(props) {
4298
4356
  const {
4299
4357
  disabled,
4300
4358
  errors = [],
4301
- errorMessageId,
4302
4359
  onFocus,
4303
4360
  domId,
4304
4361
  onBlur,
@@ -4326,7 +4383,7 @@ function Taglist(props) {
4326
4383
  } = useOptionsAsync(field);
4327
4384
 
4328
4385
  // ensures we render based on array content instead of reference
4329
- const values = useDeepCompareState(value || [], []);
4386
+ const values = useDeepCompareMemoize(value || []);
4330
4387
  useCleanupMultiSelectValue({
4331
4388
  field,
4332
4389
  loadState,
@@ -4434,6 +4491,8 @@ function Taglist(props) {
4434
4491
  inputRef.current.focus();
4435
4492
  };
4436
4493
  const shouldDisplayDropdown = useMemo(() => !disabled && loadState === LOAD_STATES.LOADED && isDropdownExpanded && !isEscapeClosed, [disabled, isDropdownExpanded, isEscapeClosed, loadState]);
4494
+ const descriptionId = `${domId}-description`;
4495
+ const errorMessageId = `${domId}-error-message`;
4437
4496
  return jsxs("div", {
4438
4497
  ref: focusScopeRef,
4439
4498
  class: formFieldClasses(type$5, {
@@ -4450,7 +4509,7 @@ function Taglist(props) {
4450
4509
  children: [jsx(Label, {
4451
4510
  label: label,
4452
4511
  required: required,
4453
- id: domId
4512
+ htmlFor: domId
4454
4513
  }), !disabled && !readonly && !!values.length && jsx(SkipLink, {
4455
4514
  className: "fjs-taglist-skip-link",
4456
4515
  label: "Skip to search",
@@ -4498,7 +4557,9 @@ function Taglist(props) {
4498
4557
  onMouseDown: () => setIsEscapeClose(false),
4499
4558
  onFocus: onInputFocus,
4500
4559
  onBlur: onInputBlur,
4501
- "aria-describedby": errorMessageId
4560
+ "aria-describedby": [descriptionId, errorMessageId].join(' '),
4561
+ required: required,
4562
+ "aria-invalid": errors.length > 0
4502
4563
  })]
4503
4564
  }), jsx("div", {
4504
4565
  class: "fjs-taglist-anchor",
@@ -4510,10 +4571,11 @@ function Taglist(props) {
4510
4571
  listenerElement: inputRef.current
4511
4572
  })
4512
4573
  }), jsx(Description, {
4574
+ id: descriptionId,
4513
4575
  description: description
4514
4576
  }), jsx(Errors, {
4515
- errors: errors,
4516
- id: errorMessageId
4577
+ id: errorMessageId,
4578
+ errors: errors
4517
4579
  })]
4518
4580
  });
4519
4581
  }
@@ -4830,7 +4892,6 @@ function Textfield(props) {
4830
4892
  const {
4831
4893
  disabled,
4832
4894
  errors = [],
4833
- errorMessageId,
4834
4895
  domId,
4835
4896
  onBlur,
4836
4897
  onFocus,
@@ -4866,6 +4927,8 @@ function Textfield(props) {
4866
4927
  const onInputFocus = () => {
4867
4928
  onFocus && onFocus();
4868
4929
  };
4930
+ const descriptionId = `${domId}-description`;
4931
+ const errorMessageId = `${domId}-error-message`;
4869
4932
  return jsxs("div", {
4870
4933
  class: formFieldClasses(type$2, {
4871
4934
  errors,
@@ -4873,7 +4936,7 @@ function Textfield(props) {
4873
4936
  readonly
4874
4937
  }),
4875
4938
  children: [jsx(Label, {
4876
- id: domId,
4939
+ htmlFor: domId,
4877
4940
  label: label,
4878
4941
  required: required
4879
4942
  }), jsx(TemplatedInputAdorner, {
@@ -4891,13 +4954,16 @@ function Textfield(props) {
4891
4954
  onFocus: onInputFocus,
4892
4955
  type: "text",
4893
4956
  value: value,
4894
- "aria-describedby": errorMessageId
4957
+ "aria-describedby": [descriptionId, errorMessageId].join(' '),
4958
+ required: required,
4959
+ "aria-invalid": errors.length > 0
4895
4960
  })
4896
4961
  }), jsx(Description, {
4962
+ id: descriptionId,
4897
4963
  description: description
4898
4964
  }), jsx(Errors, {
4899
- errors: errors,
4900
- id: errorMessageId
4965
+ id: errorMessageId,
4966
+ errors: errors
4901
4967
  })]
4902
4968
  });
4903
4969
  }
@@ -4930,7 +4996,6 @@ function Textarea(props) {
4930
4996
  const {
4931
4997
  disabled,
4932
4998
  errors = [],
4933
- errorMessageId,
4934
4999
  domId,
4935
5000
  onBlur,
4936
5001
  onFocus,
@@ -4974,6 +5039,8 @@ function Textarea(props) {
4974
5039
  useEffect(() => {
4975
5040
  autoSizeTextarea(textareaRef.current);
4976
5041
  }, []);
5042
+ const descriptionId = `${domId}-description`;
5043
+ const errorMessageId = `${domId}-error-message`;
4977
5044
  return jsxs("div", {
4978
5045
  class: formFieldClasses(type$1, {
4979
5046
  errors,
@@ -4981,7 +5048,7 @@ function Textarea(props) {
4981
5048
  readonly
4982
5049
  }),
4983
5050
  children: [jsx(Label, {
4984
- id: domId,
5051
+ htmlFor: domId,
4985
5052
  label: label,
4986
5053
  required: required
4987
5054
  }), jsx("textarea", {
@@ -4994,12 +5061,15 @@ function Textarea(props) {
4994
5061
  onFocus: onInputFocus,
4995
5062
  value: value,
4996
5063
  ref: textareaRef,
4997
- "aria-describedby": errorMessageId
5064
+ "aria-describedby": [descriptionId, errorMessageId].join(' '),
5065
+ required: required,
5066
+ "aria-invalid": errors.length > 0
4998
5067
  }), jsx(Description, {
5068
+ id: descriptionId,
4999
5069
  description: description
5000
5070
  }), jsx(Errors, {
5001
- errors: errors,
5002
- id: errorMessageId
5071
+ id: errorMessageId,
5072
+ errors: errors
5003
5073
  })]
5004
5074
  });
5005
5075
  }
@@ -5168,7 +5238,7 @@ function Table(props) {
5168
5238
  return jsxs("div", {
5169
5239
  class: formFieldClasses(type),
5170
5240
  children: [jsx(Label, {
5171
- id: prefixId(id),
5241
+ htmlFor: prefixId(id),
5172
5242
  label: label
5173
5243
  }), jsxs("div", {
5174
5244
  class: classNames('fjs-table-middle-container', {
@@ -5568,7 +5638,7 @@ class FormFields {
5568
5638
  }
5569
5639
 
5570
5640
  const EXPRESSION_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'conditional.hide', 'description', 'label', 'source', 'readonly', 'text', 'validate.min', 'validate.max', 'validate.minLength', 'validate.maxLength', 'valuesExpression', 'url', 'dataSource', 'columnsExpression'];
5571
- const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text', 'url'];
5641
+ const TEMPLATE_PROPERTIES = ['alt', 'appearance.prefixAdorner', 'appearance.suffixAdorner', 'description', 'label', 'source', 'text', 'content', 'url'];
5572
5642
 
5573
5643
  /**
5574
5644
  * @typedef { import('../types').Schema } Schema
@@ -6554,47 +6624,17 @@ class RepeatRenderManager {
6554
6624
  };
6555
6625
  const parentExpressionContextInfo = useContext(LocalExpressionContext);
6556
6626
  return jsx(Fragment, {
6557
- children: displayValues.map((value, index) => {
6558
- const elementProps = useMemo(() => ({
6559
- ...restProps,
6560
- indexes: {
6561
- ...(indexes || {}),
6562
- [repeaterField.id]: index
6563
- }
6564
- }), [index]);
6565
- const localExpressionContextInfo = useMemo(() => ({
6566
- data: parentExpressionContextInfo.data,
6567
- this: value,
6568
- parent: buildExpressionContext(parentExpressionContextInfo),
6569
- i: [...parentExpressionContextInfo.i, index + 1]
6570
- }), [index, value]);
6571
- return !showRemove ? jsx(LocalExpressionContext.Provider, {
6572
- value: localExpressionContextInfo,
6573
- children: jsx(RowsRenderer, {
6574
- ...elementProps
6575
- })
6576
- }) : jsxs("div", {
6577
- class: "fjs-repeat-row-container",
6578
- children: [jsx("div", {
6579
- class: "fjs-repeat-row-rows",
6580
- children: jsx(LocalExpressionContext.Provider, {
6581
- value: localExpressionContextInfo,
6582
- children: jsx(RowsRenderer, {
6583
- ...elementProps
6584
- })
6585
- })
6586
- }), jsx("button", {
6587
- class: "fjs-repeat-row-remove",
6588
- type: "button",
6589
- "aria-label": `Remove list item ${index + 1}`,
6590
- onClick: () => onDeleteItem(index),
6591
- children: jsx("div", {
6592
- class: "fjs-repeat-row-remove-icon-container",
6593
- children: jsx(DeleteSvg, {})
6594
- })
6595
- })]
6596
- });
6597
- })
6627
+ children: displayValues.map((value, index) => jsx(RepetitionScaffold, {
6628
+ index: index,
6629
+ value: value,
6630
+ parentExpressionContextInfo: parentExpressionContextInfo,
6631
+ repeaterField: repeaterField,
6632
+ RowsRenderer: RowsRenderer,
6633
+ indexes: indexes,
6634
+ onDeleteItem: onDeleteItem,
6635
+ showRemove: showRemove,
6636
+ ...restProps
6637
+ }, index))
6598
6638
  });
6599
6639
  }
6600
6640
  RepeatFooter(props) {
@@ -6657,18 +6697,18 @@ class RepeatRenderManager {
6657
6697
  'fjs-remove-allowed': repeaterField.allowAddRemove
6658
6698
  }),
6659
6699
  children: [showAdd ? jsx("button", {
6700
+ type: "button",
6660
6701
  readOnly: readonly,
6661
6702
  disabled: disabled || readonly,
6662
6703
  class: "fjs-repeat-render-add",
6663
- type: "button",
6664
6704
  ref: addButtonRef,
6665
6705
  onClick: onAddItem,
6666
6706
  children: jsxs(Fragment, {
6667
6707
  children: [jsx(AddSvg, {}), " ", 'Add new']
6668
6708
  })
6669
6709
  }) : null, collapseEnabled ? jsx("button", {
6670
- class: "fjs-repeat-render-collapse",
6671
6710
  type: "button",
6711
+ class: "fjs-repeat-render-collapse",
6672
6712
  onClick: toggle,
6673
6713
  children: isCollapsed ? jsxs(Fragment, {
6674
6714
  children: [jsx(ExpandSvg, {}), " ", `Expand all (${values.length})`]
@@ -6686,6 +6726,73 @@ class RepeatRenderManager {
6686
6726
  return nonCollapsedItems ? nonCollapsedItems : DEFAULT_NON_COLLAPSED_ITEMS;
6687
6727
  }
6688
6728
  }
6729
+
6730
+ /**
6731
+ * Individual repetition of a repeated field and context scaffolding.
6732
+ *
6733
+ * @param {Object} props
6734
+ * @param {number} props.index
6735
+ * @param {Object} props.value
6736
+ * @param {Object} props.parentExpressionContextInfo
6737
+ * @param {Object} props.repeaterField
6738
+ * @param {Function} props.RowsRenderer
6739
+ * @param {Object} props.indexes
6740
+ * @param {Function} props.onDeleteItem
6741
+ * @param {boolean} props.showRemove
6742
+ */
6743
+
6744
+ const RepetitionScaffold = props => {
6745
+ const {
6746
+ index,
6747
+ value,
6748
+ parentExpressionContextInfo,
6749
+ repeaterField,
6750
+ RowsRenderer,
6751
+ indexes,
6752
+ onDeleteItem,
6753
+ showRemove,
6754
+ ...restProps
6755
+ } = props;
6756
+ const elementProps = useMemo(() => ({
6757
+ ...restProps,
6758
+ indexes: {
6759
+ ...(indexes || {}),
6760
+ [repeaterField.id]: index
6761
+ }
6762
+ }), [index, indexes, repeaterField.id, restProps]);
6763
+ const localExpressionContextInfo = useMemo(() => ({
6764
+ data: parentExpressionContextInfo.data,
6765
+ this: value,
6766
+ parent: buildExpressionContext(parentExpressionContextInfo),
6767
+ i: [...parentExpressionContextInfo.i, index + 1]
6768
+ }), [index, parentExpressionContextInfo, value]);
6769
+ return !showRemove ? jsx(LocalExpressionContext.Provider, {
6770
+ value: localExpressionContextInfo,
6771
+ children: jsx(RowsRenderer, {
6772
+ ...elementProps
6773
+ })
6774
+ }) : jsxs("div", {
6775
+ class: "fjs-repeat-row-container",
6776
+ children: [jsx("div", {
6777
+ class: "fjs-repeat-row-rows",
6778
+ children: jsx(LocalExpressionContext.Provider, {
6779
+ value: localExpressionContextInfo,
6780
+ children: jsx(RowsRenderer, {
6781
+ ...elementProps
6782
+ })
6783
+ })
6784
+ }), jsx("button", {
6785
+ type: "button",
6786
+ class: "fjs-repeat-row-remove",
6787
+ "aria-label": `Remove list item ${index + 1}`,
6788
+ onClick: () => onDeleteItem(index),
6789
+ children: jsx("div", {
6790
+ class: "fjs-repeat-row-remove-icon-container",
6791
+ children: jsx(DeleteSvg, {})
6792
+ })
6793
+ })]
6794
+ });
6795
+ };
6689
6796
  RepeatRenderManager.$inject = ['form', 'formFields', 'formFieldRegistry', 'pathRegistry'];
6690
6797
 
6691
6798
  const RepeatRenderModule = {