Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## v11.0.0

### Accessibility

- Resolved orphaned form labels issue (Criteria 3.3.2).

## v10.2.0

### Breaking Changes
Expand Down
21 changes: 21 additions & 0 deletions src/components/record/Field.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
DateInput,
StructuredDateInput,
} from '../../helpers/configContextInputs';
import buildInputId from '../../helpers/inputIdHelper';

const {
getPath,
Expand Down Expand Up @@ -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];
Expand All @@ -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,
});
}
Expand All @@ -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],
Expand All @@ -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,
}));
};
Expand Down
21 changes: 18 additions & 3 deletions src/components/record/InputTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
dataPathToFieldDescriptorPath,
} from '../../helpers/configHelpers';

import buildInputId from '../../helpers/inputIdHelper';

const {
Label,
InputTable: BaseInputTable,
Expand All @@ -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,
};
Expand All @@ -60,6 +63,7 @@ const contextTypes = {
config: PropTypes.shape({
recordTypes: PropTypes.object,
}),
formName: PropTypes.string,
intl: intlShape,
recordType: PropTypes.string,
};
Expand All @@ -72,6 +76,7 @@ export default function InputTable(props, context) {

const {
config,
formName,
intl,
recordType,
} = context;
Expand All @@ -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) => {
Expand Down
29 changes: 28 additions & 1 deletion src/components/record/TypedHierarchyEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 <Label htmlFor={id}>{label}</Label>;
};

return (
<InputTable label={intl.formatMessage(messages.parent)}>
<InputTable
label={intl.formatMessage(messages.parent)}
renderLabel={renderParentLabel}
>
<MiniViewPopupAutocompleteInputContainer
id={parentRefNameId}
label={intl.formatMessage(messages.parentName)}
name="parentRefName"
source={source}
Expand All @@ -166,6 +180,7 @@ export class BaseTypedHierarchyEditor extends Component {
matchFilter={this.filterMatch}
/>
<OptionPickerInputContainer
id={parentTypeId}
label={intl.formatMessage(messages.parentType)}
name="parentType"
source={parentTypeOptionListName}
Expand Down Expand Up @@ -197,16 +212,28 @@ export class BaseTypedHierarchyEditor extends Component {
const source = [recordType, vocabulary].join('/');
const children = value.get('children');

const childrenTemplateId = buildInputId(recordType, undefined, ['hierarchy', 'children', 'template']);

// point the label to the first input.
const renderChildInputLabel = (input) => {
const { name, label } = input.props;
const childInputId = `${childrenTemplateId}-0-${name}`;

return <Label htmlFor={childInputId}>{label}</Label>;
};

return (
<CompoundInput
label={intl.formatMessage(messages.children)}
value={children}
readOnly={readOnly}
>
<CompoundInput
id={childrenTemplateId}
tabular
repeating
reorderable={false}
renderChildInputLabel={renderChildInputLabel}
onAddInstance={this.handleAddChild}
onRemoveInstance={this.handleRemoveChild}
>
Expand Down
7 changes: 7 additions & 0 deletions src/components/record/UntypedHierarchyEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<MiniViewPopupAutocompleteInputContainer
id={parentId}
label={intl.formatMessage(messages.parent)}
source={source}
value={parentRefName}
Expand Down Expand Up @@ -152,8 +156,11 @@ export class BaseUntypedHierarchyEditor extends Component {

const childRefNames = value.get('children').map((child) => child.get('refName'));

const childrenContainerId = buildInputId(recordType, undefined, ['hierarchy', 'children']);

return (
<CompoundInput
id={childrenContainerId}
label={intl.formatMessage(messages.children)}
value={{ childRefNames }}
readOnly={readOnly}
Expand Down
23 changes: 23 additions & 0 deletions src/helpers/inputIdHelper.js
Original file line number Diff line number Diff line change
@@ -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('-');
}
Loading