From 342dbbc0b27ef1829deb6b0c11481c8cd089bd84 Mon Sep 17 00:00:00 2001 From: spiros dimopulos Date: Thu, 7 May 2026 16:21:32 +0300 Subject: [PATCH 1/4] DRYD-1923: added assigning input id and htmlFor in Field; --- src/components/record/Field.jsx | 21 +++++++++++++++++++++ src/helpers/inputIdHelper.js | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/helpers/inputIdHelper.js diff --git a/src/components/record/Field.jsx b/src/components/record/Field.jsx index 779266919..7a51e5264 100644 --- a/src/components/record/Field.jsx +++ b/src/components/record/Field.jsx @@ -21,6 +21,7 @@ import { DateInput, StructuredDateInput, } from '../../helpers/configContextInputs'; +import buildInputId from '../../helpers/inputIdHelper'; const { getPath, @@ -166,6 +167,13 @@ export default function Field(props, context) { const basePropTypes = BaseComponent.propTypes; Object.keys(props).forEach((propName) => { + // Skip id: Field always computes its own id from the data path, so a propagated + // id (from a parent CustomCompoundInput's decorateInputs) must not override + // it — that would make the input id not match with label's htmlFor. + if (propName === 'id') { + return; + } + if (propName in basePropTypes) { // eslint-disable-next-line react/destructuring-assignment providedProps[propName] = props[propName]; @@ -186,12 +194,17 @@ export default function Field(props, context) { const effectiveReadOnly = providedProps.readOnly || isFieldViewReadOnly(computeContext); const computedProps = {}; + const inputId = buildInputId(recordType, formName, fullPath); + computedProps.id = inputId; + if (fieldConfig.repeating && viewType !== 'search') { computedProps.repeating = fieldConfig.repeating; } if ('label' in basePropTypes) { computedProps.label = renderLabel(field, labelMessage, computeContext, { + htmlFor: inputId, + id: `${inputId}-label`, readOnly: effectiveReadOnly, }); } @@ -209,6 +222,12 @@ export default function Field(props, context) { const childName = childInput.props.name; const childLabelMessage = childInput.props.labelMessage; const childField = field[childName]; + // For repeating containers (e.g. tabular repeating CompoundInput), + // point the label to the first input. + const childPath = fieldConfig.repeating + ? [...fullPath, '0', childName] + : [...fullPath, childName]; + const childInputId = buildInputId(recordType, formName, childPath); const childComputeContext = { path: [...path, childName], @@ -222,6 +241,8 @@ export default function Field(props, context) { return (childField && renderLabel(childField, childLabelMessage, childComputeContext, { key: childName, + htmlFor: childInputId, + id: `${childInputId}-label`, readOnly: effectiveReadOnly, })); }; diff --git a/src/helpers/inputIdHelper.js b/src/helpers/inputIdHelper.js new file mode 100644 index 000000000..252f75e6c --- /dev/null +++ b/src/helpers/inputIdHelper.js @@ -0,0 +1,23 @@ +const sanitizeIdSegment = (segment) => String(segment).replace(/[^a-zA-Z0-9_-]/g, '_'); + +export default function buildInputId(recordType, formName, path) { + const parts = ['cspace']; + + if (recordType) { + parts.push(recordType); + } + + if (formName) { + parts.push(formName); + } + + if (Array.isArray(path)) { + path.forEach((part) => { + if (part !== undefined && part !== null && part !== '') { + parts.push(part); + } + }); + } + + return parts.map(sanitizeIdSegment).join('-'); +} From 713a9d1dd6a960e0736b90e968ea26bef83c52dc Mon Sep 17 00:00:00 2001 From: spiros dimopulos Date: Thu, 7 May 2026 16:27:50 +0300 Subject: [PATCH 2/4] DRYD-1923: built htmlFor for InputTable column headers --- src/components/record/InputTable.jsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/components/record/InputTable.jsx b/src/components/record/InputTable.jsx index 60a1308de..c11d8a0e3 100644 --- a/src/components/record/InputTable.jsx +++ b/src/components/record/InputTable.jsx @@ -13,6 +13,8 @@ import { dataPathToFieldDescriptorPath, } from '../../helpers/configHelpers'; +import buildInputId from '../../helpers/inputIdHelper'; + const { Label, InputTable: BaseInputTable, @@ -32,10 +34,11 @@ const renderTableLabel = (recordTypeConfig, name) => { return (message ? renderMessageLabel(message) : null); }; -const renderFieldLabel = (fieldConfig, inputProps) => { +const renderFieldLabel = (fieldConfig, inputProps, labelProps) => { const message = get(fieldConfig, ['messages', 'name']); const props = { + ...labelProps, readOnly: inputProps.readOnly, required: inputProps.required, }; @@ -60,6 +63,7 @@ const contextTypes = { config: PropTypes.shape({ recordTypes: PropTypes.object, }), + formName: PropTypes.string, intl: intlShape, recordType: PropTypes.string, }; @@ -72,6 +76,7 @@ export default function InputTable(props, context) { const { config, + formName, intl, recordType, } = context; @@ -80,11 +85,21 @@ export default function InputTable(props, context) { const fields = get(recordTypeConfig, 'fields'); const renderLabel = (input) => { - const path = dataPathToFieldDescriptorPath(pathHelpers.getPath(input.props)); + const fullPath = pathHelpers.getPath(input.props); + const path = dataPathToFieldDescriptorPath(fullPath); const field = get(fields, path); const fieldConfig = get(field, configKey); - return (fieldConfig && renderFieldLabel(fieldConfig, input.props)); + if (!fieldConfig) { + return null; + } + + const inputId = buildInputId(recordType, formName, fullPath); + + return renderFieldLabel(fieldConfig, input.props, { + htmlFor: inputId, + id: `${inputId}-label`, + }); }; const renderAriaLabel = (input) => { From ba6c34284b8948a335546c57e7e85b2cd6193513 Mon Sep 17 00:00:00 2001 From: spiros dimopulos Date: Thu, 7 May 2026 16:39:29 +0300 Subject: [PATCH 3/4] DRYD-1923: propagate ids for HierarchyEditor labels; --- .../record/TypedHierarchyEditor.jsx | 29 ++++++++++++++++++- .../record/UntypedHierarchyEditor.jsx | 7 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/record/TypedHierarchyEditor.jsx b/src/components/record/TypedHierarchyEditor.jsx index 94fa4784e..65d40bf8a 100644 --- a/src/components/record/TypedHierarchyEditor.jsx +++ b/src/components/record/TypedHierarchyEditor.jsx @@ -5,10 +5,12 @@ import Immutable from 'immutable'; import { components as inputComponents } from 'cspace-input'; import MiniViewPopupAutocompleteInputContainer from '../../containers/record/MiniViewPopupAutocompleteInputContainer'; import OptionPickerInputContainer from '../../containers/record/OptionPickerInputContainer'; +import buildInputId from '../../helpers/inputIdHelper'; const { CompoundInput, InputTable, + Label, } = inputComponents; const propTypes = { @@ -152,9 +154,21 @@ export class BaseTypedHierarchyEditor extends Component { const parentRefName = value.getIn(['parent', 'refName']); const parentType = value.getIn(['parent', 'type']); + const parentRefNameId = buildInputId(recordType, undefined, ['hierarchy', 'parent', 'parentRefName']); + const parentTypeId = buildInputId(recordType, undefined, ['hierarchy', 'parent', 'parentType']); + + const renderParentLabel = (input) => { + const { id, label } = input.props; + return ; + }; + return ( - + { + const { name, label } = input.props; + const childInputId = `${childrenTemplateId}-0-${name}`; + + return ; + }; + return ( diff --git a/src/components/record/UntypedHierarchyEditor.jsx b/src/components/record/UntypedHierarchyEditor.jsx index 6cf3d81b3..b926fe996 100644 --- a/src/components/record/UntypedHierarchyEditor.jsx +++ b/src/components/record/UntypedHierarchyEditor.jsx @@ -4,6 +4,7 @@ import { injectIntl, intlShape } from 'react-intl'; import Immutable from 'immutable'; import { components as inputComponents } from 'cspace-input'; import MiniViewPopupAutocompleteInputContainer from '../../containers/record/MiniViewPopupAutocompleteInputContainer'; +import buildInputId from '../../helpers/inputIdHelper'; const { CompoundInput, @@ -118,8 +119,11 @@ export class BaseUntypedHierarchyEditor extends Component { const source = [recordType, vocabulary].join('/'); const parentRefName = value.getIn(['parent', 'refName']); + const parentId = buildInputId(recordType, undefined, ['hierarchy', 'parent']); + return ( child.get('refName')); + const childrenContainerId = buildInputId(recordType, undefined, ['hierarchy', 'children']); + return ( Date: Mon, 11 May 2026 10:37:29 +0300 Subject: [PATCH 4/4] DRYD-1923: updated changelog; --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af0a133d..d8cfe8386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v11.0.0 +### Accessibility + +- Resolved orphaned form labels issue (Criteria 3.3.2). + ## v10.2.0 ### Breaking Changes