@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 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.9-snapshot-5](https://github.com/instructure/instructure-ui/compare/v11.7.8...v11.7.9-snapshot-5) (2026-10-13)
6
+ ## [11.7.10](https://github.com/instructure/instructure-ui/compare/v11.7.8...v11.7.10) (2026-10-14)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-form-field
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)
@@ -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
- return Children.map(this.props.children, (child, index) => {
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 = 'label',
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("span", {
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
- // needs to be wrapped because it needs an `id`
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
- children: children
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
- return _react.Children.map(this.props.children, (child, index) => {
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 = 'label',
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)("span", {
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
- // needs to be wrapped because it needs an `id`
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
- children: children
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.9-snapshot-5",
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.9-snapshot-5",
19
- "@instructure/emotion": "11.7.9-snapshot-5",
20
- "@instructure/shared-types": "11.7.9-snapshot-5",
21
- "@instructure/ui-a11y-content": "11.7.9-snapshot-5",
22
- "@instructure/ui-a11y-utils": "11.7.9-snapshot-5",
23
- "@instructure/ui-grid": "11.7.9-snapshot-5",
24
- "@instructure/ui-icons": "11.7.9-snapshot-5",
25
- "@instructure/ui-react-utils": "11.7.9-snapshot-5",
26
- "@instructure/ui-themes": "11.7.9-snapshot-5",
27
- "@instructure/ui-utils": "11.7.9-snapshot-5",
28
- "@instructure/uid": "11.7.9-snapshot-5"
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.9-snapshot-5",
32
- "@instructure/ui-babel-preset": "11.7.9-snapshot-5",
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="_foo121" label={<ScreenReaderContent>hidden text</ScreenReaderContent>} width="400px" layout="stacked">
34
- <TextInput id="_foo121" />
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, so a single form control can reference just the
51
- * label text via `aria-labelledby` (keeping messages out of its accessible name)
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
- children?: React.ReactNode
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'