@abgov/jsonforms-components 2.70.3 → 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';
|
|
@@ -8818,7 +8818,75 @@ const GoATextControl = props => jsx(GoAInputBaseControl, Object.assign({}, props
|
|
|
8818
8818
|
const GoATextControlTester = rankWith(1, isStringControl);
|
|
8819
8819
|
const GoAInputTextControl = withJsonFormsControlProps(GoATextControl);
|
|
8820
8820
|
|
|
8821
|
-
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
|
+
|
|
8822
8890
|
const MultiLineText = props => {
|
|
8823
8891
|
var _uischema$options$com, _uischema$options, _uischema$options2, _uischema$options3, _uischema$options$com2, _uischema$options4, _uischema$options5;
|
|
8824
8892
|
const {
|
|
@@ -8834,29 +8902,29 @@ const MultiLineText = props => {
|
|
|
8834
8902
|
errors,
|
|
8835
8903
|
setIsVisited
|
|
8836
8904
|
} = props;
|
|
8837
|
-
const
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
},
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
onChangeForInputControl({
|
|
8846
|
-
value: debouncedValue,
|
|
8847
|
-
controlProps: props
|
|
8848
|
-
});
|
|
8849
|
-
}
|
|
8850
|
-
}, [debouncedValue]);
|
|
8905
|
+
const {
|
|
8906
|
+
value: textAreaValue,
|
|
8907
|
+
setValue: setTextAreaValue,
|
|
8908
|
+
flush: flushTextArea
|
|
8909
|
+
} = useDebouncedCommit(data || '', value => onChangeForInputControl({
|
|
8910
|
+
value,
|
|
8911
|
+
controlProps: props
|
|
8912
|
+
}));
|
|
8851
8913
|
const appliedUiSchemaOptions = Object.assign({}, config, uischema == null ? void 0 : uischema.options);
|
|
8852
8914
|
const placeholder = (appliedUiSchemaOptions == null ? void 0 : appliedUiSchemaOptions.placeholder) || (schema == null ? void 0 : schema.description) || '';
|
|
8853
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%';
|
|
8854
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;
|
|
8855
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;
|
|
8856
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
|
+
};
|
|
8857
8925
|
const txtAreaComponent = jsx(GoabTextArea, Object.assign({
|
|
8858
8926
|
error: isVisited && errors.length > 0,
|
|
8859
|
-
value:
|
|
8927
|
+
value: textAreaValue,
|
|
8860
8928
|
disabled: !enabled,
|
|
8861
8929
|
readOnly: readOnly,
|
|
8862
8930
|
placeholder: placeholder,
|
|
@@ -8865,21 +8933,21 @@ const MultiLineText = props => {
|
|
|
8865
8933
|
width: width,
|
|
8866
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
|
|
8867
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
|
+
},
|
|
8868
8942
|
onKeyPress: detail => {
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
if (isVisited === false && setIsVisited) {
|
|
8872
|
-
setIsVisited();
|
|
8873
|
-
}
|
|
8943
|
+
setTextAreaValue(applyCapitalization(detail.value));
|
|
8944
|
+
markVisited();
|
|
8874
8945
|
},
|
|
8875
8946
|
onBlur: detail => {
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
}
|
|
8881
|
-
},
|
|
8882
|
-
onChange: detail => {}
|
|
8947
|
+
setTextAreaValue(applyCapitalization(detail.value));
|
|
8948
|
+
flushTextArea();
|
|
8949
|
+
markVisited();
|
|
8950
|
+
}
|
|
8883
8951
|
}, uischema == null || (_uischema$options5 = uischema.options) == null ? void 0 : _uischema$options5.componentProps));
|
|
8884
8952
|
return txtAreaComponent;
|
|
8885
8953
|
};
|
|
@@ -18523,21 +18591,19 @@ const filterSuggestionsWithoutAddressCount = suggestions => {
|
|
|
18523
18591
|
});
|
|
18524
18592
|
};
|
|
18525
18593
|
const mapSuggestionToAddress = suggestion => {
|
|
18526
|
-
|
|
18527
|
-
const
|
|
18528
|
-
const textParts =
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
|
|
18533
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
const
|
|
18537
|
-
const
|
|
18538
|
-
const
|
|
18539
|
-
const subdivisionCode = provinceAndPostalCode[0];
|
|
18540
|
-
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;
|
|
18541
18607
|
return {
|
|
18542
18608
|
addressLine1,
|
|
18543
18609
|
addressLine2,
|
|
@@ -18639,17 +18705,18 @@ const AddressLookUpControl = props => {
|
|
|
18639
18705
|
const [open, setOpen] = useState(false);
|
|
18640
18706
|
const addressContainerRef = useRef(null);
|
|
18641
18707
|
const label = typeof (uischema == null ? void 0 : uischema.label) === 'string' && uischema.label ? uischema.label : '';
|
|
18642
|
-
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));
|
|
18643
18714
|
const [searchTerm, setSearchTerm] = useState('');
|
|
18644
18715
|
const [suggestions, setSuggestions] = useState([]);
|
|
18645
18716
|
const [loading, setLoading] = useState(false);
|
|
18646
18717
|
const [errors, setErrors] = useState({});
|
|
18647
18718
|
const requiredFields = schema.required;
|
|
18648
18719
|
const [dropdownSelected, setDropdownSelected] = useState(false);
|
|
18649
|
-
const updateFormData = updatedAddress => {
|
|
18650
|
-
handleChange(path, updatedAddress);
|
|
18651
|
-
};
|
|
18652
|
-
const debouncedRenderAddress = useDebounce(searchTerm, 500);
|
|
18653
18720
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
18654
18721
|
const dropdownRef = useRef(null);
|
|
18655
18722
|
const debouncedTerm = useDebounce(searchTerm, 500);
|
|
@@ -18657,39 +18724,35 @@ const AddressLookUpControl = props => {
|
|
|
18657
18724
|
const lastQueryRef = useRef('');
|
|
18658
18725
|
const cacheRef = useRef(new Map());
|
|
18659
18726
|
const spinnerTimerRef = useRef(null);
|
|
18660
|
-
|
|
18661
|
-
const nextAddress = normalizeAddressData(data, isAlbertaAddress);
|
|
18662
|
-
setAddress(currentAddress => {
|
|
18663
|
-
const keys = ['addressLine1', 'addressLine2', 'municipality', 'postalCode', 'country', 'subdivisionCode'];
|
|
18664
|
-
const unchanged = keys.every(key => currentAddress[key] === nextAddress[key]);
|
|
18665
|
-
return unchanged ? currentAddress : nextAddress;
|
|
18666
|
-
});
|
|
18667
|
-
}, [data, isAlbertaAddress]);
|
|
18668
|
-
const handleInputChange = (field, value) => {
|
|
18727
|
+
const handleInputChange = (field, value, options) => {
|
|
18669
18728
|
var _schema$errorMessage;
|
|
18670
|
-
|
|
18671
|
-
setDropdownSelected(false);
|
|
18672
|
-
}
|
|
18673
|
-
let newAddress;
|
|
18729
|
+
let newValue = value;
|
|
18674
18730
|
const postalCodeErrorMessage = (_schema$errorMessage = schema.errorMessage) == null || (_schema$errorMessage = _schema$errorMessage.properties) == null ? void 0 : _schema$errorMessage.postalCode;
|
|
18675
18731
|
if (field === 'postalCode') {
|
|
18676
18732
|
const validatePc = validatePostalCode(value);
|
|
18677
|
-
setErrors(handlePostalCodeValidation(validatePc, postalCodeErrorMessage ? postalCodeErrorMessage : '', value,
|
|
18678
|
-
|
|
18679
|
-
newAddress = Object.assign({}, address, {
|
|
18680
|
-
[field]: value.toUpperCase()
|
|
18681
|
-
});
|
|
18733
|
+
setErrors(currentErrors => handlePostalCodeValidation(validatePc, postalCodeErrorMessage != null ? postalCodeErrorMessage : '', value, currentErrors));
|
|
18734
|
+
newValue = formatPostalCode(value).toUpperCase();
|
|
18682
18735
|
} else {
|
|
18683
|
-
|
|
18684
|
-
|
|
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;
|
|
18685
18743
|
});
|
|
18686
|
-
delete errors[field];
|
|
18687
18744
|
}
|
|
18688
|
-
|
|
18745
|
+
const newAddress = Object.assign({}, address, {
|
|
18746
|
+
[field]: newValue
|
|
18747
|
+
});
|
|
18748
|
+
if (newValue === '') {
|
|
18689
18749
|
delete newAddress[field];
|
|
18690
18750
|
}
|
|
18691
|
-
setAddress(newAddress);
|
|
18692
|
-
|
|
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
|
+
}
|
|
18693
18756
|
};
|
|
18694
18757
|
const renderHelp = () => {
|
|
18695
18758
|
return jsx(HelpContentComponent, Object.assign({}, props, {
|
|
@@ -18697,70 +18760,46 @@ const AddressLookUpControl = props => {
|
|
|
18697
18760
|
showLabel: false
|
|
18698
18761
|
}));
|
|
18699
18762
|
};
|
|
18700
|
-
|
|
18701
|
-
const fetchSuggestions = async () => {
|
|
18702
|
-
/* istanbul ignore next */
|
|
18703
|
-
if (debouncedRenderAddress.length > 2 && dropdownSelected === false) {
|
|
18704
|
-
setLoading(true);
|
|
18705
|
-
setOpen(true);
|
|
18706
|
-
try {
|
|
18707
|
-
await fetchAddressSuggestions(formUrl, formatPostalCodeIfNeeded(searchTerm), isAlbertaAddress).then(response => {
|
|
18708
|
-
const suggestions = filterSuggestionsWithoutAddressCount(response);
|
|
18709
|
-
if (isAlbertaAddress) {
|
|
18710
|
-
setSuggestions(filterAlbertaAddresses(suggestions));
|
|
18711
|
-
} else {
|
|
18712
|
-
setSuggestions(suggestions);
|
|
18713
|
-
}
|
|
18714
|
-
setLoading(false);
|
|
18715
|
-
});
|
|
18716
|
-
// eslint-disable-next-line
|
|
18717
|
-
} catch (error) {
|
|
18718
|
-
if (error.name !== 'AbortError') {
|
|
18719
|
-
console.error('Address fetch failed:', error);
|
|
18720
|
-
}
|
|
18721
|
-
} finally {
|
|
18722
|
-
setLoading(false);
|
|
18723
|
-
}
|
|
18724
|
-
} else {
|
|
18725
|
-
setSuggestions([]);
|
|
18726
|
-
setOpen(false);
|
|
18727
|
-
}
|
|
18728
|
-
};
|
|
18729
|
-
fetchSuggestions();
|
|
18730
|
-
}, [debouncedTerm]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
18731
|
-
const handleDropdownChange = value => {
|
|
18763
|
+
const handleAddressLine1Change = value => {
|
|
18732
18764
|
setDropdownSelected(false);
|
|
18733
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
|
+
});
|
|
18734
18772
|
};
|
|
18735
18773
|
/* istanbul ignore next */
|
|
18736
18774
|
const handleSuggestionClick = suggestion => {
|
|
18737
18775
|
const suggestAddress = mapSuggestionToAddress(suggestion);
|
|
18738
18776
|
setAddress(suggestAddress);
|
|
18739
|
-
|
|
18777
|
+
flushAddress();
|
|
18740
18778
|
setSuggestions([]);
|
|
18741
18779
|
setErrors({});
|
|
18742
18780
|
setDropdownSelected(true);
|
|
18743
18781
|
};
|
|
18744
18782
|
const handleRequiredFieldBlur = (name, value) => {
|
|
18745
|
-
var _ref, _ref2
|
|
18746
|
-
const
|
|
18747
|
-
|
|
18748
|
-
|
|
18749
|
-
|
|
18750
|
-
|
|
18751
|
-
|
|
18752
|
-
|
|
18753
|
-
|
|
18754
|
-
|
|
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
|
+
});
|
|
18755
18794
|
setSuggestions([]);
|
|
18756
18795
|
setOpen(false);
|
|
18757
18796
|
if (name === 'addressLine1' && value !== undefined) {
|
|
18758
|
-
|
|
18797
|
+
setAddress(Object.assign({}, address, {
|
|
18759
18798
|
addressLine1: value
|
|
18760
|
-
});
|
|
18761
|
-
setAddress(newAddress);
|
|
18762
|
-
updateFormData(newAddress);
|
|
18799
|
+
}));
|
|
18763
18800
|
}
|
|
18801
|
+
// Leaving the field ends the burst of typing, so push what is there now.
|
|
18802
|
+
flushAddress();
|
|
18764
18803
|
};
|
|
18765
18804
|
useEffect(() => {
|
|
18766
18805
|
if (dropdownRef.current && activeIndex !== -1) {
|
|
@@ -18889,12 +18928,14 @@ const AddressLookUpControl = props => {
|
|
|
18889
18928
|
ariaLabel: 'address-form-address1',
|
|
18890
18929
|
placeholder: "Start typing the first line of your address, required.",
|
|
18891
18930
|
value: (address == null ? void 0 : address.addressLine1) || '',
|
|
18892
|
-
onChange: detail =>
|
|
18931
|
+
onChange: detail => handleAddressLine1Change(detail.value),
|
|
18893
18932
|
onBlur: detail => handleRequiredFieldBlur(detail.name, detail.value),
|
|
18894
18933
|
width: "100%",
|
|
18895
18934
|
onKeyPress: detail => {
|
|
18896
18935
|
if (open) {
|
|
18897
|
-
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));
|
|
18898
18939
|
setActiveIndex(newIndex);
|
|
18899
18940
|
}
|
|
18900
18941
|
}
|
|
@@ -18947,7 +18988,8 @@ const NameInputs = ({
|
|
|
18947
18988
|
requiredFields,
|
|
18948
18989
|
disabled,
|
|
18949
18990
|
errors,
|
|
18950
|
-
onFieldBlur
|
|
18991
|
+
onFieldBlur,
|
|
18992
|
+
onFieldCommit
|
|
18951
18993
|
}) => {
|
|
18952
18994
|
var _displayedErrors$firs, _displayedErrors$last;
|
|
18953
18995
|
const [internalErrors, setInternalErrors] = useState({});
|
|
@@ -19001,6 +19043,7 @@ const NameInputs = ({
|
|
|
19001
19043
|
};
|
|
19002
19044
|
const displayedErrors = errors != null ? errors : internalErrors;
|
|
19003
19045
|
const handleBlur = name => {
|
|
19046
|
+
onFieldCommit == null || onFieldCommit();
|
|
19004
19047
|
if (onFieldBlur) {
|
|
19005
19048
|
onFieldBlur(name);
|
|
19006
19049
|
} else {
|
|
@@ -19047,6 +19090,7 @@ const NameInputs = ({
|
|
|
19047
19090
|
ariaLabel: 'name-form-middle-name',
|
|
19048
19091
|
value: middleName || '',
|
|
19049
19092
|
onChange: detail => handleInputChange(detail.name, detail.value),
|
|
19093
|
+
onBlur: () => onFieldCommit == null ? void 0 : onFieldCommit(),
|
|
19050
19094
|
width: "100%"
|
|
19051
19095
|
})
|
|
19052
19096
|
}), jsx(GoabFormItem, {
|
|
@@ -19137,24 +19181,19 @@ const FullNameControl = props => {
|
|
|
19137
19181
|
uischema
|
|
19138
19182
|
} = props;
|
|
19139
19183
|
const requiredFields = schema.required;
|
|
19140
|
-
const [nameData, setNameData] = useState(normalizeNameData(data));
|
|
19141
19184
|
const controlRef = useRef(null);
|
|
19142
19185
|
const formStepperCtx = useContext(JsonFormsStepperContext);
|
|
19143
19186
|
const stepperState = formStepperCtx == null || formStepperCtx.selectStepperState == null ? void 0 : formStepperCtx.selectStepperState();
|
|
19144
|
-
const
|
|
19145
|
-
|
|
19146
|
-
|
|
19147
|
-
|
|
19148
|
-
|
|
19149
|
-
|
|
19150
|
-
setNameData(currentData => currentData.firstName === nextData.firstName && currentData.middleName === nextData.middleName && currentData.lastName === nextData.lastName ? currentData : nextData);
|
|
19151
|
-
}, [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 !== ''))));
|
|
19152
19193
|
const handleInputChange = (field, value) => {
|
|
19153
|
-
|
|
19194
|
+
setNameData(Object.assign({}, nameData, {
|
|
19154
19195
|
[field]: value
|
|
19155
|
-
});
|
|
19156
|
-
setNameData(updatedName);
|
|
19157
|
-
updateFormData(updatedName);
|
|
19196
|
+
}));
|
|
19158
19197
|
};
|
|
19159
19198
|
/* istanbul ignore next */
|
|
19160
19199
|
useEffect(() => {
|
|
@@ -19197,7 +19236,8 @@ const FullNameControl = props => {
|
|
|
19197
19236
|
handleInputChange: handleInputChange,
|
|
19198
19237
|
data: data,
|
|
19199
19238
|
disabled: !enabled,
|
|
19200
|
-
requiredFields: requiredFields
|
|
19239
|
+
requiredFields: requiredFields,
|
|
19240
|
+
onFieldCommit: flushName
|
|
19201
19241
|
})
|
|
19202
19242
|
})
|
|
19203
19243
|
});
|
|
@@ -19270,21 +19310,20 @@ const FullNameDobControl = props => {
|
|
|
19270
19310
|
maxDate: currentDate.toISOString().substring(0, 10)
|
|
19271
19311
|
};
|
|
19272
19312
|
};
|
|
19273
|
-
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 !== ''))));
|
|
19274
19319
|
const updateFormData = updatedData => {
|
|
19275
|
-
|
|
19276
|
-
|
|
19277
|
-
handleChange(path, nextData);
|
|
19320
|
+
setFormData(updatedData);
|
|
19321
|
+
flushFormData();
|
|
19278
19322
|
};
|
|
19279
|
-
useEffect(() => {
|
|
19280
|
-
const nextData = normalizeNameDobData(data);
|
|
19281
|
-
setFormData(currentData => currentData.firstName === nextData.firstName && currentData.middleName === nextData.middleName && currentData.lastName === nextData.lastName && currentData.dateOfBirth === nextData.dateOfBirth ? currentData : nextData);
|
|
19282
|
-
}, [data]);
|
|
19283
19323
|
const handleInputChange = (field, value) => {
|
|
19284
|
-
|
|
19324
|
+
setFormData(Object.assign({}, formData, {
|
|
19285
19325
|
[field]: value
|
|
19286
|
-
});
|
|
19287
|
-
updateFormData(updatedData);
|
|
19326
|
+
}));
|
|
19288
19327
|
};
|
|
19289
19328
|
// eslint-disable-next-line
|
|
19290
19329
|
const handleRequiredFieldBlur = (name, updatedData) => {
|
|
@@ -19348,7 +19387,8 @@ const FullNameDobControl = props => {
|
|
|
19348
19387
|
disabled: !enabled,
|
|
19349
19388
|
requiredFields: requiredFields,
|
|
19350
19389
|
errors: errors,
|
|
19351
|
-
onFieldBlur: handleRequiredFieldBlur
|
|
19390
|
+
onFieldBlur: handleRequiredFieldBlur,
|
|
19391
|
+
onFieldCommit: flushFormData
|
|
19352
19392
|
}), jsx(GoabGrid, {
|
|
19353
19393
|
minChildWidth: "0ch",
|
|
19354
19394
|
gap: "s",
|
|
@@ -19369,7 +19409,7 @@ const FullNameDobControl = props => {
|
|
|
19369
19409
|
value: formData == null ? void 0 : formData.dateOfBirth,
|
|
19370
19410
|
onChange: detail => handleInputChange(detail.name, detail.value),
|
|
19371
19411
|
onBlur: detail => {
|
|
19372
|
-
|
|
19412
|
+
flushFormData();
|
|
19373
19413
|
handleRequiredFieldBlur(detail.name);
|
|
19374
19414
|
},
|
|
19375
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>;
|