@instructure/ui-form-field 11.7.9-snapshot-5 → 11.7.10
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/CHANGELOG.md +8 -2
- package/es/FormField/v2/index.js +1 -6
- package/es/FormFieldGroup/v2/index.js +20 -12
- package/es/FormFieldLayout/v2/index.js +39 -5
- package/es/FormFieldLayout/v2/styles.js +2 -2
- package/lib/FormField/v2/index.js +1 -6
- package/lib/FormFieldGroup/v2/index.js +19 -11
- package/lib/FormFieldLayout/v2/index.js +39 -5
- package/lib/FormFieldLayout/v2/styles.js +2 -2
- package/package.json +14 -14
- package/src/FormField/v2/README.md +28 -2
- package/src/FormField/v2/index.tsx +1 -5
- package/src/FormField/v2/props.ts +10 -3
- package/src/FormFieldGroup/v2/index.tsx +21 -9
- package/src/FormFieldGroup/v2/props.ts +18 -3
- package/src/FormFieldLayout/v2/index.tsx +64 -8
- package/src/FormFieldLayout/v2/props.ts +39 -6
- package/src/FormFieldLayout/v2/styles.ts +2 -2
- package/src/exports/b.ts +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/FormField/v2/index.d.ts.map +1 -1
- package/types/FormField/v2/props.d.ts +10 -3
- package/types/FormField/v2/props.d.ts.map +1 -1
- package/types/FormFieldGroup/v2/index.d.ts +7 -4
- package/types/FormFieldGroup/v2/index.d.ts.map +1 -1
- package/types/FormFieldGroup/v2/props.d.ts +8 -3
- package/types/FormFieldGroup/v2/props.d.ts.map +1 -1
- package/types/FormFieldLayout/v2/index.d.ts.map +1 -1
- package/types/FormFieldLayout/v2/props.d.ts +34 -7
- package/types/FormFieldLayout/v2/props.d.ts.map +1 -1
- package/types/exports/b.d.ts +1 -1
- package/types/exports/b.d.ts.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [11.7.
|
|
6
|
+
## [11.7.10](https://github.com/instructure/instructure-ui/compare/v11.7.8...v11.7.10) (2026-10-14)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **many:** Stop wrapping form field controls in their label ([#2740](https://github.com/instructure/instructure-ui/issues/2740)) ([467269e](https://github.com/instructure/instructure-ui/commit/467269ea6d66c88ef9deb8bd14980730bbd645e8))
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
## [11.7.9](https://github.com/instructure/instructure-ui/compare/v11.7.8...v11.7.9) (2026-10-13)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
11
16
|
|
|
17
|
+
* **many:** Stop wrapping form field controls in their label ([#2740](https://github.com/instructure/instructure-ui/issues/2740)) ([467269e](https://github.com/instructure/instructure-ui/commit/467269ea6d66c88ef9deb8bd14980730bbd645e8))
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
## [11.7.8](https://github.com/instructure/instructure-ui/compare/v11.7.7...v11.7.8) (2026-10-02)
|
package/es/FormField/v2/index.js
CHANGED
|
@@ -58,13 +58,8 @@ class FormField extends Component {
|
|
|
58
58
|
...omitProps(this.props, FormField.allowedProps),
|
|
59
59
|
...pickProps(this.props, formFieldLayoutAllowedProps),
|
|
60
60
|
label: this.props.label,
|
|
61
|
+
id: this.props.id,
|
|
61
62
|
vAlign: this.props.vAlign,
|
|
62
|
-
as: "label"
|
|
63
|
-
// This makes the control in focus when the label is clicked
|
|
64
|
-
// This is needed to prevent the wrong element to be focused, e.g.
|
|
65
|
-
// multi selects Tag-s
|
|
66
|
-
,
|
|
67
|
-
htmlFor: this.props.id,
|
|
68
63
|
elementRef: this.handleRef,
|
|
69
64
|
margin: this.props.margin,
|
|
70
65
|
isRequired: this.props.isRequired,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _dec, _class;
|
|
1
|
+
var _dec, _dec2, _class;
|
|
2
2
|
/*
|
|
3
3
|
* The MIT License (MIT)
|
|
4
4
|
*
|
|
@@ -25,7 +25,7 @@ var _dec, _class;
|
|
|
25
25
|
|
|
26
26
|
import { Component, Children, isValidElement } from 'react';
|
|
27
27
|
import { Grid } from '@instructure/ui-grid/latest';
|
|
28
|
-
import { pickProps, omitProps, safeCloneElement } from '@instructure/ui-react-utils';
|
|
28
|
+
import { pickProps, omitProps, safeCloneElement, withDeterministicId } from '@instructure/ui-react-utils';
|
|
29
29
|
import { withStyleNew } from '@instructure/emotion';
|
|
30
30
|
import { allowedProps as formFieldLayoutAllowedProps } from '../../FormFieldLayout/v2/props.js';
|
|
31
31
|
import { FormFieldLayout } from '../../FormFieldLayout/v2/index.js';
|
|
@@ -37,7 +37,7 @@ import { jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
|
37
37
|
category: components
|
|
38
38
|
---
|
|
39
39
|
**/
|
|
40
|
-
let FormFieldGroup = (_dec = withStyleNew(generateStyle), _dec(_class = class FormFieldGroup extends Component {
|
|
40
|
+
let FormFieldGroup = (_dec = withDeterministicId(), _dec2 = withStyleNew(generateStyle), _dec(_class = _dec2(_class = class FormFieldGroup extends Component {
|
|
41
41
|
static displayName = 'FormFieldGroup';
|
|
42
42
|
static componentId = 'FormFieldGroup';
|
|
43
43
|
static allowedProps = allowedProps;
|
|
@@ -49,6 +49,11 @@ let FormFieldGroup = (_dec = withStyleNew(generateStyle), _dec(_class = class Fo
|
|
|
49
49
|
vAlign: 'middle',
|
|
50
50
|
isGroup: true
|
|
51
51
|
};
|
|
52
|
+
constructor(props) {
|
|
53
|
+
super(props);
|
|
54
|
+
this._defaultId = props.deterministicId();
|
|
55
|
+
}
|
|
56
|
+
_defaultId;
|
|
52
57
|
ref = null;
|
|
53
58
|
handleRef = el => {
|
|
54
59
|
const {
|
|
@@ -70,11 +75,13 @@ let FormFieldGroup = (_dec = withStyleNew(generateStyle), _dec(_class = class Fo
|
|
|
70
75
|
return message.type === 'error' || message.type === 'newError';
|
|
71
76
|
}) >= 0;
|
|
72
77
|
}
|
|
73
|
-
renderColumns() {
|
|
78
|
+
renderColumns(params) {
|
|
74
79
|
const {
|
|
75
|
-
disabled
|
|
80
|
+
disabled,
|
|
81
|
+
children
|
|
76
82
|
} = this.props;
|
|
77
|
-
|
|
83
|
+
const fields = typeof children === 'function' ? children(params) : children;
|
|
84
|
+
return Children.map(fields, (child, index) => {
|
|
78
85
|
if (!child) return null;
|
|
79
86
|
const el = child;
|
|
80
87
|
const renderedChild = disabled && /*#__PURE__*/isValidElement(el) ? safeCloneElement(el, {
|
|
@@ -86,24 +93,24 @@ let FormFieldGroup = (_dec = withStyleNew(generateStyle), _dec(_class = class Fo
|
|
|
86
93
|
}, index);
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
|
-
renderChildren() {
|
|
96
|
+
renderChildren(params) {
|
|
90
97
|
return _jsx(Grid, {
|
|
91
98
|
colSpacing: this.props.colSpacing,
|
|
92
99
|
rowSpacing: this.props.rowSpacing,
|
|
93
100
|
vAlign: this.props.vAlign,
|
|
94
101
|
startAt: this.props.startAt || (this.props.layout === 'columns' ? 'medium' : null),
|
|
95
102
|
children: _jsx(Grid.Row, {
|
|
96
|
-
children: this.renderColumns()
|
|
103
|
+
children: this.renderColumns(params)
|
|
97
104
|
})
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
|
-
renderFields() {
|
|
107
|
+
renderFields(params) {
|
|
101
108
|
const {
|
|
102
109
|
styles
|
|
103
110
|
} = this.props;
|
|
104
111
|
return _jsx("span", {
|
|
105
112
|
css: styles?.formFieldGroup,
|
|
106
|
-
children: this.renderChildren()
|
|
113
|
+
children: this.renderChildren(params)
|
|
107
114
|
}, "fields");
|
|
108
115
|
}
|
|
109
116
|
render() {
|
|
@@ -128,13 +135,14 @@ let FormFieldGroup = (_dec = withStyleNew(generateStyle), _dec(_class = class Fo
|
|
|
128
135
|
vAlign: props.vAlign,
|
|
129
136
|
layout: props.layout === 'inline' ? 'inline' : 'stacked',
|
|
130
137
|
label: props.description,
|
|
138
|
+
id: props.id || this._defaultId,
|
|
131
139
|
"aria-disabled": props.disabled ? 'true' : undefined,
|
|
132
140
|
"aria-invalid": ariaInvalid,
|
|
133
141
|
elementRef: this.handleRef,
|
|
134
142
|
isGroup: isGroup,
|
|
135
|
-
children: this.renderFields()
|
|
143
|
+
children: params => this.renderFields(params)
|
|
136
144
|
});
|
|
137
145
|
}
|
|
138
|
-
}) || _class);
|
|
146
|
+
}) || _class) || _class);
|
|
139
147
|
export default FormFieldGroup;
|
|
140
148
|
export { FormFieldGroup };
|
|
@@ -30,6 +30,12 @@ import { FormFieldMessages } from '../../FormFieldMessages/v2/index.js';
|
|
|
30
30
|
import generateStyle from './styles.js';
|
|
31
31
|
import { allowedProps } from './props.js';
|
|
32
32
|
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "@emotion/react/jsx-runtime";
|
|
33
|
+
// Elements that handle clicks themselves, do not override it
|
|
34
|
+
const INTERACTIVE_SELECTOR = ['a[href]', 'button', 'input', 'select', 'textarea', 'label', 'summary', 'iframe', 'embed', 'object', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].join(',');
|
|
35
|
+
|
|
36
|
+
// Clicks already forwarded by a nested FormFieldLayout
|
|
37
|
+
const forwardedClicks = new WeakSet();
|
|
38
|
+
|
|
33
39
|
/**
|
|
34
40
|
---
|
|
35
41
|
parent: FormField
|
|
@@ -39,10 +45,11 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
39
45
|
const {
|
|
40
46
|
inline = false,
|
|
41
47
|
layout = 'stacked',
|
|
42
|
-
as = '
|
|
48
|
+
as = 'div',
|
|
43
49
|
labelAlign = 'end',
|
|
44
50
|
vAlign,
|
|
45
51
|
label,
|
|
52
|
+
id,
|
|
46
53
|
messages,
|
|
47
54
|
messagesId: messagesIdProp,
|
|
48
55
|
labelId: labelIdProp,
|
|
@@ -93,6 +100,7 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
const hasVisibleLabel = label ? hasVisibleChildren(label) : false;
|
|
103
|
+
const describedBy = hasMessages ? messagesId : undefined;
|
|
96
104
|
const invalid = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError');
|
|
97
105
|
|
|
98
106
|
// Styles
|
|
@@ -132,6 +140,28 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
132
140
|
inputContainerRef(node);
|
|
133
141
|
}
|
|
134
142
|
}, [inputContainerRef]);
|
|
143
|
+
|
|
144
|
+
// Clicks on the rest of the control area (icons, before/after content) need
|
|
145
|
+
// to be forwarded to the control.
|
|
146
|
+
const handleChildrenClick = e => {
|
|
147
|
+
if (
|
|
148
|
+
// it's a group, no clear control to forward to
|
|
149
|
+
ElementType === 'fieldset' || e.defaultPrevented || forwardedClicks.has(e.nativeEvent)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const container = e.currentTarget;
|
|
153
|
+
const interactive = e.target.closest(INTERACTIVE_SELECTOR);
|
|
154
|
+
if (interactive && container.contains(interactive)) {
|
|
155
|
+
return; // do not steal clicks for e.g. DateInput's calendar icon
|
|
156
|
+
}
|
|
157
|
+
const control = id ? container.ownerDocument.getElementById(id) : null;
|
|
158
|
+
if (!control || !container.contains(control)) {
|
|
159
|
+
return; // no control
|
|
160
|
+
}
|
|
161
|
+
forwardedClicks.add(e.nativeEvent);
|
|
162
|
+
control.focus();
|
|
163
|
+
control.click();
|
|
164
|
+
};
|
|
135
165
|
const renderLabel = () => {
|
|
136
166
|
const labelContent = hasVisibleLabel ? _jsxs(_Fragment, {
|
|
137
167
|
children: [label, isRequired && _jsxs("span", {
|
|
@@ -154,9 +184,10 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
154
184
|
})
|
|
155
185
|
});
|
|
156
186
|
}
|
|
157
|
-
return _jsx("
|
|
187
|
+
return _jsx("label", {
|
|
158
188
|
css: styles?.formFieldLabel,
|
|
159
189
|
id: labelId,
|
|
190
|
+
htmlFor: id,
|
|
160
191
|
children: labelContent
|
|
161
192
|
});
|
|
162
193
|
} else if (label) {
|
|
@@ -169,9 +200,9 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
169
200
|
children: label
|
|
170
201
|
});
|
|
171
202
|
}
|
|
172
|
-
|
|
173
|
-
return _jsx("div", {
|
|
203
|
+
return _jsx("label", {
|
|
174
204
|
id: labelId,
|
|
205
|
+
htmlFor: id,
|
|
175
206
|
style: {
|
|
176
207
|
display: 'contents'
|
|
177
208
|
},
|
|
@@ -198,7 +229,10 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
|
|
|
198
229
|
children: [renderLabel(), isGroup && renderMessages(), _jsx("span", {
|
|
199
230
|
css: styles?.formFieldChildren,
|
|
200
231
|
ref: handleInputContainerRef,
|
|
201
|
-
|
|
232
|
+
onClick: handleChildrenClick,
|
|
233
|
+
children: typeof children === 'function' ? children({
|
|
234
|
+
describedBy
|
|
235
|
+
}) : children
|
|
202
236
|
}), !isGroup && renderMessages()]
|
|
203
237
|
});
|
|
204
238
|
});
|
|
@@ -90,6 +90,7 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
|
|
|
90
90
|
fontWeight: componentTheme.fontWeight,
|
|
91
91
|
fontSize: componentTheme.fontSize,
|
|
92
92
|
lineHeight: componentTheme.lineHeight,
|
|
93
|
+
paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives,
|
|
93
94
|
...(isInlineLayout && {
|
|
94
95
|
margin: '0',
|
|
95
96
|
// when inline add a small padding between the label and the control
|
|
@@ -143,8 +144,7 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
|
|
|
143
144
|
...labelStyles,
|
|
144
145
|
// NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
|
|
145
146
|
'&:is(label)': labelStyles,
|
|
146
|
-
'&:-webkit-any(label)': labelStyles
|
|
147
|
-
paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives
|
|
147
|
+
'&:-webkit-any(label)': labelStyles
|
|
148
148
|
},
|
|
149
149
|
formFieldChildren: {
|
|
150
150
|
label: 'formFieldLayout__children',
|
|
@@ -64,13 +64,8 @@ class FormField extends _react.Component {
|
|
|
64
64
|
...(0, _uiReactUtils.omitProps)(this.props, FormField.allowedProps),
|
|
65
65
|
...(0, _uiReactUtils.pickProps)(this.props, _props.allowedProps),
|
|
66
66
|
label: this.props.label,
|
|
67
|
+
id: this.props.id,
|
|
67
68
|
vAlign: this.props.vAlign,
|
|
68
|
-
as: "label"
|
|
69
|
-
// This makes the control in focus when the label is clicked
|
|
70
|
-
// This is needed to prevent the wrong element to be focused, e.g.
|
|
71
|
-
// multi selects Tag-s
|
|
72
|
-
,
|
|
73
|
-
htmlFor: this.props.id,
|
|
74
69
|
elementRef: this.handleRef,
|
|
75
70
|
margin: this.props.margin,
|
|
76
71
|
isRequired: this.props.isRequired,
|
|
@@ -14,7 +14,7 @@ var _index = require("../../FormFieldLayout/v2/index.js");
|
|
|
14
14
|
var _styles = _interopRequireDefault(require("./styles.js"));
|
|
15
15
|
var _props2 = require("./props.js");
|
|
16
16
|
var _jsxRuntime = require("@emotion/react/jsx-runtime");
|
|
17
|
-
var _dec, _class;
|
|
17
|
+
var _dec, _dec2, _class;
|
|
18
18
|
/*
|
|
19
19
|
* The MIT License (MIT)
|
|
20
20
|
*
|
|
@@ -43,7 +43,7 @@ var _dec, _class;
|
|
|
43
43
|
category: components
|
|
44
44
|
---
|
|
45
45
|
**/
|
|
46
|
-
let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _emotion.withStyleNew)(_styles.default), _dec(_class = class FormFieldGroup extends _react.Component {
|
|
46
|
+
let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _uiReactUtils.withDeterministicId)(), _dec2 = (0, _emotion.withStyleNew)(_styles.default), _dec(_class = _dec2(_class = class FormFieldGroup extends _react.Component {
|
|
47
47
|
static displayName = 'FormFieldGroup';
|
|
48
48
|
static componentId = 'FormFieldGroup';
|
|
49
49
|
static allowedProps = _props2.allowedProps;
|
|
@@ -55,6 +55,11 @@ let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _emotion.withStyleNew)
|
|
|
55
55
|
vAlign: 'middle',
|
|
56
56
|
isGroup: true
|
|
57
57
|
};
|
|
58
|
+
constructor(props) {
|
|
59
|
+
super(props);
|
|
60
|
+
this._defaultId = props.deterministicId();
|
|
61
|
+
}
|
|
62
|
+
_defaultId;
|
|
58
63
|
ref = null;
|
|
59
64
|
handleRef = el => {
|
|
60
65
|
const {
|
|
@@ -76,11 +81,13 @@ let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _emotion.withStyleNew)
|
|
|
76
81
|
return message.type === 'error' || message.type === 'newError';
|
|
77
82
|
}) >= 0;
|
|
78
83
|
}
|
|
79
|
-
renderColumns() {
|
|
84
|
+
renderColumns(params) {
|
|
80
85
|
const {
|
|
81
|
-
disabled
|
|
86
|
+
disabled,
|
|
87
|
+
children
|
|
82
88
|
} = this.props;
|
|
83
|
-
|
|
89
|
+
const fields = typeof children === 'function' ? children(params) : children;
|
|
90
|
+
return _react.Children.map(fields, (child, index) => {
|
|
84
91
|
if (!child) return null;
|
|
85
92
|
const el = child;
|
|
86
93
|
const renderedChild = disabled && /*#__PURE__*/(0, _react.isValidElement)(el) ? (0, _uiReactUtils.safeCloneElement)(el, {
|
|
@@ -92,24 +99,24 @@ let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _emotion.withStyleNew)
|
|
|
92
99
|
}, index);
|
|
93
100
|
});
|
|
94
101
|
}
|
|
95
|
-
renderChildren() {
|
|
102
|
+
renderChildren(params) {
|
|
96
103
|
return (0, _jsxRuntime.jsx)(_latest.Grid, {
|
|
97
104
|
colSpacing: this.props.colSpacing,
|
|
98
105
|
rowSpacing: this.props.rowSpacing,
|
|
99
106
|
vAlign: this.props.vAlign,
|
|
100
107
|
startAt: this.props.startAt || (this.props.layout === 'columns' ? 'medium' : null),
|
|
101
108
|
children: (0, _jsxRuntime.jsx)(_latest.Grid.Row, {
|
|
102
|
-
children: this.renderColumns()
|
|
109
|
+
children: this.renderColumns(params)
|
|
103
110
|
})
|
|
104
111
|
});
|
|
105
112
|
}
|
|
106
|
-
renderFields() {
|
|
113
|
+
renderFields(params) {
|
|
107
114
|
const {
|
|
108
115
|
styles
|
|
109
116
|
} = this.props;
|
|
110
117
|
return (0, _jsxRuntime.jsx)("span", {
|
|
111
118
|
css: styles?.formFieldGroup,
|
|
112
|
-
children: this.renderChildren()
|
|
119
|
+
children: this.renderChildren(params)
|
|
113
120
|
}, "fields");
|
|
114
121
|
}
|
|
115
122
|
render() {
|
|
@@ -134,12 +141,13 @@ let FormFieldGroup = exports.FormFieldGroup = (_dec = (0, _emotion.withStyleNew)
|
|
|
134
141
|
vAlign: props.vAlign,
|
|
135
142
|
layout: props.layout === 'inline' ? 'inline' : 'stacked',
|
|
136
143
|
label: props.description,
|
|
144
|
+
id: props.id || this._defaultId,
|
|
137
145
|
"aria-disabled": props.disabled ? 'true' : undefined,
|
|
138
146
|
"aria-invalid": ariaInvalid,
|
|
139
147
|
elementRef: this.handleRef,
|
|
140
148
|
isGroup: isGroup,
|
|
141
|
-
children: this.renderFields()
|
|
149
|
+
children: params => this.renderFields(params)
|
|
142
150
|
});
|
|
143
151
|
}
|
|
144
|
-
}) || _class);
|
|
152
|
+
}) || _class) || _class);
|
|
145
153
|
var _default = exports.default = FormFieldGroup;
|
|
@@ -44,6 +44,12 @@ var _jsxRuntime = require("@emotion/react/jsx-runtime");
|
|
|
44
44
|
* SOFTWARE.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
|
+
// Elements that handle clicks themselves, do not override it
|
|
48
|
+
const INTERACTIVE_SELECTOR = ['a[href]', 'button', 'input', 'select', 'textarea', 'label', 'summary', 'iframe', 'embed', 'object', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', '[tabindex]'].join(',');
|
|
49
|
+
|
|
50
|
+
// Clicks already forwarded by a nested FormFieldLayout
|
|
51
|
+
const forwardedClicks = new WeakSet();
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
---
|
|
49
55
|
parent: FormField
|
|
@@ -53,10 +59,11 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
53
59
|
const {
|
|
54
60
|
inline = false,
|
|
55
61
|
layout = 'stacked',
|
|
56
|
-
as = '
|
|
62
|
+
as = 'div',
|
|
57
63
|
labelAlign = 'end',
|
|
58
64
|
vAlign,
|
|
59
65
|
label,
|
|
66
|
+
id,
|
|
60
67
|
messages,
|
|
61
68
|
messagesId: messagesIdProp,
|
|
62
69
|
labelId: labelIdProp,
|
|
@@ -107,6 +114,7 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
const hasVisibleLabel = label ? (0, _uiA11yUtils.hasVisibleChildren)(label) : false;
|
|
117
|
+
const describedBy = hasMessages ? messagesId : undefined;
|
|
110
118
|
const invalid = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError');
|
|
111
119
|
|
|
112
120
|
// Styles
|
|
@@ -146,6 +154,28 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
146
154
|
inputContainerRef(node);
|
|
147
155
|
}
|
|
148
156
|
}, [inputContainerRef]);
|
|
157
|
+
|
|
158
|
+
// Clicks on the rest of the control area (icons, before/after content) need
|
|
159
|
+
// to be forwarded to the control.
|
|
160
|
+
const handleChildrenClick = e => {
|
|
161
|
+
if (
|
|
162
|
+
// it's a group, no clear control to forward to
|
|
163
|
+
ElementType === 'fieldset' || e.defaultPrevented || forwardedClicks.has(e.nativeEvent)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const container = e.currentTarget;
|
|
167
|
+
const interactive = e.target.closest(INTERACTIVE_SELECTOR);
|
|
168
|
+
if (interactive && container.contains(interactive)) {
|
|
169
|
+
return; // do not steal clicks for e.g. DateInput's calendar icon
|
|
170
|
+
}
|
|
171
|
+
const control = id ? container.ownerDocument.getElementById(id) : null;
|
|
172
|
+
if (!control || !container.contains(control)) {
|
|
173
|
+
return; // no control
|
|
174
|
+
}
|
|
175
|
+
forwardedClicks.add(e.nativeEvent);
|
|
176
|
+
control.focus();
|
|
177
|
+
control.click();
|
|
178
|
+
};
|
|
149
179
|
const renderLabel = () => {
|
|
150
180
|
const labelContent = hasVisibleLabel ? (0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
151
181
|
children: [label, isRequired && (0, _jsxRuntime.jsxs)("span", {
|
|
@@ -168,9 +198,10 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
168
198
|
})
|
|
169
199
|
});
|
|
170
200
|
}
|
|
171
|
-
return (0, _jsxRuntime.jsx)("
|
|
201
|
+
return (0, _jsxRuntime.jsx)("label", {
|
|
172
202
|
css: styles?.formFieldLabel,
|
|
173
203
|
id: labelId,
|
|
204
|
+
htmlFor: id,
|
|
174
205
|
children: labelContent
|
|
175
206
|
});
|
|
176
207
|
} else if (label) {
|
|
@@ -183,9 +214,9 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
183
214
|
children: label
|
|
184
215
|
});
|
|
185
216
|
}
|
|
186
|
-
|
|
187
|
-
return (0, _jsxRuntime.jsx)("div", {
|
|
217
|
+
return (0, _jsxRuntime.jsx)("label", {
|
|
188
218
|
id: labelId,
|
|
219
|
+
htmlFor: id,
|
|
189
220
|
style: {
|
|
190
221
|
display: 'contents'
|
|
191
222
|
},
|
|
@@ -212,7 +243,10 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
|
|
|
212
243
|
children: [renderLabel(), isGroup && renderMessages(), (0, _jsxRuntime.jsx)("span", {
|
|
213
244
|
css: styles?.formFieldChildren,
|
|
214
245
|
ref: handleInputContainerRef,
|
|
215
|
-
|
|
246
|
+
onClick: handleChildrenClick,
|
|
247
|
+
children: typeof children === 'function' ? children({
|
|
248
|
+
describedBy
|
|
249
|
+
}) : children
|
|
216
250
|
}), !isGroup && renderMessages()]
|
|
217
251
|
});
|
|
218
252
|
});
|
|
@@ -96,6 +96,7 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
|
|
|
96
96
|
fontWeight: componentTheme.fontWeight,
|
|
97
97
|
fontSize: componentTheme.fontSize,
|
|
98
98
|
lineHeight: componentTheme.lineHeight,
|
|
99
|
+
paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives,
|
|
99
100
|
...(isInlineLayout && {
|
|
100
101
|
margin: '0',
|
|
101
102
|
// when inline add a small padding between the label and the control
|
|
@@ -149,8 +150,7 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
|
|
|
149
150
|
...labelStyles,
|
|
150
151
|
// NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
|
|
151
152
|
'&:is(label)': labelStyles,
|
|
152
|
-
'&:-webkit-any(label)': labelStyles
|
|
153
|
-
paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives
|
|
153
|
+
'&:-webkit-any(label)': labelStyles
|
|
154
154
|
},
|
|
155
155
|
formFieldChildren: {
|
|
156
156
|
label: 'formFieldLayout__children',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-form-field",
|
|
3
|
-
"version": "11.7.
|
|
3
|
+
"version": "11.7.10",
|
|
4
4
|
"description": "Form layout components.",
|
|
5
5
|
"author": "Instructure, Inc. Engineering and Product Design",
|
|
6
6
|
"module": "./es/index.js",
|
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@babel/runtime": "^7.29.7",
|
|
18
|
-
"@instructure/console": "11.7.
|
|
19
|
-
"@instructure/emotion": "11.7.
|
|
20
|
-
"@instructure/shared-types": "11.7.
|
|
21
|
-
"@instructure/ui-a11y-content": "11.7.
|
|
22
|
-
"@instructure/ui-a11y-utils": "11.7.
|
|
23
|
-
"@instructure/ui-grid": "11.7.
|
|
24
|
-
"@instructure/ui-icons": "11.7.
|
|
25
|
-
"@instructure/ui-react-utils": "11.7.
|
|
26
|
-
"@instructure/ui-themes": "11.7.
|
|
27
|
-
"@instructure/ui-utils": "11.7.
|
|
28
|
-
"@instructure/uid": "11.7.
|
|
18
|
+
"@instructure/console": "11.7.10",
|
|
19
|
+
"@instructure/emotion": "11.7.10",
|
|
20
|
+
"@instructure/shared-types": "11.7.10",
|
|
21
|
+
"@instructure/ui-a11y-content": "11.7.10",
|
|
22
|
+
"@instructure/ui-a11y-utils": "11.7.10",
|
|
23
|
+
"@instructure/ui-grid": "11.7.10",
|
|
24
|
+
"@instructure/ui-icons": "11.7.10",
|
|
25
|
+
"@instructure/ui-react-utils": "11.7.10",
|
|
26
|
+
"@instructure/ui-themes": "11.7.10",
|
|
27
|
+
"@instructure/ui-utils": "11.7.10",
|
|
28
|
+
"@instructure/uid": "11.7.10"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@instructure/ui-axe-check": "11.7.
|
|
32
|
-
"@instructure/ui-babel-preset": "11.7.
|
|
31
|
+
"@instructure/ui-axe-check": "11.7.10",
|
|
32
|
+
"@instructure/ui-babel-preset": "11.7.10",
|
|
33
33
|
"vitest": "^5.0.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
@@ -5,6 +5,32 @@ describes: FormField
|
|
|
5
5
|
This is a helper component that is used by most of the custom form
|
|
6
6
|
components. In most cases it shouldn't be used directly.
|
|
7
7
|
|
|
8
|
+
### Accessibility
|
|
9
|
+
|
|
10
|
+
`FormField` renders a `<label>` that wraps the label text, and links it to
|
|
11
|
+
the form control via its `for` attribute. Make sure that you mind the following:
|
|
12
|
+
|
|
13
|
+
- The control needs to have the same `id` as the one passed to `FormField` via `labelId`.
|
|
14
|
+
you can also use `aria-labelledby` on your control to read the label.
|
|
15
|
+
- Messages aren't part of the control's accessible name. To have screen readers
|
|
16
|
+
announce them, pass a function as `children`. It receives `{ describedBy }`,
|
|
17
|
+
the id of the rendered messages (or `undefined` when none are shown), which
|
|
18
|
+
you add to the control's `aria-describedby`.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
---
|
|
22
|
+
type: code
|
|
23
|
+
---
|
|
24
|
+
<FormField id="email" label="Email" messages={messages}>
|
|
25
|
+
{({ describedBy }) => (
|
|
26
|
+
<input id="email" type="email" aria-describedby={describedBy} />
|
|
27
|
+
)}
|
|
28
|
+
</FormField>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Clicking non-interactive content around the control (e.g. icons or padding)
|
|
32
|
+
focuses and clicks the control, like clicking the label does.
|
|
33
|
+
|
|
8
34
|
```js
|
|
9
35
|
---
|
|
10
36
|
type: example
|
|
@@ -30,8 +56,8 @@ type: example
|
|
|
30
56
|
<TextInput id="_foo124"/>
|
|
31
57
|
</FormField>
|
|
32
58
|
<hr/>
|
|
33
|
-
<FormField id="
|
|
34
|
-
<TextInput id="
|
|
59
|
+
<FormField id="_foo125" label={<ScreenReaderContent>hidden text</ScreenReaderContent>} width="400px" layout="stacked">
|
|
60
|
+
<TextInput id="_foo125" />
|
|
35
61
|
</FormField>
|
|
36
62
|
<hr/>
|
|
37
63
|
</div>
|
|
@@ -68,12 +68,8 @@ class FormField extends Component<FormFieldProps> {
|
|
|
68
68
|
{...omitProps(this.props, FormField.allowedProps)}
|
|
69
69
|
{...pickProps(this.props, formFieldLayoutAllowedProps)}
|
|
70
70
|
label={this.props.label}
|
|
71
|
+
id={this.props.id}
|
|
71
72
|
vAlign={this.props.vAlign}
|
|
72
|
-
as="label"
|
|
73
|
-
// This makes the control in focus when the label is clicked
|
|
74
|
-
// This is needed to prevent the wrong element to be focused, e.g.
|
|
75
|
-
// multi selects Tag-s
|
|
76
|
-
htmlFor={this.props.id}
|
|
77
73
|
elementRef={this.handleRef}
|
|
78
74
|
margin={this.props.margin}
|
|
79
75
|
isRequired={this.props.isRequired}
|
|
@@ -27,6 +27,7 @@ import React from 'react'
|
|
|
27
27
|
import type { OtherHTMLAttributes } from '@instructure/shared-types'
|
|
28
28
|
import type { FormMessage } from '../../utils/v1/FormPropTypes'
|
|
29
29
|
import type { Spacing } from '@instructure/emotion'
|
|
30
|
+
import type { FormFieldLayoutChildren } from '../../FormFieldLayout/v2/props'
|
|
30
31
|
|
|
31
32
|
type FormFieldOwnProps = {
|
|
32
33
|
label: React.ReactNode
|
|
@@ -47,11 +48,17 @@ type FormFieldOwnProps = {
|
|
|
47
48
|
*/
|
|
48
49
|
messagesId?: string
|
|
49
50
|
/**
|
|
50
|
-
* id for the label element
|
|
51
|
-
*
|
|
51
|
+
* `id` for the label/legend element. Useful when the control is not a
|
|
52
|
+
* [labelable](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories#labelable)
|
|
53
|
+
* element (e.g. a custom widget), and needs to reference the label via
|
|
54
|
+
* `aria-labelledby`.
|
|
52
55
|
*/
|
|
53
56
|
labelId?: string
|
|
54
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The form control. Can be a function that receives `{ describedBy }`, so
|
|
59
|
+
* the control can reference the messages via `aria-describedby`.
|
|
60
|
+
*/
|
|
61
|
+
children?: FormFieldLayoutChildren
|
|
55
62
|
inline?: boolean
|
|
56
63
|
layout?: 'stacked' | 'inline'
|
|
57
64
|
labelAlign?: 'start' | 'end'
|