@abgov/jsonforms-components 2.70.2 → 2.70.4
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
|
@@ -6,7 +6,7 @@ import axios from 'axios';
|
|
|
6
6
|
import get$1 from 'lodash/get';
|
|
7
7
|
import { toDataPath, INIT, UPDATE_CORE, isVisible, isEnabled, deriveLabelForUISchemaElement, rankWith, isStringControl, and, optionIs, isDateControl, isNumberControl, isIntegerControl, isDateTimeControl, isTimeControl, uiTypeIs, isControl as isControl$1, isEnumControl, or, isOneOfEnumControl, isBooleanControl, createDefaultValue, Paths, Resolve, schemaTypeIs, formatIs, getAjv, schemaMatches, isObjectArrayControl, isPrimitiveArrayControl, composePaths, hasType, isCategorization, isLayout } from '@jsonforms/core';
|
|
8
8
|
import * as _$c from 'lodash';
|
|
9
|
-
import ___default, { isEmpty, isObject as isObject$i } from 'lodash';
|
|
9
|
+
import ___default, { isEqual, isEmpty, isObject as isObject$i } from 'lodash';
|
|
10
10
|
import { useJsonForms, withJsonFormsControlProps, withJsonFormsEnumProps, withTranslateProps, JsonFormsDispatch, withJsonFormsAllOfProps, withJsonFormsArrayLayoutProps, withJsonFormsLayoutProps, withJsonFormsCellProps } from '@jsonforms/react';
|
|
11
11
|
import dompurify from 'dompurify';
|
|
12
12
|
import * as xss from 'xss';
|
|
@@ -8007,7 +8007,13 @@ const USER_FIELD_DEFINITIONS = {
|
|
|
8007
8007
|
}
|
|
8008
8008
|
};
|
|
8009
8009
|
const AUTO_POPULATE_ACTION_TYPES = new Set([INIT, UPDATE_CORE]);
|
|
8010
|
-
|
|
8010
|
+
// clean-code-ignore: RULE-19 — covered by ./autoPopulate.spec.tsx, colocated. The rule only looks
|
|
8011
|
+
// for a .test.ts sibling; adding one would duplicate the existing suite.
|
|
8012
|
+
// Only a value the user has never set is a populate target. An empty string is a real value here:
|
|
8013
|
+
// the text controls write '' when a field is cleared (see onChangeForInputControl), so treating it
|
|
8014
|
+
// as empty made auto-populate refill the field the instant it was emptied, and the user could
|
|
8015
|
+
// never clear it.
|
|
8016
|
+
const isEmptyAutoPopulateTarget = value => value === undefined || value === null;
|
|
8011
8017
|
const shouldAutoPopulate = actionType => AUTO_POPULATE_ACTION_TYPES.has(actionType);
|
|
8012
8018
|
const isAutoPopulateSource = value => typeof value === 'string' && AUTO_POPULATE_SOURCES.includes(value);
|
|
8013
8019
|
const autoPopulateValue = (user, props) => {
|
|
@@ -8812,7 +8818,75 @@ const GoATextControl = props => jsx(GoAInputBaseControl, Object.assign({}, props
|
|
|
8812
8818
|
const GoATextControlTester = rankWith(1, isStringControl);
|
|
8813
8819
|
const GoAInputTextControl = withJsonFormsControlProps(GoATextControl);
|
|
8814
8820
|
|
|
8815
|
-
const
|
|
8821
|
+
const COMMIT_DELAY = 300;
|
|
8822
|
+
/**
|
|
8823
|
+
* Keeps a live, per-keystroke mirror of a control's value and pushes it into the form data only
|
|
8824
|
+
* once the user pauses typing.
|
|
8825
|
+
*
|
|
8826
|
+
* The GoA web components keep the value in their own DOM and report changes back a macrotask
|
|
8827
|
+
* late, while their update path writes an incoming `value` prop straight back into the field
|
|
8828
|
+
* whenever it differs from what is in the box. So a render that commits a value older than what
|
|
8829
|
+
* has been typed since wipes out the characters in between. Committing on every keystroke
|
|
8830
|
+
* re-renders and revalidates the whole form, which makes that window wide enough to swallow
|
|
8831
|
+
* characters; debouncing the commit keeps it to a single local render.
|
|
8832
|
+
*
|
|
8833
|
+
* The echo of our own commit is ignored while further edits are outstanding, so data arriving
|
|
8834
|
+
* back from the form can never overwrite what the user has typed in the meantime.
|
|
8835
|
+
*
|
|
8836
|
+
* @param externalValue the value held in the form data; must be referentially stable between
|
|
8837
|
+
* renders (memoize it when it is an object), otherwise the sync effect runs on every render
|
|
8838
|
+
* @param commit pushes a settled value into the form data
|
|
8839
|
+
* @param delay how long to wait for typing to stop before committing
|
|
8840
|
+
*/
|
|
8841
|
+
function useDebouncedCommit(externalValue, commit, delay = COMMIT_DELAY) {
|
|
8842
|
+
const [value, setLocalValue] = useState(externalValue);
|
|
8843
|
+
// Set while the user has edits we have not pushed upstream yet.
|
|
8844
|
+
const pendingRef = useRef(null);
|
|
8845
|
+
const autoCommitRef = useRef(true);
|
|
8846
|
+
const commitRef = useRef(commit);
|
|
8847
|
+
useEffect(() => {
|
|
8848
|
+
commitRef.current = commit;
|
|
8849
|
+
});
|
|
8850
|
+
const debouncedValue = useDebounce(value, delay);
|
|
8851
|
+
const flush = useCallback(() => {
|
|
8852
|
+
const pending = pendingRef.current;
|
|
8853
|
+
if (pending === null) {
|
|
8854
|
+
return;
|
|
8855
|
+
}
|
|
8856
|
+
pendingRef.current = null;
|
|
8857
|
+
autoCommitRef.current = true;
|
|
8858
|
+
commitRef.current(pending);
|
|
8859
|
+
}, []);
|
|
8860
|
+
const setValue = useCallback((next, options) => {
|
|
8861
|
+
pendingRef.current = next;
|
|
8862
|
+
autoCommitRef.current = (options == null ? void 0 : options.autoCommit) !== false;
|
|
8863
|
+
setLocalValue(next);
|
|
8864
|
+
}, []);
|
|
8865
|
+
useEffect(() => {
|
|
8866
|
+
if (!autoCommitRef.current) {
|
|
8867
|
+
return;
|
|
8868
|
+
}
|
|
8869
|
+
// Only commit once the debounce has caught up with the latest keystroke.
|
|
8870
|
+
if (pendingRef.current === null || pendingRef.current !== debouncedValue) {
|
|
8871
|
+
return;
|
|
8872
|
+
}
|
|
8873
|
+
flush();
|
|
8874
|
+
}, [debouncedValue, flush]);
|
|
8875
|
+
useEffect(() => {
|
|
8876
|
+
if (pendingRef.current !== null) {
|
|
8877
|
+
return;
|
|
8878
|
+
}
|
|
8879
|
+
setLocalValue(current => isEqual(current, externalValue) ? current : externalValue);
|
|
8880
|
+
}, [externalValue]);
|
|
8881
|
+
// Don't drop the tail of what was typed when the control goes away, e.g. on stepper navigation.
|
|
8882
|
+
useEffect(() => flush, [flush]);
|
|
8883
|
+
return {
|
|
8884
|
+
value,
|
|
8885
|
+
setValue,
|
|
8886
|
+
flush
|
|
8887
|
+
};
|
|
8888
|
+
}
|
|
8889
|
+
|
|
8816
8890
|
const MultiLineText = props => {
|
|
8817
8891
|
var _uischema$options$com, _uischema$options, _uischema$options2, _uischema$options3, _uischema$options$com2, _uischema$options4, _uischema$options5;
|
|
8818
8892
|
const {
|
|
@@ -8828,29 +8902,29 @@ const MultiLineText = props => {
|
|
|
8828
8902
|
errors,
|
|
8829
8903
|
setIsVisited
|
|
8830
8904
|
} = props;
|
|
8831
|
-
const
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
},
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
onChangeForInputControl({
|
|
8840
|
-
value: debouncedValue,
|
|
8841
|
-
controlProps: props
|
|
8842
|
-
});
|
|
8843
|
-
}
|
|
8844
|
-
}, [debouncedValue]);
|
|
8905
|
+
const {
|
|
8906
|
+
value: textAreaValue,
|
|
8907
|
+
setValue: setTextAreaValue,
|
|
8908
|
+
flush: flushTextArea
|
|
8909
|
+
} = useDebouncedCommit(data || '', value => onChangeForInputControl({
|
|
8910
|
+
value,
|
|
8911
|
+
controlProps: props
|
|
8912
|
+
}));
|
|
8845
8913
|
const appliedUiSchemaOptions = Object.assign({}, config, uischema == null ? void 0 : uischema.options);
|
|
8846
8914
|
const placeholder = (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.placeholder) || (schema == null ? void 0 : schema.description) || '';
|
|
8847
8915
|
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%';
|
|
8848
8916
|
const autoCapitalize = (uischema == null || (_uischema$options2 = uischema.options) == null || (_uischema$options2 = _uischema$options2.componentProps) == null ? void 0 : _uischema$options2.autoCapitalize) === true || (uischema == null || (_uischema$options3 = uischema.options) == null ? void 0 : _uischema$options3.autoCapitalize) === true;
|
|
8849
8917
|
const readOnly = (_uischema$options$com2 = uischema == null || (_uischema$options4 = uischema.options) == null || (_uischema$options4 = _uischema$options4.componentProps) == null ? void 0 : _uischema$options4.readOnly) != null ? _uischema$options$com2 : false;
|
|
8850
8918
|
const textAreaName = `${label || path}-text-area` || '';
|
|
8919
|
+
const applyCapitalization = value => autoCapitalize ? value.toUpperCase() : value;
|
|
8920
|
+
const markVisited = () => {
|
|
8921
|
+
if (isVisited === false && setIsVisited) {
|
|
8922
|
+
setIsVisited();
|
|
8923
|
+
}
|
|
8924
|
+
};
|
|
8851
8925
|
const txtAreaComponent = jsx(GoabTextArea, Object.assign({
|
|
8852
8926
|
error: isVisited && errors.length > 0,
|
|
8853
|
-
value:
|
|
8927
|
+
value: textAreaValue,
|
|
8854
8928
|
disabled: !enabled,
|
|
8855
8929
|
readOnly: readOnly,
|
|
8856
8930
|
placeholder: placeholder,
|
|
@@ -8859,21 +8933,21 @@ const MultiLineText = props => {
|
|
|
8859
8933
|
width: width,
|
|
8860
8934
|
// Note: Paul Jan-09-2023. The latest ui-component come with the maxCount. We need to uncomment the following line when the component is updated
|
|
8861
8935
|
// maxCount={schema.maxLength || 256}
|
|
8936
|
+
// The component raises _change from its input event, so unlike _keyPress (keyup) this also
|
|
8937
|
+
// covers pasting, IME composition and held-key repeats.
|
|
8938
|
+
onChange: detail => {
|
|
8939
|
+
setTextAreaValue(applyCapitalization(detail.value));
|
|
8940
|
+
markVisited();
|
|
8941
|
+
},
|
|
8862
8942
|
onKeyPress: detail => {
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
if (isVisited === false && setIsVisited) {
|
|
8866
|
-
setIsVisited();
|
|
8867
|
-
}
|
|
8943
|
+
setTextAreaValue(applyCapitalization(detail.value));
|
|
8944
|
+
markVisited();
|
|
8868
8945
|
},
|
|
8869
8946
|
onBlur: detail => {
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
}
|
|
8875
|
-
},
|
|
8876
|
-
onChange: detail => {}
|
|
8947
|
+
setTextAreaValue(applyCapitalization(detail.value));
|
|
8948
|
+
flushTextArea();
|
|
8949
|
+
markVisited();
|
|
8950
|
+
}
|
|
8877
8951
|
}, uischema == null || (_uischema$options5 = uischema.options) == null ? void 0 : _uischema$options5.componentProps));
|
|
8878
8952
|
return txtAreaComponent;
|
|
8879
8953
|
};
|
|
@@ -18517,21 +18591,19 @@ const filterSuggestionsWithoutAddressCount = suggestions => {
|
|
|
18517
18591
|
});
|
|
18518
18592
|
};
|
|
18519
18593
|
const mapSuggestionToAddress = suggestion => {
|
|
18520
|
-
|
|
18521
|
-
const
|
|
18522
|
-
const textParts =
|
|
18523
|
-
|
|
18524
|
-
|
|
18525
|
-
|
|
18526
|
-
|
|
18527
|
-
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
const
|
|
18531
|
-
const
|
|
18532
|
-
const
|
|
18533
|
-
const subdivisionCode = provinceAndPostalCode[0];
|
|
18534
|
-
const postalCode = descriptionParts[2].trim();
|
|
18594
|
+
var _suggestion$Text$trim, _suggestion$Text, _suggestion$Descripti;
|
|
18595
|
+
const text = (_suggestion$Text$trim = suggestion == null || (_suggestion$Text = suggestion.Text) == null ? void 0 : _suggestion$Text.trim()) != null ? _suggestion$Text$trim : '';
|
|
18596
|
+
const textParts = text.split(' ');
|
|
18597
|
+
const hasSuite = /(Suite|Apt|Unit|#)+/i.test(text) && textParts.length > 1;
|
|
18598
|
+
const addressLine1 = hasSuite ? text.replace(textParts[0], '').trim() : text;
|
|
18599
|
+
const addressLine2 = hasSuite ? textParts[0].trim() : '';
|
|
18600
|
+
// Descriptions usually read "municipality, province, postalCode", but the lookup also returns
|
|
18601
|
+
// "municipality, province postalCode" and, for partial matches, drops the postal code entirely.
|
|
18602
|
+
// Reading the parts positionally used to throw on anything but the three part form.
|
|
18603
|
+
const descriptionParts = ((_suggestion$Descripti = suggestion == null ? void 0 : suggestion.Description) != null ? _suggestion$Descripti : '').split(',').map(part => part.trim());
|
|
18604
|
+
const [municipality = '', provinceAndPostalCode = '', trailingPostalCode = ''] = descriptionParts;
|
|
18605
|
+
const [subdivisionCode = '', ...postalCodeParts] = provinceAndPostalCode.split(' ').filter(Boolean);
|
|
18606
|
+
const postalCode = postalCodeParts.length > 0 ? postalCodeParts.join(' ') : trailingPostalCode;
|
|
18535
18607
|
return {
|
|
18536
18608
|
addressLine1,
|
|
18537
18609
|
addressLine2,
|
|
@@ -18633,17 +18705,18 @@ const AddressLookUpControl = props => {
|
|
|
18633
18705
|
const [open, setOpen] = useState(false);
|
|
18634
18706
|
const addressContainerRef = useRef(null);
|
|
18635
18707
|
const label = typeof (uischema == null ? void 0 : uischema.label) === 'string' && uischema.label ? uischema.label : '';
|
|
18636
|
-
const
|
|
18708
|
+
const normalizedData = useMemo(() => normalizeAddressData(data, isAlbertaAddress), [data, isAlbertaAddress]);
|
|
18709
|
+
const {
|
|
18710
|
+
value: address,
|
|
18711
|
+
setValue: setAddress,
|
|
18712
|
+
flush: flushAddress
|
|
18713
|
+
} = useDebouncedCommit(normalizedData, updatedAddress => handleChange(path, updatedAddress));
|
|
18637
18714
|
const [searchTerm, setSearchTerm] = useState('');
|
|
18638
18715
|
const [suggestions, setSuggestions] = useState([]);
|
|
18639
18716
|
const [loading, setLoading] = useState(false);
|
|
18640
18717
|
const [errors, setErrors] = useState({});
|
|
18641
18718
|
const requiredFields = schema.required;
|
|
18642
18719
|
const [dropdownSelected, setDropdownSelected] = useState(false);
|
|
18643
|
-
const updateFormData = updatedAddress => {
|
|
18644
|
-
handleChange(path, updatedAddress);
|
|
18645
|
-
};
|
|
18646
|
-
const debouncedRenderAddress = useDebounce(searchTerm, 500);
|
|
18647
18720
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
18648
18721
|
const dropdownRef = useRef(null);
|
|
18649
18722
|
const debouncedTerm = useDebounce(searchTerm, 500);
|
|
@@ -18651,39 +18724,35 @@ const AddressLookUpControl = props => {
|
|
|
18651
18724
|
const lastQueryRef = useRef('');
|
|
18652
18725
|
const cacheRef = useRef(new Map());
|
|
18653
18726
|
const spinnerTimerRef = useRef(null);
|
|
18654
|
-
|
|
18655
|
-
const nextAddress = normalizeAddressData(data, isAlbertaAddress);
|
|
18656
|
-
setAddress(currentAddress => {
|
|
18657
|
-
const keys = ['addressLine1', 'addressLine2', 'municipality', 'postalCode', 'country', 'subdivisionCode'];
|
|
18658
|
-
const unchanged = keys.every(key => currentAddress[key] === nextAddress[key]);
|
|
18659
|
-
return unchanged ? currentAddress : nextAddress;
|
|
18660
|
-
});
|
|
18661
|
-
}, [data, isAlbertaAddress]);
|
|
18662
|
-
const handleInputChange = (field, value) => {
|
|
18727
|
+
const handleInputChange = (field, value, options) => {
|
|
18663
18728
|
var _schema$errorMessage;
|
|
18664
|
-
|
|
18665
|
-
setDropdownSelected(false);
|
|
18666
|
-
}
|
|
18667
|
-
let newAddress;
|
|
18729
|
+
let newValue = value;
|
|
18668
18730
|
const postalCodeErrorMessage = (_schema$errorMessage = schema.errorMessage) == null || (_schema$errorMessage = _schema$errorMessage.properties) == null ? void 0 : _schema$errorMessage.postalCode;
|
|
18669
18731
|
if (field === 'postalCode') {
|
|
18670
18732
|
const validatePc = validatePostalCode(value);
|
|
18671
|
-
setErrors(handlePostalCodeValidation(validatePc, postalCodeErrorMessage ? postalCodeErrorMessage : '', value,
|
|
18672
|
-
|
|
18673
|
-
newAddress = Object.assign({}, address, {
|
|
18674
|
-
[field]: value.toUpperCase()
|
|
18675
|
-
});
|
|
18733
|
+
setErrors(currentErrors => handlePostalCodeValidation(validatePc, postalCodeErrorMessage != null ? postalCodeErrorMessage : '', value, currentErrors));
|
|
18734
|
+
newValue = formatPostalCode(value).toUpperCase();
|
|
18676
18735
|
} else {
|
|
18677
|
-
|
|
18678
|
-
|
|
18736
|
+
setErrors(currentErrors => {
|
|
18737
|
+
if (!(field in currentErrors)) {
|
|
18738
|
+
return currentErrors;
|
|
18739
|
+
}
|
|
18740
|
+
const nextErrors = Object.assign({}, currentErrors);
|
|
18741
|
+
delete nextErrors[field];
|
|
18742
|
+
return nextErrors;
|
|
18679
18743
|
});
|
|
18680
|
-
delete errors[field];
|
|
18681
18744
|
}
|
|
18682
|
-
|
|
18745
|
+
const newAddress = Object.assign({}, address, {
|
|
18746
|
+
[field]: newValue
|
|
18747
|
+
});
|
|
18748
|
+
if (newValue === '') {
|
|
18683
18749
|
delete newAddress[field];
|
|
18684
18750
|
}
|
|
18685
|
-
setAddress(newAddress);
|
|
18686
|
-
|
|
18751
|
+
setAddress(newAddress, options);
|
|
18752
|
+
// Picking a province isn't typing, so there is nothing to wait for.
|
|
18753
|
+
if (field === 'subdivisionCode') {
|
|
18754
|
+
flushAddress();
|
|
18755
|
+
}
|
|
18687
18756
|
};
|
|
18688
18757
|
const renderHelp = () => {
|
|
18689
18758
|
return jsx(HelpContentComponent, Object.assign({}, props, {
|
|
@@ -18691,70 +18760,46 @@ const AddressLookUpControl = props => {
|
|
|
18691
18760
|
showLabel: false
|
|
18692
18761
|
}));
|
|
18693
18762
|
};
|
|
18694
|
-
|
|
18695
|
-
const fetchSuggestions = async () => {
|
|
18696
|
-
/* istanbul ignore next */
|
|
18697
|
-
if (debouncedRenderAddress.length > 2 && dropdownSelected === false) {
|
|
18698
|
-
setLoading(true);
|
|
18699
|
-
setOpen(true);
|
|
18700
|
-
try {
|
|
18701
|
-
await fetchAddressSuggestions(formUrl, formatPostalCodeIfNeeded(searchTerm), isAlbertaAddress).then(response => {
|
|
18702
|
-
const suggestions = filterSuggestionsWithoutAddressCount(response);
|
|
18703
|
-
if (isAlbertaAddress) {
|
|
18704
|
-
setSuggestions(filterAlbertaAddresses(suggestions));
|
|
18705
|
-
} else {
|
|
18706
|
-
setSuggestions(suggestions);
|
|
18707
|
-
}
|
|
18708
|
-
setLoading(false);
|
|
18709
|
-
});
|
|
18710
|
-
// eslint-disable-next-line
|
|
18711
|
-
} catch (error) {
|
|
18712
|
-
if (error.name !== 'AbortError') {
|
|
18713
|
-
console.error('Address fetch failed:', error);
|
|
18714
|
-
}
|
|
18715
|
-
} finally {
|
|
18716
|
-
setLoading(false);
|
|
18717
|
-
}
|
|
18718
|
-
} else {
|
|
18719
|
-
setSuggestions([]);
|
|
18720
|
-
setOpen(false);
|
|
18721
|
-
}
|
|
18722
|
-
};
|
|
18723
|
-
fetchSuggestions();
|
|
18724
|
-
}, [debouncedTerm]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
18725
|
-
const handleDropdownChange = value => {
|
|
18763
|
+
const handleAddressLine1Change = value => {
|
|
18726
18764
|
setDropdownSelected(false);
|
|
18727
18765
|
setSearchTerm(value);
|
|
18766
|
+
// Mirror the keystroke so the field never renders a stale value, but leave the form data alone
|
|
18767
|
+
// until blur or a suggestion is picked. Committing mid-lookup re-renders the whole form while
|
|
18768
|
+
// the search is still debouncing, which is what closed the suggestions dropdown.
|
|
18769
|
+
handleInputChange('addressLine1', value, {
|
|
18770
|
+
autoCommit: false
|
|
18771
|
+
});
|
|
18728
18772
|
};
|
|
18729
18773
|
/* istanbul ignore next */
|
|
18730
18774
|
const handleSuggestionClick = suggestion => {
|
|
18731
18775
|
const suggestAddress = mapSuggestionToAddress(suggestion);
|
|
18732
18776
|
setAddress(suggestAddress);
|
|
18733
|
-
|
|
18777
|
+
flushAddress();
|
|
18734
18778
|
setSuggestions([]);
|
|
18735
18779
|
setErrors({});
|
|
18736
18780
|
setDropdownSelected(true);
|
|
18737
18781
|
};
|
|
18738
18782
|
const handleRequiredFieldBlur = (name, value) => {
|
|
18739
|
-
var _ref, _ref2
|
|
18740
|
-
const
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
18783
|
+
var _ref, _ref2;
|
|
18784
|
+
const liveInputValue = (_ref = (_ref2 = value != null ? value : address[name]) != null ? _ref2 : data == null ? void 0 : data[name]) != null ? _ref : '';
|
|
18785
|
+
setErrors(currentErrors => {
|
|
18786
|
+
const nextErrors = Object.assign({}, currentErrors);
|
|
18787
|
+
if (liveInputValue.trim() === '') {
|
|
18788
|
+
nextErrors[name] = `${getAddressLookupFieldLabel(name)} is required`;
|
|
18789
|
+
} else {
|
|
18790
|
+
delete nextErrors[name];
|
|
18791
|
+
}
|
|
18792
|
+
return nextErrors;
|
|
18793
|
+
});
|
|
18749
18794
|
setSuggestions([]);
|
|
18750
18795
|
setOpen(false);
|
|
18751
18796
|
if (name === 'addressLine1' && value !== undefined) {
|
|
18752
|
-
|
|
18797
|
+
setAddress(Object.assign({}, address, {
|
|
18753
18798
|
addressLine1: value
|
|
18754
|
-
});
|
|
18755
|
-
setAddress(newAddress);
|
|
18756
|
-
updateFormData(newAddress);
|
|
18799
|
+
}));
|
|
18757
18800
|
}
|
|
18801
|
+
// Leaving the field ends the burst of typing, so push what is there now.
|
|
18802
|
+
flushAddress();
|
|
18758
18803
|
};
|
|
18759
18804
|
useEffect(() => {
|
|
18760
18805
|
if (dropdownRef.current && activeIndex !== -1) {
|
|
@@ -18883,12 +18928,14 @@ const AddressLookUpControl = props => {
|
|
|
18883
18928
|
ariaLabel: 'address-form-address1',
|
|
18884
18929
|
placeholder: "Start typing the first line of your address, required.",
|
|
18885
18930
|
value: (address == null ? void 0 : address.addressLine1) || '',
|
|
18886
|
-
onChange: detail =>
|
|
18931
|
+
onChange: detail => handleAddressLine1Change(detail.value),
|
|
18887
18932
|
onBlur: detail => handleRequiredFieldBlur(detail.name, detail.value),
|
|
18888
18933
|
width: "100%",
|
|
18889
18934
|
onKeyPress: detail => {
|
|
18890
18935
|
if (open) {
|
|
18891
|
-
const newIndex = handleAddressKeyDown(detail.key, detail.value, activeIndex, suggestions,
|
|
18936
|
+
const newIndex = handleAddressKeyDown(detail.key, detail.value, activeIndex, suggestions,
|
|
18937
|
+
// Navigating the suggestions must never blank out what has been typed.
|
|
18938
|
+
val => typeof val === 'string' && handleInputChange('addressLine1', val), suggestion => handleSuggestionClick(suggestion));
|
|
18892
18939
|
setActiveIndex(newIndex);
|
|
18893
18940
|
}
|
|
18894
18941
|
}
|
|
@@ -18941,7 +18988,8 @@ const NameInputs = ({
|
|
|
18941
18988
|
requiredFields,
|
|
18942
18989
|
disabled,
|
|
18943
18990
|
errors,
|
|
18944
|
-
onFieldBlur
|
|
18991
|
+
onFieldBlur,
|
|
18992
|
+
onFieldCommit
|
|
18945
18993
|
}) => {
|
|
18946
18994
|
var _displayedErrors$firs, _displayedErrors$last;
|
|
18947
18995
|
const [internalErrors, setInternalErrors] = useState({});
|
|
@@ -18995,6 +19043,7 @@ const NameInputs = ({
|
|
|
18995
19043
|
};
|
|
18996
19044
|
const displayedErrors = errors != null ? errors : internalErrors;
|
|
18997
19045
|
const handleBlur = name => {
|
|
19046
|
+
onFieldCommit == null || onFieldCommit();
|
|
18998
19047
|
if (onFieldBlur) {
|
|
18999
19048
|
onFieldBlur(name);
|
|
19000
19049
|
} else {
|
|
@@ -19041,6 +19090,7 @@ const NameInputs = ({
|
|
|
19041
19090
|
ariaLabel: 'name-form-middle-name',
|
|
19042
19091
|
value: middleName || '',
|
|
19043
19092
|
onChange: detail => handleInputChange(detail.name, detail.value),
|
|
19093
|
+
onBlur: () => onFieldCommit == null ? void 0 : onFieldCommit(),
|
|
19044
19094
|
width: "100%"
|
|
19045
19095
|
})
|
|
19046
19096
|
}), jsx(GoabFormItem, {
|
|
@@ -19131,24 +19181,19 @@ const FullNameControl = props => {
|
|
|
19131
19181
|
uischema
|
|
19132
19182
|
} = props;
|
|
19133
19183
|
const requiredFields = schema.required;
|
|
19134
|
-
const [nameData, setNameData] = useState(normalizeNameData(data));
|
|
19135
19184
|
const controlRef = useRef(null);
|
|
19136
19185
|
const formStepperCtx = useContext(JsonFormsStepperContext);
|
|
19137
19186
|
const stepperState = formStepperCtx == null || formStepperCtx.selectStepperState == null ? void 0 : formStepperCtx.selectStepperState();
|
|
19138
|
-
const
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
setNameData(currentData => currentData.firstName === nextData.firstName && currentData.middleName === nextData.middleName && currentData.lastName === nextData.lastName ? currentData : nextData);
|
|
19145
|
-
}, [data]);
|
|
19187
|
+
const normalizedData = useMemo(() => normalizeNameData(data), [data]);
|
|
19188
|
+
const {
|
|
19189
|
+
value: nameData,
|
|
19190
|
+
setValue: setNameData,
|
|
19191
|
+
flush: flushName
|
|
19192
|
+
} = useDebouncedCommit(normalizedData, updatedData => handleChange(path, Object.fromEntries(Object.entries(updatedData).filter(([, value]) => value !== ''))));
|
|
19146
19193
|
const handleInputChange = (field, value) => {
|
|
19147
|
-
|
|
19194
|
+
setNameData(Object.assign({}, nameData, {
|
|
19148
19195
|
[field]: value
|
|
19149
|
-
});
|
|
19150
|
-
setNameData(updatedName);
|
|
19151
|
-
updateFormData(updatedName);
|
|
19196
|
+
}));
|
|
19152
19197
|
};
|
|
19153
19198
|
/* istanbul ignore next */
|
|
19154
19199
|
useEffect(() => {
|
|
@@ -19191,7 +19236,8 @@ const FullNameControl = props => {
|
|
|
19191
19236
|
handleInputChange: handleInputChange,
|
|
19192
19237
|
data: data,
|
|
19193
19238
|
disabled: !enabled,
|
|
19194
|
-
requiredFields: requiredFields
|
|
19239
|
+
requiredFields: requiredFields,
|
|
19240
|
+
onFieldCommit: flushName
|
|
19195
19241
|
})
|
|
19196
19242
|
})
|
|
19197
19243
|
});
|
|
@@ -19264,21 +19310,20 @@ const FullNameDobControl = props => {
|
|
|
19264
19310
|
maxDate: currentDate.toISOString().substring(0, 10)
|
|
19265
19311
|
};
|
|
19266
19312
|
};
|
|
19267
|
-
const
|
|
19313
|
+
const normalizedData = useMemo(() => normalizeNameDobData(data), [data]);
|
|
19314
|
+
const {
|
|
19315
|
+
value: formData,
|
|
19316
|
+
setValue: setFormData,
|
|
19317
|
+
flush: flushFormData
|
|
19318
|
+
} = useDebouncedCommit(normalizedData, updatedData => handleChange(path, Object.fromEntries(Object.entries(updatedData).filter(([, value]) => value !== ''))));
|
|
19268
19319
|
const updateFormData = updatedData => {
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
handleChange(path, nextData);
|
|
19320
|
+
setFormData(updatedData);
|
|
19321
|
+
flushFormData();
|
|
19272
19322
|
};
|
|
19273
|
-
useEffect(() => {
|
|
19274
|
-
const nextData = normalizeNameDobData(data);
|
|
19275
|
-
setFormData(currentData => currentData.firstName === nextData.firstName && currentData.middleName === nextData.middleName && currentData.lastName === nextData.lastName && currentData.dateOfBirth === nextData.dateOfBirth ? currentData : nextData);
|
|
19276
|
-
}, [data]);
|
|
19277
19323
|
const handleInputChange = (field, value) => {
|
|
19278
|
-
|
|
19324
|
+
setFormData(Object.assign({}, formData, {
|
|
19279
19325
|
[field]: value
|
|
19280
|
-
});
|
|
19281
|
-
updateFormData(updatedData);
|
|
19326
|
+
}));
|
|
19282
19327
|
};
|
|
19283
19328
|
// eslint-disable-next-line
|
|
19284
19329
|
const handleRequiredFieldBlur = (name, updatedData) => {
|
|
@@ -19342,7 +19387,8 @@ const FullNameDobControl = props => {
|
|
|
19342
19387
|
disabled: !enabled,
|
|
19343
19388
|
requiredFields: requiredFields,
|
|
19344
19389
|
errors: errors,
|
|
19345
|
-
onFieldBlur: handleRequiredFieldBlur
|
|
19390
|
+
onFieldBlur: handleRequiredFieldBlur,
|
|
19391
|
+
onFieldCommit: flushFormData
|
|
19346
19392
|
}), jsx(GoabGrid, {
|
|
19347
19393
|
minChildWidth: "0ch",
|
|
19348
19394
|
gap: "s",
|
|
@@ -19363,7 +19409,7 @@ const FullNameDobControl = props => {
|
|
|
19363
19409
|
value: formData == null ? void 0 : formData.dateOfBirth,
|
|
19364
19410
|
onChange: detail => handleInputChange(detail.name, detail.value),
|
|
19365
19411
|
onBlur: detail => {
|
|
19366
|
-
|
|
19412
|
+
flushFormData();
|
|
19367
19413
|
handleRequiredFieldBlur(detail.name);
|
|
19368
19414
|
},
|
|
19369
19415
|
width: "100%"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abgov/jsonforms-components",
|
|
3
|
-
"version": "2.70.
|
|
3
|
+
"version": "2.70.4",
|
|
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",
|
package/renderer-catalog.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-07-
|
|
4
|
-
"sourceCommit": "
|
|
3
|
+
"generatedAt": "2026-07-31T19:55:33.198Z",
|
|
4
|
+
"sourceCommit": "ef8ca17fd76dacfba02abd9bb758c3be5c38e9ae",
|
|
5
5
|
"sourcePath": "libs/jsonforms-components/src/index.ts",
|
|
6
6
|
"rendererCount": 33,
|
|
7
7
|
"renderers": [
|
|
@@ -9,6 +9,8 @@ interface NameInputsProps {
|
|
|
9
9
|
requiredFields?: string[];
|
|
10
10
|
errors?: Record<string, string>;
|
|
11
11
|
onFieldBlur?: (name: string) => void;
|
|
12
|
+
/** Called whenever a field loses focus, so the owner can push pending edits without waiting out the debounce. */
|
|
13
|
+
onFieldCommit?: () => void;
|
|
12
14
|
handleInputChange: (field: string, value: string) => void;
|
|
13
15
|
}
|
|
14
16
|
export declare const NameInputs: React.FC<NameInputsProps>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const COMMIT_DELAY = 300;
|
|
2
|
+
export interface SetValueOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the edit should be pushed upstream once typing stops. Pass false for a field whose
|
|
5
|
+
* owner commits on its own schedule; the value is still mirrored and still protected from
|
|
6
|
+
* incoming data until something calls flush.
|
|
7
|
+
*/
|
|
8
|
+
autoCommit?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface DebouncedCommit<T> {
|
|
11
|
+
/** The live value to bind to the input; updated on every keystroke. */
|
|
12
|
+
value: T;
|
|
13
|
+
/** Records a keystroke and, unless told otherwise, schedules the commit. */
|
|
14
|
+
setValue: (next: T, options?: SetValueOptions) => void;
|
|
15
|
+
/** Commits straight away, for events that are not typing (blur, selecting a suggestion, unmount). */
|
|
16
|
+
flush: () => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Keeps a live, per-keystroke mirror of a control's value and pushes it into the form data only
|
|
20
|
+
* once the user pauses typing.
|
|
21
|
+
*
|
|
22
|
+
* The GoA web components keep the value in their own DOM and report changes back a macrotask
|
|
23
|
+
* late, while their update path writes an incoming `value` prop straight back into the field
|
|
24
|
+
* whenever it differs from what is in the box. So a render that commits a value older than what
|
|
25
|
+
* has been typed since wipes out the characters in between. Committing on every keystroke
|
|
26
|
+
* re-renders and revalidates the whole form, which makes that window wide enough to swallow
|
|
27
|
+
* characters; debouncing the commit keeps it to a single local render.
|
|
28
|
+
*
|
|
29
|
+
* The echo of our own commit is ignored while further edits are outstanding, so data arriving
|
|
30
|
+
* back from the form can never overwrite what the user has typed in the meantime.
|
|
31
|
+
*
|
|
32
|
+
* @param externalValue the value held in the form data; must be referentially stable between
|
|
33
|
+
* renders (memoize it when it is an object), otherwise the sync effect runs on every render
|
|
34
|
+
* @param commit pushes a settled value into the form data
|
|
35
|
+
* @param delay how long to wait for typing to stop before committing
|
|
36
|
+
*/
|
|
37
|
+
export declare function useDebouncedCommit<T>(externalValue: T, commit: (value: T) => void, delay?: number): DebouncedCommit<T>;
|