@abgov/jsonforms-components 2.74.1 → 2.75.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/index.esm.js CHANGED
@@ -4821,6 +4821,166 @@ const UTCToFullLocalTime = (fullTime, useUTC = false) => {
4821
4821
  }).replace('p.m.', 'PM').replace('a.m.', 'AM');
4822
4822
  };
4823
4823
 
4824
+ // Default formats keyed by type; the mask can be overridden per field via UI schema options.
4825
+ const DEFAULT_PATTERNS = {
4826
+ phone: {
4827
+ mask: '(###) ###-####',
4828
+ pattern: /^\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}$/,
4829
+ error: 'Must be a valid 10-digit phone number in format (000) 000-0000',
4830
+ allowedKeys: /[0-9]/
4831
+ },
4832
+ sin: {
4833
+ mask: '### ### ###',
4834
+ pattern: /^\d{3} \d{3} \d{3}$/,
4835
+ error: 'Must be three groups of three digits.',
4836
+ allowedKeys: /[0-9]/
4837
+ },
4838
+ postalCode: {
4839
+ mask: '### ###',
4840
+ pattern: /^[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d$/,
4841
+ error: 'Must be in A0A 0A0 capital letters and numbers format',
4842
+ allowedKeys: /[A-Za-z0-9]/
4843
+ },
4844
+ driverId: {
4845
+ // Alberta driver's licence: six digits, a dash, then three digits.
4846
+ mask: '######-###',
4847
+ pattern: /^\d{6}-\d{3}$/,
4848
+ error: "Must be a valid driver's licence number in format 000000-000",
4849
+ allowedKeys: /[0-9]/
4850
+ },
4851
+ mvid: {
4852
+ // Motor vehicle ID: four digits, a dash, then five digits.
4853
+ mask: '####-#####',
4854
+ pattern: /^\d{4}-\d{5}$/,
4855
+ error: 'Must be a valid motor vehicle ID in format 0000-00000',
4856
+ allowedKeys: /[0-9]/
4857
+ }
4858
+ };
4859
+ // Mask placeholder characters; each consumes one input character. Any other mask character is treated as a literal.
4860
+ const MASK_PLACEHOLDERS = new Set(['#', '*', '_']);
4861
+ const isPlaceholder = char => MASK_PLACEHOLDERS.has(char);
4862
+ // Navigation/editing keys that keystroke filtering must always let through.
4863
+ const MASK_CONTROL_KEYS = new Set(['Backspace', 'Delete', 'Tab', 'Enter', 'Escape', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
4864
+ // A single-character key should be blocked when the format restricts input and the key isn't allowed.
4865
+ const shouldBlockKey = (key, allowedKeys) => !!allowedKeys && key.length === 1 && !MASK_CONTROL_KEYS.has(key) && !allowedKeys.test(key);
4866
+ // Removes characters the format doesn't allow (e.g. letters from a digit-only field), for paste/programmatic input.
4867
+ const filterAllowedKeys = (value, allowedKeys) => allowedKeys ? [...(value != null ? value : '')].filter(char => allowedKeys.test(char)).join('') : value != null ? value : '';
4868
+ // A mask slot holds one content character (letter or digit); everything else in the value is a separator.
4869
+ const NON_CONTENT = /[^a-zA-Z0-9]/g;
4870
+ const IS_CONTENT = /[a-zA-Z0-9]/;
4871
+ // Number of fill slots in a mask (its placeholder characters).
4872
+ const maskDigitCount = mask => [...mask].filter(isPlaceholder).length;
4873
+ // Formats a value into a placeholder mask (e.g. '(###) ###-####'), filling only the content entered so far.
4874
+ // Placeholder characters consume one content character each; every other mask character is a literal.
4875
+ const formatWithPattern = (value, mask) => {
4876
+ const maxContent = maskDigitCount(mask);
4877
+ const content = (value != null ? value : '').replace(NON_CONTENT, '').slice(0, maxContent);
4878
+ let index = 0;
4879
+ let formatted = '';
4880
+ for (const maskChar of mask) {
4881
+ if (index >= content.length) {
4882
+ break;
4883
+ }
4884
+ formatted += isPlaceholder(maskChar) ? content[index++] : maskChar;
4885
+ }
4886
+ return formatted;
4887
+ };
4888
+ // Fills the mask with the value's content, leaving the placeholder character for positions not yet entered.
4889
+ const toMaskTemplate = (value, mask) => {
4890
+ const maxContent = maskDigitCount(mask);
4891
+ const content = (value != null ? value : '').replace(NON_CONTENT, '').slice(0, maxContent);
4892
+ let index = 0;
4893
+ let display = '';
4894
+ for (const maskChar of mask) {
4895
+ display += isPlaceholder(maskChar) ? index < content.length ? content[index++] : maskChar : maskChar;
4896
+ }
4897
+ return display;
4898
+ };
4899
+ // Caret position just after the Nth content character within a rendered mask, so in-place edits keep the cursor in place.
4900
+ const caretAfterDigits = (display, digitCount) => {
4901
+ if (digitCount <= 0) {
4902
+ return 0;
4903
+ }
4904
+ let seen = 0;
4905
+ for (let i = 0; i < display.length; i++) {
4906
+ if (IS_CONTENT.test(display[i])) {
4907
+ seen += 1;
4908
+ if (seen === digitCount) {
4909
+ return i + 1;
4910
+ }
4911
+ }
4912
+ }
4913
+ return display.length;
4914
+ };
4915
+ // Computes an in-place masked edit from the raw input and caret index: what to show, what to store, where to put the caret.
4916
+ const computeMaskEdit = (rawValue, caretIndex, mask) => {
4917
+ const digitsBeforeCaret = rawValue.slice(0, caretIndex).replace(NON_CONTENT, '').length;
4918
+ const display = toMaskTemplate(rawValue, mask);
4919
+ const stored = formatWithPattern(rawValue, mask);
4920
+ const caret = caretAfterDigits(display, Math.min(digitsBeforeCaret, maskDigitCount(mask)));
4921
+ return {
4922
+ display,
4923
+ stored,
4924
+ caret
4925
+ };
4926
+ };
4927
+ // True once the value has as many content characters as the mask has fill slots.
4928
+ const isMaskFilled = (value, mask) => value.replace(NON_CONTENT, '').length >= maskDigitCount(mask);
4929
+ // A human placeholder for a mask, e.g. '(###) ###-####' -> '(000) 000-0000'.
4930
+ const maskPlaceholder = mask => [...mask].map(char => isPlaceholder(char) ? '0' : char).join('');
4931
+ // Extracts the underlying input element from a GoA change/keypress detail, if available.
4932
+ const getMaskInputTarget = detail => {
4933
+ var _detail$event$target, _detail$event;
4934
+ return (_detail$event$target = (_detail$event = detail.event) == null ? void 0 : _detail$event.target) != null ? _detail$event$target : undefined;
4935
+ };
4936
+ // Applies an in-place edit to the DOM: forces the (fixed-length) value and restores the caret after React commits.
4937
+ const applyInPlaceEdit = (target, edit) => {
4938
+ if (!target) {
4939
+ return;
4940
+ }
4941
+ target.value = edit.display;
4942
+ if (target.setSelectionRange) {
4943
+ requestAnimationFrame(() => target.setSelectionRange == null ? void 0 : target.setSelectionRange(edit.caret, edit.caret));
4944
+ }
4945
+ };
4946
+ /**
4947
+ * Applies a mask template to a value, e.g. mask "(###) ###-####" turns "7801234567" into "(780) 123-4567".
4948
+ * '#' consumes one content character from the input; every other character is a literal inserted into the output.
4949
+ * The mask's literal characters are stripped from the input first so an already formatted value re-formats cleanly.
4950
+ * Returns the original value unchanged when the mask is empty or no content characters remain.
4951
+ * @param value - The raw input value
4952
+ * @param mask - The mask template string
4953
+ * @returns The masked value
4954
+ */
4955
+ const applyFormatPattern = (value, mask) => {
4956
+ if (!mask || !value) {
4957
+ return value;
4958
+ }
4959
+ const literals = new Set(mask.split('').filter(char => !isPlaceholder(char)));
4960
+ const content = value.split('').filter(char => !literals.has(char));
4961
+ if (content.length === 0) {
4962
+ return value;
4963
+ }
4964
+ let contentIndex = 0;
4965
+ let result = '';
4966
+ for (const maskChar of mask) {
4967
+ if (contentIndex >= content.length) {
4968
+ break;
4969
+ }
4970
+ if (isPlaceholder(maskChar)) {
4971
+ result += content[contentIndex];
4972
+ contentIndex += 1;
4973
+ } else {
4974
+ result += maskChar;
4975
+ }
4976
+ }
4977
+ // Append any remaining content characters that did not fit the mask.
4978
+ if (contentIndex < content.length) {
4979
+ result += content.slice(contentIndex).join('');
4980
+ }
4981
+ return result;
4982
+ };
4983
+
4824
4984
  /**
4825
4985
  * Checks input controls data value to determine is required and has any data.
4826
4986
  * @param props - The JsonForm control props
@@ -9810,54 +9970,6 @@ if (DESCRIPTORS$1 && isCallable(NativeSymbol) && (!('description' in SymbolProto
9810
9970
  });
9811
9971
  }
9812
9972
 
9813
- var call$2 = functionCall;
9814
- var fixRegExpWellKnownSymbolLogic = fixRegexpWellKnownSymbolLogic;
9815
- var anObject = anObject$t;
9816
- var isNullOrUndefined = isNullOrUndefined$7;
9817
- var toLength$2 = toLength$7;
9818
- var toString$5 = toString$n;
9819
- var requireObjectCoercible$3 = requireObjectCoercible$e;
9820
- var getMethod = getMethod$7;
9821
- var advanceStringIndex = advanceStringIndex$3;
9822
- var regExpExec = regexpExecAbstract;
9823
-
9824
- // @@match logic
9825
- fixRegExpWellKnownSymbolLogic('match', function (MATCH, nativeMatch, maybeCallNative) {
9826
- return [
9827
- // `String.prototype.match` method
9828
- // https://tc39.es/ecma262/#sec-string.prototype.match
9829
- function match(regexp) {
9830
- var O = requireObjectCoercible$3(this);
9831
- var matcher = isNullOrUndefined(regexp) ? undefined : getMethod(regexp, MATCH);
9832
- return matcher ? call$2(matcher, regexp, O) : new RegExp(regexp)[MATCH](toString$5(O));
9833
- },
9834
- // `RegExp.prototype[@@match]` method
9835
- // https://tc39.es/ecma262/#sec-regexp.prototype-@@match
9836
- function (string) {
9837
- var rx = anObject(this);
9838
- var S = toString$5(string);
9839
- var res = maybeCallNative(nativeMatch, rx, S);
9840
-
9841
- if (res.done) return res.value;
9842
-
9843
- if (!rx.global) return regExpExec(rx, S);
9844
-
9845
- var fullUnicode = rx.unicode;
9846
- rx.lastIndex = 0;
9847
- var A = [];
9848
- var n = 0;
9849
- var result;
9850
- while ((result = regExpExec(rx, S)) !== null) {
9851
- var matchStr = toString$5(result[0]);
9852
- A[n] = matchStr;
9853
- if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength$2(rx.lastIndex), fullUnicode);
9854
- n++;
9855
- }
9856
- return n === 0 ? null : A;
9857
- }
9858
- ];
9859
- });
9860
-
9861
9973
  //type DebounceValueType = string | boolean | number | Record<string, unknown>;
9862
9974
  /**
9863
9975
  * A helper util to return a value at a certain delay. The delay is reset if the value arg changes
@@ -9888,23 +10000,6 @@ function fetchRegisterConfigFromOptions$1(options) {
9888
10000
  const config = Object.assign({}, options);
9889
10001
  return config;
9890
10002
  }
9891
- const formattedSinPattern = /^\d{3}-\d{3}-\d{3}$/;
9892
- const allowedSinInputPattern = /^[\d-]*$/;
9893
- const allowedSinKeyPattern = /^[\d-]$/;
9894
- const allowedSinControlKeys = new Set(['Backspace', 'Delete', 'Tab', 'Enter', 'Escape', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End']);
9895
- const formatSinForDisplay = value => value.replace(/ /g, '-');
9896
- const formatSinForSchema = value => value.replace(/-/g, ' ');
9897
- const formatSin = value => {
9898
- var _digits$match$join, _digits$match;
9899
- if (!allowedSinInputPattern.test(value)) {
9900
- return '';
9901
- }
9902
- if (formattedSinPattern.test(value)) {
9903
- return value;
9904
- }
9905
- const digits = value == null ? void 0 : value.replace(/\D/g, '').slice(0, 9);
9906
- return (_digits$match$join = digits == null || (_digits$match = digits.match(/.{1,3}/g)) == null ? void 0 : _digits$match.join('-')) != null ? _digits$match$join : '';
9907
- };
9908
10003
  const resetInputValue = (detail, value) => {
9909
10004
  var _detail$event;
9910
10005
  const target = (_detail$event = detail.event) == null ? void 0 : _detail$event.target;
@@ -9912,6 +10007,13 @@ const resetInputValue = (detail, value) => {
9912
10007
  target.value = value;
9913
10008
  }
9914
10009
  };
10010
+ // Resolve the mask format for a field: schema.format must match a DEFAULT_PATTERNS key.
10011
+ const resolveFormatConfig = schema => {
10012
+ if (schema.format && schema.format in DEFAULT_PATTERNS) {
10013
+ return DEFAULT_PATTERNS[schema.format];
10014
+ }
10015
+ return schema.title === sinTitle ? DEFAULT_PATTERNS.sin : undefined;
10016
+ };
9915
10017
  const GoAInputText = props => {
9916
10018
  return jsxs(JsonFormRegisterProvider, {
9917
10019
  defaultRegisters: undefined,
@@ -9919,7 +10021,7 @@ const GoAInputText = props => {
9919
10021
  });
9920
10022
  };
9921
10023
  const InnerGoAInputText = props => {
9922
- var _uischema$options$com, _uischema$options, _props$uischema, _uischema$options2, _uischema$options3, _uischema$options$pla, _uischema$options4, _props$uischema2, _uischema$options5, _uischema$options6, _uischema$options$com2, _uischema$options7, _uischema$options8, _uischema$options9;
10024
+ var _formatConfig$mask, _uischema$options, _uischema$options2, _uischema$options$com, _uischema$options3, _props$uischema, _uischema$options4, _uischema$options5, _uischema$options$pla, _uischema$options6, _props$uischema2, _uischema$options7, _uischema$options8, _uischema$options$com2, _uischema$options9, _uischema$options0, _uischema$options1, _uischema$options10;
9923
10025
  const {
9924
10026
  data,
9925
10027
  config,
@@ -9934,18 +10036,37 @@ const InnerGoAInputText = props => {
9934
10036
  isVisited,
9935
10037
  setIsVisited
9936
10038
  } = props;
9937
- const isSinField = schema.title === sinTitle;
9938
- const initialValue = isSinField && typeof data === 'string' ? formatSinForDisplay(data) : data;
10039
+ // Detect the mask format from the JSON schema `format` (with the SIN title kept for backward compatibility).
10040
+ const formatConfig = resolveFormatConfig(schema);
10041
+ const mask = (_formatConfig$mask = formatConfig == null ? void 0 : formatConfig.mask) != null ? _formatConfig$mask : uischema == null || (_uischema$options = uischema.options) == null ? void 0 : _uischema$options.mask;
10042
+ const allowedKeys = formatConfig == null ? void 0 : formatConfig.allowedKeys;
10043
+ // In-place shows the fill-in template and keeps the caret in place while editing.
10044
+ const inPlace = !!mask && (uischema == null || (_uischema$options2 = uischema.options) == null ? void 0 : _uischema$options2.inPlace) === true;
10045
+ const toDisplay = value => {
10046
+ if (!mask) {
10047
+ return value;
10048
+ }
10049
+ const str = typeof value === 'string' ? value : '';
10050
+ return inPlace ? toMaskTemplate(str, mask) : formatWithPattern(str, mask);
10051
+ };
9939
10052
  const [manualInput, setManualInput] = useState(false);
9940
- const [localValue, setLocalValue] = useState(initialValue);
10053
+ // In-place mounts empty then sets the template so the web component reflects it (a value change forces the paint).
10054
+ const [localValue, setLocalValue] = useState(inPlace ? '' : toDisplay(data));
10055
+ useEffect(() => {
10056
+ if (inPlace) {
10057
+ setLocalValue(toDisplay(data));
10058
+ }
10059
+ // eslint-disable-next-line react-hooks/exhaustive-deps
10060
+ }, []);
9941
10061
  const debouncedValue = useDebounce(localValue, 300);
9942
10062
  const hasDefault = Object.prototype.hasOwnProperty.call(schema, 'default');
9943
10063
  useEffect(() => {
9944
10064
  if (data === undefined || data === null) {
9945
10065
  return;
9946
10066
  }
9947
- setLocalValue(isSinField && typeof data === 'string' ? formatSinForDisplay(data) : data);
9948
- }, [data, isSinField]);
10067
+ setLocalValue(toDisplay(data));
10068
+ // eslint-disable-next-line react-hooks/exhaustive-deps
10069
+ }, [data, mask]);
9949
10070
  useEffect(() => {
9950
10071
  if (typeof handleChange === 'function' && hasDefault && data === undefined && !manualInput) {
9951
10072
  handleChange(props.path, schema.default);
@@ -9955,17 +10076,19 @@ const InnerGoAInputText = props => {
9955
10076
  }, [schema.default, data]);
9956
10077
  /* istanbul ignore next */
9957
10078
  useEffect(() => {
9958
- const dataForInput = isSinField && typeof data === 'string' ? formatSinForDisplay(data) : data;
10079
+ const dataForInput = toDisplay(data);
9959
10080
  if (debouncedValue === dataForInput) return;
9960
10081
  // Only sync if debouncedValue differs from data and is not initial empty state
9961
10082
  if (debouncedValue !== dataForInput && (debouncedValue !== '' || data !== undefined)) {
9962
10083
  onChangeForInputControl({
9963
- value: isSinField ? formatSinForSchema(debouncedValue) : debouncedValue,
10084
+ // Normalize to the clean value; for in-place this drops the unfilled template characters.
10085
+ value: mask ? formatWithPattern(debouncedValue, mask) : debouncedValue,
9964
10086
  controlProps: props
9965
10087
  });
9966
10088
  }
10089
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9967
10090
  }, [debouncedValue]);
9968
- const width = (_uischema$options$com = uischema == null || (_uischema$options = uischema.options) == null || (_uischema$options = _uischema$options.componentProps) == null ? void 0 : _uischema$options.width) != null ? _uischema$options$com : '100%';
10091
+ const width = (_uischema$options$com = uischema == null || (_uischema$options3 = uischema.options) == null || (_uischema$options3 = _uischema$options3.componentProps) == null ? void 0 : _uischema$options3.width) != null ? _uischema$options$com : '100%';
9969
10092
  const registerCtx = useContext(JsonFormsRegisterContext);
9970
10093
  const registerConfig = fetchRegisterConfigFromOptions$1((_props$uischema = props.uischema) == null || (_props$uischema = _props$uischema.options) == null ? void 0 : _props$uischema.register);
9971
10094
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -9973,9 +10096,9 @@ const InnerGoAInputText = props => {
9973
10096
  if (registerConfig) {
9974
10097
  registerData = registerCtx == null ? void 0 : registerCtx.selectRegisterData(registerConfig);
9975
10098
  }
9976
- const labelPath = (uischema == null || (_uischema$options2 = uischema.options) == null ? void 0 : _uischema$options2.label) || 'label';
9977
- const valuePath = (uischema == null || (_uischema$options3 = uischema.options) == null ? void 0 : _uischema$options3.value) || 'value';
9978
- const dropDownPlaceholder = (_uischema$options$pla = uischema == null || (_uischema$options4 = uischema.options) == null ? void 0 : _uischema$options4.placeholder) != null ? _uischema$options$pla : 'Select an option';
10099
+ const labelPath = (uischema == null || (_uischema$options4 = uischema.options) == null ? void 0 : _uischema$options4.label) || 'label';
10100
+ const valuePath = (uischema == null || (_uischema$options5 = uischema.options) == null ? void 0 : _uischema$options5.value) || 'value';
10101
+ const dropDownPlaceholder = (_uischema$options$pla = uischema == null || (_uischema$options6 = uischema.options) == null ? void 0 : _uischema$options6.placeholder) != null ? _uischema$options$pla : 'Select an option';
9979
10102
  const autoCompletion = ((_props$uischema2 = props.uischema) == null || (_props$uischema2 = _props$uischema2.options) == null ? void 0 : _props$uischema2.autoComplete) === true;
9980
10103
  const mergedOptions = useMemo(() => {
9981
10104
  var _registerData;
@@ -10012,32 +10135,10 @@ const InnerGoAInputText = props => {
10012
10135
  }, [registerCtx, registerConfig]);
10013
10136
  const appliedUiSchemaOptions = Object.assign({}, config, uischema == null ? void 0 : uischema.options);
10014
10137
  const placeholder = (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.placeholder) || (schema == null ? void 0 : schema.description) || '';
10015
- const autoCapitalize = (uischema == null || (_uischema$options5 = uischema.options) == null || (_uischema$options5 = _uischema$options5.componentProps) == null ? void 0 : _uischema$options5.autoCapitalize) === true || (uischema == null || (_uischema$options6 = uischema.options) == null ? void 0 : _uischema$options6.autoCapitalize) === true;
10016
- const readOnly = (_uischema$options$com2 = uischema == null || (_uischema$options7 = uischema.options) == null || (_uischema$options7 = _uischema$options7.componentProps) == null ? void 0 : _uischema$options7.readOnly) != null ? _uischema$options$com2 : false;
10017
- const preventInvalidSinKey = event => {
10018
- if (!isSinField || allowedSinControlKeys.has(event.key) || event.metaKey || event.ctrlKey || event.altKey || allowedSinKeyPattern.test(event.key)) {
10019
- return;
10020
- }
10021
- event.preventDefault();
10022
- };
10023
- const preventInvalidSinBeforeInput = event => {
10024
- if (!isSinField) {
10025
- return;
10026
- }
10027
- const data = event.nativeEvent.data;
10028
- if (data && !allowedSinInputPattern.test(data)) {
10029
- event.preventDefault();
10030
- }
10031
- };
10032
- const preventInvalidSinPaste = event => {
10033
- if (isSinField && !allowedSinInputPattern.test(event.clipboardData.getData('text'))) {
10034
- event.preventDefault();
10035
- }
10036
- };
10138
+ const autoCapitalize = (uischema == null || (_uischema$options7 = uischema.options) == null || (_uischema$options7 = _uischema$options7.componentProps) == null ? void 0 : _uischema$options7.autoCapitalize) === true || (uischema == null || (_uischema$options8 = uischema.options) == null ? void 0 : _uischema$options8.autoCapitalize) === true;
10139
+ const readOnly = (_uischema$options$com2 = uischema == null || (_uischema$options9 = uischema.options) == null || (_uischema$options9 = _uischema$options9.componentProps) == null ? void 0 : _uischema$options9.readOnly) != null ? _uischema$options$com2 : false;
10140
+ const formatPattern = uischema == null || (_uischema$options0 = uischema.options) == null ? void 0 : _uischema$options0.formatPattern;
10037
10141
  return jsx("div", {
10038
- onKeyDownCapture: preventInvalidSinKey,
10039
- onBeforeInputCapture: preventInvalidSinBeforeInput,
10040
- onPasteCapture: preventInvalidSinPaste,
10041
10142
  children: mergedOptions.length > 1 ? jsx(GoabDropdown, {
10042
10143
  name: `jsonforms-${path}-dropdown`,
10043
10144
  value: data,
@@ -10058,22 +10159,29 @@ const InnerGoAInputText = props => {
10058
10159
  value: localValue,
10059
10160
  width: width,
10060
10161
  readonly: readOnly,
10061
- maxLength: isSinField ? 11 : undefined,
10062
- placeholder: placeholder,
10162
+ maxLength: inPlace ? undefined : mask ? mask.length : undefined,
10163
+ placeholder: inPlace ? undefined : mask ? maskPlaceholder(mask) : placeholder,
10063
10164
  name: (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.name) || `${id || label}-input`,
10064
10165
  ariaLabel: (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.name) || `${id || label}-input`,
10065
10166
  testId: (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.testId) || `${id}-input`
10066
- }, (_uischema$options8 = uischema.options) == null ? void 0 : _uischema$options8.componentProps, {
10167
+ }, (_uischema$options1 = uischema.options) == null ? void 0 : _uischema$options1.componentProps, {
10067
10168
  onChange: detail => {
10068
- let formattedValue = detail.value;
10069
- if (isSinField && !allowedSinInputPattern.test(detail.value)) {
10070
- resetInputValue(detail, localValue);
10071
- return;
10072
- }
10073
- if (isSinField && detail.value !== '') {
10074
- formattedValue = formatSin(detail.value);
10169
+ const cleaned = allowedKeys ? filterAllowedKeys(detail.value, allowedKeys) : detail.value;
10170
+ if (inPlace && mask) {
10171
+ var _target$selectionStar;
10172
+ const target = getMaskInputTarget(detail);
10173
+ const caretIndex = (_target$selectionStar = target == null ? void 0 : target.selectionStart) != null ? _target$selectionStar : cleaned.length;
10174
+ const edit = computeMaskEdit(cleaned, caretIndex, mask);
10175
+ applyInPlaceEdit(target, edit);
10176
+ setLocalValue(edit.display);
10177
+ } else {
10178
+ const formattedValue = mask && cleaned !== '' ? formatWithPattern(cleaned, mask) : cleaned;
10179
+ // If characters were rejected, force the input to show the cleaned value.
10180
+ if (allowedKeys && cleaned !== detail.value) {
10181
+ resetInputValue(detail, formattedValue);
10182
+ }
10183
+ setLocalValue(formattedValue);
10075
10184
  }
10076
- setLocalValue(formattedValue);
10077
10185
  setManualInput(true);
10078
10186
  if (isVisited === false && setIsVisited) {
10079
10187
  setIsVisited();
@@ -10083,19 +10191,29 @@ const InnerGoAInputText = props => {
10083
10191
  if (isVisited === false && setIsVisited) {
10084
10192
  setIsVisited();
10085
10193
  }
10194
+ const capitalizedValue = autoCapitalize ? detail.value.toUpperCase() : detail.value;
10195
+ if (formatPattern) {
10196
+ const formattedValue = applyFormatPattern(capitalizedValue, formatPattern);
10197
+ setLocalValue(formattedValue);
10198
+ handleChange(path, formattedValue);
10199
+ return;
10200
+ }
10086
10201
  onBlurForTextControl({
10087
10202
  name: detail.name,
10088
10203
  controlProps: props,
10089
- value: autoCapitalize ? detail.value.toUpperCase() : detail.value
10204
+ value: capitalizedValue
10090
10205
  });
10091
10206
  },
10092
10207
  onKeyPress: detail => {
10093
- if (isSinField && detail.key && !allowedSinKeyPattern.test(detail.key)) {
10208
+ const blockDisallowed = shouldBlockKey(detail.key, allowedKeys);
10209
+ // In-place has no length cap, so also block content keys once the template is full.
10210
+ const blockOverflow = inPlace && !!mask && /^[A-Za-z0-9]$/.test(detail.key) && isMaskFilled(localValue, mask);
10211
+ if (blockDisallowed || blockOverflow) {
10094
10212
  var _detail$event2;
10095
10213
  (_detail$event2 = detail.event) == null || _detail$event2.preventDefault();
10096
10214
  }
10097
10215
  }
10098
- }, uischema == null || (_uischema$options9 = uischema.options) == null ? void 0 : _uischema$options9.componentProps))
10216
+ }, uischema == null || (_uischema$options10 = uischema.options) == null ? void 0 : _uischema$options10.componentProps))
10099
10217
  });
10100
10218
  };
10101
10219
  const GoATextControl = props => jsx(GoAInputBaseControl, Object.assign({}, props, {
@@ -10254,15 +10372,15 @@ function _objectWithoutPropertiesLoose(r, e) {
10254
10372
  }
10255
10373
 
10256
10374
  var toIntegerOrInfinity$1 = toIntegerOrInfinity$9;
10257
- var toString$4 = toString$n;
10258
- var requireObjectCoercible$2 = requireObjectCoercible$e;
10375
+ var toString$5 = toString$n;
10376
+ var requireObjectCoercible$3 = requireObjectCoercible$e;
10259
10377
 
10260
10378
  var $RangeError$1 = RangeError;
10261
10379
 
10262
10380
  // `String.prototype.repeat` method implementation
10263
10381
  // https://tc39.es/ecma262/#sec-string.prototype.repeat
10264
10382
  var stringRepeat = function repeat(count) {
10265
- var str = toString$4(requireObjectCoercible$2(this));
10383
+ var str = toString$5(requireObjectCoercible$3(this));
10266
10384
  var result = '';
10267
10385
  var n = toIntegerOrInfinity$1(count);
10268
10386
  if (n < 0 || n === Infinity) throw new $RangeError$1('Wrong number of repetitions');
@@ -10272,10 +10390,10 @@ var stringRepeat = function repeat(count) {
10272
10390
 
10273
10391
  // https://github.com/tc39/proposal-string-pad-start-end
10274
10392
  var uncurryThis$4 = functionUncurryThis;
10275
- var toLength$1 = toLength$7;
10276
- var toString$3 = toString$n;
10393
+ var toLength$2 = toLength$7;
10394
+ var toString$4 = toString$n;
10277
10395
  var $repeat = stringRepeat;
10278
- var requireObjectCoercible$1 = requireObjectCoercible$e;
10396
+ var requireObjectCoercible$2 = requireObjectCoercible$e;
10279
10397
 
10280
10398
  var repeat = uncurryThis$4($repeat);
10281
10399
  var stringSlice$1 = uncurryThis$4(''.slice);
@@ -10284,10 +10402,10 @@ var ceil = Math.ceil;
10284
10402
  // `String.prototype.{ padStart, padEnd }` methods implementation
10285
10403
  var createMethod = function (IS_END) {
10286
10404
  return function ($this, maxLength, fillString) {
10287
- var S = toString$3(requireObjectCoercible$1($this));
10288
- var intMaxLength = toLength$1(maxLength);
10405
+ var S = toString$4(requireObjectCoercible$2($this));
10406
+ var intMaxLength = toLength$2(maxLength);
10289
10407
  var stringLength = S.length;
10290
- var fillStr = fillString === undefined ? ' ' : toString$3(fillString);
10408
+ var fillStr = fillString === undefined ? ' ' : toString$4(fillString);
10291
10409
  var fillLen, stringFiller;
10292
10410
  if (intMaxLength <= stringLength || fillStr === '') return S;
10293
10411
  fillLen = intMaxLength - stringLength;
@@ -11331,6 +11449,54 @@ const GoInputBaseReview = props => jsx(GoAInputBaseControl, Object.assign({}, pr
11331
11449
  }));
11332
11450
  const GoAInputBaseReviewControl = withJsonFormsControlProps(GoInputBaseReview);
11333
11451
 
11452
+ var call$2 = functionCall;
11453
+ var fixRegExpWellKnownSymbolLogic = fixRegexpWellKnownSymbolLogic;
11454
+ var anObject = anObject$t;
11455
+ var isNullOrUndefined = isNullOrUndefined$7;
11456
+ var toLength$1 = toLength$7;
11457
+ var toString$3 = toString$n;
11458
+ var requireObjectCoercible$1 = requireObjectCoercible$e;
11459
+ var getMethod = getMethod$7;
11460
+ var advanceStringIndex = advanceStringIndex$3;
11461
+ var regExpExec = regexpExecAbstract;
11462
+
11463
+ // @@match logic
11464
+ fixRegExpWellKnownSymbolLogic('match', function (MATCH, nativeMatch, maybeCallNative) {
11465
+ return [
11466
+ // `String.prototype.match` method
11467
+ // https://tc39.es/ecma262/#sec-string.prototype.match
11468
+ function match(regexp) {
11469
+ var O = requireObjectCoercible$1(this);
11470
+ var matcher = isNullOrUndefined(regexp) ? undefined : getMethod(regexp, MATCH);
11471
+ return matcher ? call$2(matcher, regexp, O) : new RegExp(regexp)[MATCH](toString$3(O));
11472
+ },
11473
+ // `RegExp.prototype[@@match]` method
11474
+ // https://tc39.es/ecma262/#sec-regexp.prototype-@@match
11475
+ function (string) {
11476
+ var rx = anObject(this);
11477
+ var S = toString$3(string);
11478
+ var res = maybeCallNative(nativeMatch, rx, S);
11479
+
11480
+ if (res.done) return res.value;
11481
+
11482
+ if (!rx.global) return regExpExec(rx, S);
11483
+
11484
+ var fullUnicode = rx.unicode;
11485
+ rx.lastIndex = 0;
11486
+ var A = [];
11487
+ var n = 0;
11488
+ var result;
11489
+ while ((result = regExpExec(rx, S)) !== null) {
11490
+ var matchStr = toString$3(result[0]);
11491
+ A[n] = matchStr;
11492
+ if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength$1(rx.lastIndex), fullUnicode);
11493
+ n++;
11494
+ }
11495
+ return n === 0 ? null : A;
11496
+ }
11497
+ ];
11498
+ });
11499
+
11334
11500
  var $$6 = _export;
11335
11501
  var toObject$2 = toObject$c;
11336
11502
  var lengthOfArrayLike$2 = lengthOfArrayLike$c;
@@ -13656,107 +13822,83 @@ const FullNameDobReviewControl = props => {
13656
13822
  };
13657
13823
  const GoAInputBaseFullNameDobControlReview = withJsonFormsAllOfProps(FullNameDobReviewControl);
13658
13824
 
13659
- let _$5 = t => t,
13660
- _t$6;
13661
- const PhoneGrid = styled.div(_t$6 || (_t$6 = _$5`
13662
- display: grid;
13663
- grid-template-columns: 1fr 2fr;
13664
- gap: 0.2rem;
13665
- align-items: start;
13666
- margin-bottom: var(--goa-space-l) !important;
13667
-
13668
- goa-form-item {
13669
- margin-right: 0 !important;
13670
- }
13671
- `));
13672
- const PHONE_REGEX = /^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
13673
- const normalizePhoneData = value => ({
13674
- type: (value == null ? void 0 : value.type) || '',
13675
- number: (value == null ? void 0 : value.number) || ''
13676
- });
13677
- const PhoneNumberWithTypeControl = props => {
13678
- const {
13679
- data,
13680
- path,
13681
- handleChange,
13682
- enabled,
13683
- visible,
13684
- required
13685
- } = props;
13686
- const [formData, setFormData] = useState(normalizePhoneData(data));
13687
- const [error, setError] = useState(undefined);
13825
+ // Encapsulates masked-input state for a GoA input: display value, mount reflection, and change/keypress handling.
13826
+ const useMaskedInput = ({
13827
+ mask,
13828
+ inPlace,
13829
+ data,
13830
+ onCommit
13831
+ }) => {
13832
+ const format = value => inPlace ? toMaskTemplate(value, mask) : formatWithPattern(value, mask);
13833
+ const initialDisplay = format(typeof data === 'string' ? data : '');
13834
+ // In-place mounts empty then sets the template so the web component reflects it (a value change forces the paint).
13835
+ const [value, setValue] = useState(inPlace ? '' : initialDisplay);
13688
13836
  useEffect(() => {
13689
- const nextData = normalizePhoneData(data);
13690
- setFormData(currentData => currentData.type === nextData.type && currentData.number === nextData.number ? currentData : nextData);
13691
- }, [data]);
13692
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13693
- const updateFormData = updated => {
13694
- setFormData(updated);
13695
- if (updated.number && !PHONE_REGEX.test(updated.number)) {
13696
- setError('Enter a valid phone number (e.g., (780) 123-4567)');
13697
- } else {
13698
- setError(undefined);
13837
+ if (inPlace) {
13838
+ setValue(initialDisplay);
13699
13839
  }
13700
- handleChange(path, updated);
13840
+ // eslint-disable-next-line react-hooks/exhaustive-deps
13841
+ }, []);
13842
+ const handleChange = detail => {
13843
+ var _target$selectionStar;
13844
+ const rawValue = detail.value;
13845
+ if (!inPlace) {
13846
+ const stored = formatWithPattern(rawValue, mask);
13847
+ setValue(stored);
13848
+ onCommit(stored);
13849
+ return;
13850
+ }
13851
+ const target = getMaskInputTarget(detail);
13852
+ const caretIndex = (_target$selectionStar = target == null ? void 0 : target.selectionStart) != null ? _target$selectionStar : rawValue.length;
13853
+ const edit = computeMaskEdit(rawValue, caretIndex, mask);
13854
+ applyInPlaceEdit(target, edit);
13855
+ setValue(edit.display);
13856
+ onCommit(edit.stored);
13857
+ };
13858
+ // In-place mode has no native length cap, so block extra content characters at the source.
13859
+ const handleKeyPress = detail => {
13860
+ if (inPlace && /^[A-Za-z0-9]$/.test(detail.key) && isMaskFilled(value, mask)) {
13861
+ var _detail$event;
13862
+ (_detail$event = detail.event) == null || _detail$event.preventDefault();
13863
+ }
13864
+ };
13865
+ return {
13866
+ value,
13867
+ handleChange,
13868
+ handleKeyPress
13701
13869
  };
13702
- return jsx(Visible, {
13703
- "$visible": visible,
13704
- children: jsxs(PhoneGrid, {
13705
- children: [jsx(GoabFormItem, {
13706
- label: "Phone type",
13707
- requirement: required ? 'required' : undefined,
13708
- children: jsxs(GoabDropdown, {
13709
- name: "type",
13710
- value: formData.type || '',
13711
- disabled: !enabled,
13712
- onChange: detail => updateFormData(Object.assign({}, formData, {
13713
- type: detail.value
13714
- })),
13715
- children: [jsx(GoabDropdownItem, {
13716
- value: "Mobile"
13717
- }), jsx(GoabDropdownItem, {
13718
- value: "Landline"
13719
- })]
13720
- })
13721
- }), jsx(GoabFormItem, {
13722
- label: "Phone number",
13723
- error: error,
13724
- requirement: required ? 'required' : undefined,
13725
- children: jsx(GoabInput, {
13726
- type: "tel",
13727
- name: "number",
13728
- placeholder: "(000) 000-0000",
13729
- value: formData.number || '',
13730
- disabled: !enabled,
13731
- onChange: detail => updateFormData(Object.assign({}, formData, {
13732
- number: detail.value
13733
- }))
13734
- })
13735
- })]
13736
- })
13737
- });
13738
13870
  };
13739
- const GoAPhoneNumberWithTypeControl = withJsonFormsControlProps(PhoneNumberWithTypeControl);
13740
13871
 
13741
13872
  const PhoneNumberControl = props => {
13873
+ var _uischema$options, _uischema$options2;
13742
13874
  const {
13743
13875
  data,
13744
13876
  path,
13745
13877
  schema,
13878
+ uischema,
13746
13879
  handleChange,
13747
13880
  enabled,
13748
13881
  visible,
13749
13882
  required
13750
13883
  } = props;
13884
+ // Default mask can be overridden per field via the UI schema options.
13885
+ const phoneMask = (uischema == null || (_uischema$options = uischema.options) == null ? void 0 : _uischema$options.phoneMask) || DEFAULT_PATTERNS.phone.mask;
13886
+ // When in-place, the field shows the fill-in template and edits keep the caret in place.
13887
+ const inPlace = (uischema == null || (_uischema$options2 = uischema.options) == null ? void 0 : _uischema$options2.inPlace) === true;
13751
13888
  const [error, setError] = useState('');
13752
- const handleInputChange = (name, value) => {
13753
- if (value && !PHONE_REGEX.test(value)) {
13754
- setError('Must be a valid 10-digit phone number in format (000) 000-0000');
13755
- } else {
13756
- setError('');
13889
+ const {
13890
+ value,
13891
+ handleChange: handleMaskChange,
13892
+ handleKeyPress
13893
+ } = useMaskedInput({
13894
+ mask: phoneMask,
13895
+ inPlace,
13896
+ data,
13897
+ onCommit: stored => {
13898
+ setError(stored && !DEFAULT_PATTERNS.phone.pattern.test(stored) ? DEFAULT_PATTERNS.phone.error : '');
13899
+ handleChange(path, stored);
13757
13900
  }
13758
- handleChange(path, value);
13759
- };
13901
+ });
13760
13902
  return jsx(Visible, {
13761
13903
  "$visible": visible,
13762
13904
  children: jsx(GoabFormItem, {
@@ -13770,9 +13912,11 @@ const PhoneNumberControl = props => {
13770
13912
  disabled: !enabled,
13771
13913
  "aria-label": "phone number input",
13772
13914
  testId: `phone-input-${path}`,
13773
- value: data || '',
13774
- onChange: detail => handleInputChange(detail.name, detail.value),
13775
- placeholder: "(000) 000-0000",
13915
+ value: value,
13916
+ maxLength: inPlace ? undefined : phoneMask.length,
13917
+ placeholder: inPlace ? undefined : maskPlaceholder(phoneMask),
13918
+ onChange: detail => handleMaskChange(detail),
13919
+ onKeyPress: inPlace ? detail => handleKeyPress(detail) : undefined,
13776
13920
  width: "100%"
13777
13921
  })
13778
13922
  })
@@ -13782,14 +13926,14 @@ const GoAPhoneNumberControl = withJsonFormsControlProps(PhoneNumberControl);
13782
13926
 
13783
13927
  const PhoneNumberTester = rankWith(4, and(isControl$1, schemaTypeIs('string'), formatIs('phone')));
13784
13928
 
13785
- let _$4 = t => t,
13786
- _t$5,
13929
+ let _$5 = t => t,
13930
+ _t$6,
13787
13931
  _t2$2,
13788
13932
  _t3$1,
13789
13933
  _t4$1,
13790
13934
  _t5,
13791
13935
  _t6;
13792
- const SearchBox = styled.div(_t$5 || (_t$5 = _$4`
13936
+ const SearchBox = styled.div(_t$6 || (_t$6 = _$5`
13793
13937
  position: relative;
13794
13938
 
13795
13939
  .suggestions {
@@ -13831,7 +13975,7 @@ const SearchBox = styled.div(_t$5 || (_t$5 = _$4`
13831
13975
  pointer-events: none; /* Prevent spinner from interfering with clicks */
13832
13976
  }
13833
13977
  `));
13834
- const AddressIndent = styled.div(_t2$2 || (_t2$2 = _$4`
13978
+ const AddressIndent = styled.div(_t2$2 || (_t2$2 = _$5`
13835
13979
  margin: 0 0 var(--goa-space-m) 0;
13836
13980
  text-wrap: wrap;
13837
13981
  word-break: break-word;
@@ -13839,15 +13983,15 @@ const AddressIndent = styled.div(_t2$2 || (_t2$2 = _$4`
13839
13983
  border-radius: var(--goa-border-radius-xl);
13840
13984
  padding: var(--goa-space-l);
13841
13985
  `));
13842
- const TextWrap = styled.p(_t3$1 || (_t3$1 = _$4`
13986
+ const TextWrap = styled.p(_t3$1 || (_t3$1 = _$5`
13843
13987
  text-wrap: auto;
13844
13988
  word-break: break-word;
13845
13989
  `));
13846
- const TextWrapDiv = styled.div(_t4$1 || (_t4$1 = _$4`
13990
+ const TextWrapDiv = styled.div(_t4$1 || (_t4$1 = _$5`
13847
13991
  text-wrap: auto;
13848
13992
  word-break: break-word;
13849
13993
  `));
13850
- const LabelDiv = styled.div(_t5 || (_t5 = _$4`
13994
+ const LabelDiv = styled.div(_t5 || (_t5 = _$5`
13851
13995
  color: var(--goa-color-text-default);
13852
13996
  font: var(--goa-typography-body-m);
13853
13997
  min-height: 2.625rem;
@@ -13857,7 +14001,7 @@ const LabelDiv = styled.div(_t5 || (_t5 = _$4`
13857
14001
  word-break: break-word;
13858
14002
  `));
13859
14003
  /* istanbul ignore next */
13860
- const ListItem = styled.li(_t6 || (_t6 = _$4`
14004
+ const ListItem = styled.li(_t6 || (_t6 = _$5`
13861
14005
  background-color: ${0};
13862
14006
  color: ${0};
13863
14007
  font-weight: ${0};
@@ -13894,6 +14038,88 @@ const PhoneNumberReviewControl = props => {
13894
14038
  };
13895
14039
  const GoabInputBasePhoneNumberReviewControl = withJsonFormsAllOfProps(PhoneNumberReviewControl);
13896
14040
 
14041
+ let _$4 = t => t,
14042
+ _t$5;
14043
+ const PhoneGrid = styled.div(_t$5 || (_t$5 = _$4`
14044
+ display: grid;
14045
+ grid-template-columns: 1fr 2fr;
14046
+ gap: 0.2rem;
14047
+ align-items: start;
14048
+ margin-bottom: var(--goa-space-l) !important;
14049
+
14050
+ goa-form-item {
14051
+ margin-right: 0 !important;
14052
+ }
14053
+ `));
14054
+ const PHONE_REGEX = /^\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
14055
+ const normalizePhoneData = value => ({
14056
+ type: (value == null ? void 0 : value.type) || '',
14057
+ number: (value == null ? void 0 : value.number) || ''
14058
+ });
14059
+ const PhoneNumberWithTypeControl = props => {
14060
+ const {
14061
+ data,
14062
+ path,
14063
+ handleChange,
14064
+ enabled,
14065
+ visible,
14066
+ required
14067
+ } = props;
14068
+ const [formData, setFormData] = useState(normalizePhoneData(data));
14069
+ const [error, setError] = useState(undefined);
14070
+ useEffect(() => {
14071
+ const nextData = normalizePhoneData(data);
14072
+ setFormData(currentData => currentData.type === nextData.type && currentData.number === nextData.number ? currentData : nextData);
14073
+ }, [data]);
14074
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14075
+ const updateFormData = updated => {
14076
+ setFormData(updated);
14077
+ if (updated.number && !PHONE_REGEX.test(updated.number)) {
14078
+ setError('Enter a valid phone number (e.g., (780) 123-4567)');
14079
+ } else {
14080
+ setError(undefined);
14081
+ }
14082
+ handleChange(path, updated);
14083
+ };
14084
+ return jsx(Visible, {
14085
+ "$visible": visible,
14086
+ children: jsxs(PhoneGrid, {
14087
+ children: [jsx(GoabFormItem, {
14088
+ label: "Phone type",
14089
+ requirement: required ? 'required' : undefined,
14090
+ children: jsxs(GoabDropdown, {
14091
+ name: "type",
14092
+ value: formData.type || '',
14093
+ disabled: !enabled,
14094
+ onChange: detail => updateFormData(Object.assign({}, formData, {
14095
+ type: detail.value
14096
+ })),
14097
+ children: [jsx(GoabDropdownItem, {
14098
+ value: "Mobile"
14099
+ }), jsx(GoabDropdownItem, {
14100
+ value: "Landline"
14101
+ })]
14102
+ })
14103
+ }), jsx(GoabFormItem, {
14104
+ label: "Phone number",
14105
+ error: error,
14106
+ requirement: required ? 'required' : undefined,
14107
+ children: jsx(GoabInput, {
14108
+ type: "tel",
14109
+ name: "number",
14110
+ placeholder: "(000) 000-0000",
14111
+ value: formData.number || '',
14112
+ disabled: !enabled,
14113
+ onChange: detail => updateFormData(Object.assign({}, formData, {
14114
+ number: detail.value
14115
+ }))
14116
+ })
14117
+ })]
14118
+ })
14119
+ });
14120
+ };
14121
+ const GoAPhoneNumberWithTypeControl = withJsonFormsControlProps(PhoneNumberWithTypeControl);
14122
+
13897
14123
  const isPhoneNumberWithType = createSchemaMatchTester(['number', 'type'], true);
13898
14124
  const PhoneNumberWithTypeTester = rankWith(4, isPhoneNumberWithType);
13899
14125
 
@@ -20316,6 +20542,8 @@ const GoAErrorControlTester = rankWith(1000, (uischema, schema, context) => {
20316
20542
  });
20317
20543
  var GoAErrorControl = withJsonFormsControlProps(ErrorControl);
20318
20544
 
20545
+ // Allow empty values so incomplete fields are not treated as format errors.
20546
+ const optionalFormat = pattern => input => !input || pattern.test(input);
20319
20547
  const createDefaultAjv = (...schemas) => {
20320
20548
  const ajv = new Ajv({
20321
20549
  allErrors: true,
@@ -20329,6 +20557,9 @@ const createDefaultAjv = (...schemas) => {
20329
20557
  addErrors(ajv);
20330
20558
  addFormats(ajv);
20331
20559
  ajv.addFormat('time', /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/);
20560
+ Object.entries(DEFAULT_PATTERNS).forEach(([format, config]) => {
20561
+ ajv.addFormat(format, optionalFormat(config.pattern));
20562
+ });
20332
20563
  ajv.addFormat('phone', PHONE_REGEX);
20333
20564
  ajv.addFormat('computed', /^[a-zA-Z0-9._-]+$/);
20334
20565
  ajv.addFormat('file-urn', {
@@ -20680,4 +20911,4 @@ const GoARenderers = [...GoABaseRenderers, {
20680
20911
  }];
20681
20912
  const GoACells = [...InputCells];
20682
20913
 
20683
- export { ADD_DATALIST_ACTION, ADD_NO_ANONYMOUS_ACTION, ADD_REGISTER_DATA_ACTION, ADD_REGISTER_DATA_ERROR, ADD_USER_ACTION, AUTO_POPULATE_SOURCES, AddressLookUpControl, AddressLookUpControlReview, AddressLookUpTester, AddressLoopUpControlTableReview, ArrayControl, ArrayControlBase, ArrayControlReview, BooleanComponent, BooleanControl, BooleanRadioComponent, BooleanRadioControl, CategorizationPagesRendererTester, CategorizationStepperRendererTester, CheckboxGroup, ContactInformationTester, ContextProviderC, ContextProviderClass, ContextProviderFactory, ContractInfoControl, ContractInfoControlReview, EnumCheckboxControl, EnumRadioControl, EnumSelect, FileUploader, FileUploaderReview, FileUploaderTester, FormPageStepper, FormPagesView, FormStepper, FormStepperControl, FormStepperPagesControl, FormStepperReviewControl, FormStepperReviewer, FormStepperView, FullNameControl, FullNameControlReview, FullNameDobControl, FullNameDobReviewControl, FullNameDobTester, FullNameReviewControl, FullNameTester, GoAArrayControlRenderer, GoAArrayControlReviewRenderer, GoAArrayControlTester, GoABaseInputReviewComponent, GoABaseRenderers, GoABaseReviewRenderers, GoABaseTableReviewRenderers, GoABooleanControl, GoABooleanControlTester, GoABooleanRadioControl, GoABooleanRadioControlTester, GoACalculationControl, GoACalculationControlTester, GoACells, GoACheckoutGroupControlTester, GoAContractInfoControlReview, GoADateControl, GoADateControlTester, GoADateInput, GoADateTimeControl, GoADateTimeControlTester, GoADateTimeInput, GoAEmailControl, GoAEmailControlTester, GoAEmailInput, GoAEnumCheckboxGroupControl, GoAEnumControl, GoAEnumControlTester, GoAEnumRadioGroupControl, GoAInputBaseControl, GoAInputBaseFullNameControlReview, GoAInputBaseFullNameDobControlReview, GoAInputBaseReviewControl, GoAInputBaseTableReview, GoAInputBaseTableReviewControl, GoAInputDateControl, GoAInputDateTimeControl, GoAInputEmailControl, GoAInputIntegerControl, GoAInputNumberControl, GoAInputText, GoAInputTextControl, GoAInputTimeControl, GoAIntegerControl, GoAIntegerControlTester, GoAListWithDetailsControlRenderer, GoAListWithDetailsTester, GoANumberControl, GoANumberControlTester, GoANumberInput, GoAPhoneNumberControl, GoAPhoneNumberWithTypeControl, GoAPrimitiveArrayRenderer, GoAPrimitiveArrayTester, GoARadioGroupControlTester, GoARenderers, GoAReviewRenderers, GoATextControl, GoATextControlTester, GoATimeControl, GoATimeControlTester, GoATimeInput, GoInputBaseReview, GoabInputBasePhoneNumberReviewControl, GoabInputBasePhoneNumberWithTypeReviewControl, GoabInputInteger, InnerGoAInputText, JsonFormContext, JsonFormRegisterProvider, JsonFormsRegisterContext, ListWithDetailsControl, MultiLineText, MultiLineTextControl, MultiLineTextControlInput, MultiLineTextControlTester, NAVIGATION_PARAMS, OPTION_DEFINITIONS, PHONE_REGEX, PhoneGrid, PhoneNumberControl, PhoneNumberReviewControl, PhoneNumberTester, PhoneNumberWithTypeControl, PhoneNumberWithTypeReviewControl, PhoneNumberWithTypeTester, PrimitiveArrayControl, REVIEW_CHANGE_ACTION, RadioGroup, ReviewRenderContext, ReviewRenderProvider, USER_FIELD_DEFINITIONS, autoPopulatePropertiesMonaco, autoPopulateValue, categoriesAreValid, createAutoPopulateMiddleware, createDefaultAjv, enumControl, errMalformedDate, fetchRegisterConfigFromOptions$1 as fetchRegisterConfigFromOptions, formatSin, getAutoPopulateControls, getAutoPopulatedData, getByJsonPointer, getCategoryScopes, getExternalFormPath, getNavigationTargetFromParams, getNavigationTargets, hasDataInScopes, hasDataValue, isAddressLookup, isContactInformation, isFullName, isFullNameDoB, isPhoneNumberWithType, mergeAutoPopulatedData, predictiveOptionsMonaco, registerReducer, resolveRefs, toDateTimeLocalInputValue, tryResolveRefs, useRegisterUser, useReviewContext, useShowChangeButtons };
20914
+ export { ADD_DATALIST_ACTION, ADD_NO_ANONYMOUS_ACTION, ADD_REGISTER_DATA_ACTION, ADD_REGISTER_DATA_ERROR, ADD_USER_ACTION, AUTO_POPULATE_SOURCES, AddressLookUpControl, AddressLookUpControlReview, AddressLookUpTester, AddressLoopUpControlTableReview, ArrayControl, ArrayControlBase, ArrayControlReview, BooleanComponent, BooleanControl, BooleanRadioComponent, BooleanRadioControl, CategorizationPagesRendererTester, CategorizationStepperRendererTester, CheckboxGroup, ContactInformationTester, ContextProviderC, ContextProviderClass, ContextProviderFactory, ContractInfoControl, ContractInfoControlReview, EnumCheckboxControl, EnumRadioControl, EnumSelect, FileUploader, FileUploaderReview, FileUploaderTester, FormPageStepper, FormPagesView, FormStepper, FormStepperControl, FormStepperPagesControl, FormStepperReviewControl, FormStepperReviewer, FormStepperView, FullNameControl, FullNameControlReview, FullNameDobControl, FullNameDobReviewControl, FullNameDobTester, FullNameReviewControl, FullNameTester, GoAArrayControlRenderer, GoAArrayControlReviewRenderer, GoAArrayControlTester, GoABaseInputReviewComponent, GoABaseRenderers, GoABaseReviewRenderers, GoABaseTableReviewRenderers, GoABooleanControl, GoABooleanControlTester, GoABooleanRadioControl, GoABooleanRadioControlTester, GoACalculationControl, GoACalculationControlTester, GoACells, GoACheckoutGroupControlTester, GoAContractInfoControlReview, GoADateControl, GoADateControlTester, GoADateInput, GoADateTimeControl, GoADateTimeControlTester, GoADateTimeInput, GoAEmailControl, GoAEmailControlTester, GoAEmailInput, GoAEnumCheckboxGroupControl, GoAEnumControl, GoAEnumControlTester, GoAEnumRadioGroupControl, GoAInputBaseControl, GoAInputBaseFullNameControlReview, GoAInputBaseFullNameDobControlReview, GoAInputBaseReviewControl, GoAInputBaseTableReview, GoAInputBaseTableReviewControl, GoAInputDateControl, GoAInputDateTimeControl, GoAInputEmailControl, GoAInputIntegerControl, GoAInputNumberControl, GoAInputText, GoAInputTextControl, GoAInputTimeControl, GoAIntegerControl, GoAIntegerControlTester, GoAListWithDetailsControlRenderer, GoAListWithDetailsTester, GoANumberControl, GoANumberControlTester, GoANumberInput, GoAPhoneNumberControl, GoAPhoneNumberWithTypeControl, GoAPrimitiveArrayRenderer, GoAPrimitiveArrayTester, GoARadioGroupControlTester, GoARenderers, GoAReviewRenderers, GoATextControl, GoATextControlTester, GoATimeControl, GoATimeControlTester, GoATimeInput, GoInputBaseReview, GoabInputBasePhoneNumberReviewControl, GoabInputBasePhoneNumberWithTypeReviewControl, GoabInputInteger, InnerGoAInputText, JsonFormContext, JsonFormRegisterProvider, JsonFormsRegisterContext, ListWithDetailsControl, MultiLineText, MultiLineTextControl, MultiLineTextControlInput, MultiLineTextControlTester, NAVIGATION_PARAMS, OPTION_DEFINITIONS, PHONE_REGEX, PhoneGrid, PhoneNumberControl, PhoneNumberReviewControl, PhoneNumberTester, PhoneNumberWithTypeControl, PhoneNumberWithTypeReviewControl, PhoneNumberWithTypeTester, PrimitiveArrayControl, REVIEW_CHANGE_ACTION, RadioGroup, ReviewRenderContext, ReviewRenderProvider, USER_FIELD_DEFINITIONS, autoPopulatePropertiesMonaco, autoPopulateValue, categoriesAreValid, createAutoPopulateMiddleware, createDefaultAjv, enumControl, errMalformedDate, fetchRegisterConfigFromOptions$1 as fetchRegisterConfigFromOptions, getAutoPopulateControls, getAutoPopulatedData, getByJsonPointer, getCategoryScopes, getExternalFormPath, getNavigationTargetFromParams, getNavigationTargets, hasDataInScopes, hasDataValue, isAddressLookup, isContactInformation, isFullName, isFullNameDoB, isPhoneNumberWithType, mergeAutoPopulatedData, predictiveOptionsMonaco, registerReducer, resolveRefs, toDateTimeLocalInputValue, tryResolveRefs, useRegisterUser, useReviewContext, useShowChangeButtons };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/jsonforms-components",
3
- "version": "2.74.1",
3
+ "version": "2.75.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Government of Alberta - React renderers for JSON Forms based on the design system.",
6
6
  "repository": "https://github.com/GovAlta/adsp-monorepo",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-03T22:56:39.161Z",
4
- "sourceCommit": "169c6d54a69229f169a5ceae66539c141522e350",
3
+ "generatedAt": "2026-09-08T14:57:25.870Z",
4
+ "sourceCommit": "4bfd7a826d9a48271e8d7b7e77fbee5e560c102f",
5
5
  "sourcePath": "libs/jsonforms-components/src/index.ts",
6
6
  "rendererCount": 33,
7
7
  "renderers": [
@@ -4,7 +4,6 @@ import { WithInputProps } from './type';
4
4
  import { RegisterConfig } from '../../Context/register';
5
5
  export type GoAInputTextProps = CellProps & WithClassname & WithInputProps;
6
6
  export declare function fetchRegisterConfigFromOptions(options: Record<string, unknown> | undefined): RegisterConfig | undefined;
7
- export declare const formatSin: (value: string) => string;
8
7
  export declare const GoAInputText: (props: GoAInputTextProps) => JSX.Element;
9
8
  export declare const InnerGoAInputText: (props: GoAInputTextProps) => JSX.Element;
10
9
  export declare const GoATextControl: (props: ControlProps) => import("react/jsx-runtime").JSX.Element;
@@ -4,6 +4,17 @@ export interface WithInputProps {
4
4
  isVisited?: boolean;
5
5
  setIsVisited?: () => void;
6
6
  }
7
+ /**
8
+ * UI schema options recognized by the text input control.
9
+ */
10
+ export interface InputControlOptions {
11
+ /**
12
+ * Mask template applied to the value on blur, e.g. "(###) ###-####".
13
+ * '#' consumes one input character; every other character is inserted as a literal.
14
+ * Formatting only; validation is still driven by the JSON schema `pattern`.
15
+ */
16
+ formatPattern?: string;
17
+ }
7
18
  /**
8
19
  * Base event control props to handle event controls
9
20
  */
@@ -32,6 +32,10 @@ export declare const onKeyPressForTimeControl: (props: EventKeyPressControlProps
32
32
  * @param props - EventKeyPressControlProps
33
33
  */
34
34
  export declare const onKeyPressForDateControl: (props: EventKeyPressControlProps) => void;
35
+ /**
36
+ * Re-exported from ./patternForm to keep existing import paths stable.
37
+ */
38
+ export { applyFormatPattern } from './patternForm';
35
39
  /**
36
40
  * Helper function to process onBlur events for text controls.
37
41
  * @param props - EventBlurControlProps
@@ -0,0 +1,41 @@
1
+ export interface MaskPattern {
2
+ mask: string;
3
+ pattern: RegExp;
4
+ error: string;
5
+ allowedKeys?: RegExp;
6
+ }
7
+ export declare const DEFAULT_PATTERNS: Record<string, MaskPattern>;
8
+ export declare const MASK_PLACEHOLDERS: Set<string>;
9
+ export declare const MASK_CONTROL_KEYS: Set<string>;
10
+ export declare const shouldBlockKey: (key: string, allowedKeys?: RegExp) => boolean;
11
+ export declare const filterAllowedKeys: (value: string, allowedKeys?: RegExp) => string;
12
+ export declare const maskDigitCount: (mask: string) => number;
13
+ export declare const formatWithPattern: (value: string, mask: string) => string;
14
+ export declare const toMaskTemplate: (value: string, mask: string) => string;
15
+ export declare const caretAfterDigits: (display: string, digitCount: number) => number;
16
+ export interface MaskEdit {
17
+ display: string;
18
+ stored: string;
19
+ caret: number;
20
+ }
21
+ export declare const computeMaskEdit: (rawValue: string, caretIndex: number, mask: string) => MaskEdit;
22
+ export declare const isMaskFilled: (value: string, mask: string) => boolean;
23
+ export declare const maskPlaceholder: (mask: string) => string;
24
+ type MaskInputTarget = (HTMLInputElement & {
25
+ setSelectionRange?: (start: number, end: number) => void;
26
+ }) | undefined;
27
+ export declare const getMaskInputTarget: (detail: {
28
+ event?: Event;
29
+ }) => MaskInputTarget;
30
+ export declare const applyInPlaceEdit: (target: MaskInputTarget, edit: MaskEdit) => void;
31
+ /**
32
+ * Applies a mask template to a value, e.g. mask "(###) ###-####" turns "7801234567" into "(780) 123-4567".
33
+ * '#' consumes one content character from the input; every other character is a literal inserted into the output.
34
+ * The mask's literal characters are stripped from the input first so an already formatted value re-formats cleanly.
35
+ * Returns the original value unchanged when the mask is empty or no content characters remain.
36
+ * @param value - The raw input value
37
+ * @param mask - The mask template string
38
+ * @returns The masked value
39
+ */
40
+ export declare const applyFormatPattern: (value: string, mask: string | undefined) => string;
41
+ export {};
@@ -0,0 +1,21 @@
1
+ interface MaskChangeDetail {
2
+ value: string;
3
+ event?: Event;
4
+ }
5
+ interface MaskKeyPressDetail {
6
+ key: string;
7
+ event?: Event;
8
+ }
9
+ interface UseMaskedInputOptions {
10
+ mask: string;
11
+ inPlace: boolean;
12
+ data: unknown;
13
+ onCommit: (stored: string) => void;
14
+ }
15
+ interface UseMaskedInputResult {
16
+ value: string;
17
+ handleChange: (detail: MaskChangeDetail) => void;
18
+ handleKeyPress: (detail: MaskKeyPressDetail) => void;
19
+ }
20
+ export declare const useMaskedInput: ({ mask, inPlace, data, onCommit }: UseMaskedInputOptions) => UseMaskedInputResult;
21
+ export {};