@instructure/ui-form-field 11.7.8-snapshot-9 → 11.7.8-snapshot-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,11 @@
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.8-snapshot-9](https://github.com/instructure/instructure-ui/compare/v11.7.7...v11.7.8-snapshot-9) (2026-10-02)
7
-
8
- **Note:** Version bump only for package @instructure/ui-form-field
9
-
6
+ ## [11.7.8-snapshot-10](https://github.com/instructure/instructure-ui/compare/v11.7.7...v11.7.8-snapshot-10) (2026-10-02)
10
7
 
8
+ ### Bug Fixes
11
9
 
10
+ * **ui-form-field:** always display messages at the top of form groups (radio group, checkbox group, formFieldGroup) ([2d18472](https://github.com/instructure/instructure-ui/commit/2d18472a52846202bf0993e2786c5c21dc7aba1b))
12
11
 
13
12
 
14
13
  ## [11.7.7](https://github.com/instructure/instructure-ui/compare/v11.7.6...v11.7.7) (2026-09-23)
@@ -69,18 +69,30 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
69
69
  // Filter out error and success messages when disabled or readOnly
70
70
  const filteredMessages = disabled || readOnly ? messages?.filter(msg => msg.type !== 'error' && msg.type !== 'newError' && msg.type !== 'success') : messages;
71
71
 
72
- // Compute style props
73
- const hasMessages = filteredMessages && filteredMessages.length > 0 ? filteredMessages.some(msg => {
74
- if (msg.text) {
75
- if (typeof msg.text === 'string') {
76
- return msg.text.length > 0;
72
+ // any message, even if it's not visible (screenreader only)
73
+ let hasMessages = false;
74
+ let hasVisibleMessage = false;
75
+ if (filteredMessages) {
76
+ for (const msg of filteredMessages) {
77
+ if (msg.text) {
78
+ if (typeof msg.text === 'string') {
79
+ if (msg.text.length > 0) {
80
+ hasMessages = true;
81
+ if (msg.type !== 'screenreader-only') {
82
+ hasVisibleMessage = true;
83
+ }
84
+ }
85
+ } else {
86
+ // if the message is a React component we just assume its non-empty
87
+ hasMessages = true;
88
+ if (msg.type !== 'screenreader-only') {
89
+ hasVisibleMessage = true;
90
+ }
91
+ }
77
92
  }
78
- return true;
79
93
  }
80
- return false;
81
- }) : false;
94
+ }
82
95
  const hasVisibleLabel = label ? hasVisibleChildren(label) : false;
83
- const hasErrorMsgAndIsGroup = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError') && !!isGroup;
84
96
  const invalid = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError');
85
97
 
86
98
  // Styles
@@ -90,13 +102,13 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
90
102
  params: {
91
103
  hasMessages,
92
104
  hasVisibleLabel,
93
- hasErrorMsgAndIsGroup,
105
+ hasVisibleMessage,
106
+ isGroup: !!isGroup,
94
107
  inline,
95
108
  layout,
96
109
  vAlign,
97
110
  labelAlign,
98
111
  margin,
99
- messages: filteredMessages,
100
112
  isRequired,
101
113
  invalid
102
114
  },
@@ -167,7 +179,7 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
167
179
  });
168
180
  } else return null;
169
181
  };
170
- const renderVisibleMessages = () => {
182
+ const renderMessages = () => {
171
183
  return hasMessages ? _jsx(FormFieldMessages, {
172
184
  id: messagesId,
173
185
  messages: filteredMessages,
@@ -183,11 +195,11 @@ const FormFieldLayout = /*#__PURE__*/forwardRef((props, ref) => {
183
195
  width
184
196
  },
185
197
  ref: handleRef,
186
- children: [renderLabel(), hasErrorMsgAndIsGroup && renderVisibleMessages(), _jsx("span", {
198
+ children: [renderLabel(), isGroup && renderMessages(), _jsx("span", {
187
199
  css: styles?.formFieldChildren,
188
200
  ref: handleInputContainerRef,
189
201
  children: children
190
- }), !hasErrorMsgAndIsGroup && renderVisibleMessages()]
202
+ }), !isGroup && renderMessages()]
191
203
  });
192
204
  });
193
205
  FormFieldLayout.displayName = 'FormFieldLayout';
@@ -23,22 +23,18 @@
23
23
  */
24
24
 
25
25
  import { calcSpacingFromShorthand } from '@instructure/emotion';
26
- const generateGridLayout = (isInlineLayout, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages) => {
26
+ const generateGridLayout = (isInlineLayout, isGroup, hasVisibleLabel, hasMessages) => {
27
27
  if (isInlineLayout) {
28
- if (hasErrorMsgAndIsGroup) {
29
- if (hasMessages) {
30
- return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
31
- ". controls"`;
32
- } else {
33
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`;
34
- }
28
+ if (isGroup && hasMessages) {
29
+ return `${hasVisibleLabel ? ' "label messages"' : '". messages"'}
30
+ ${hasVisibleLabel ? ' "label controls"' : '". controls"'}`;
35
31
  } else {
36
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
32
+ return `${hasVisibleLabel ? ' "label controls"' : '". controls"'}
37
33
  ${hasMessages ? ' ". messages"' : ''}`;
38
34
  }
39
35
  }
40
36
  // stacked layout -- in this case we could use a simple `Flex`
41
- if (hasErrorMsgAndIsGroup) {
37
+ if (isGroup) {
42
38
  return `${hasVisibleLabel ? ' "label"' : ''}
43
39
  ${hasMessages ? ' "messages"' : ''}
44
40
  "controls"`;
@@ -65,22 +61,19 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
65
61
  vAlign,
66
62
  labelAlign,
67
63
  margin,
68
- messages
69
- } = params;
70
- const {
71
64
  hasMessages,
65
+ hasVisibleMessage,
72
66
  hasVisibleLabel,
73
- hasErrorMsgAndIsGroup
67
+ isGroup
74
68
  } = params;
75
69
  const cssMargin = calcSpacingFromShorthand(margin, {
76
70
  ...sharedTokens.spacing,
77
71
  ...sharedTokens.legacy.spacing
78
72
  });
79
73
  const isInlineLayout = layout === 'inline';
80
- const hasNonEmptyMessages = messages?.reduce((acc, message) => acc || message.type !== 'screenreader-only', false);
81
74
 
82
75
  // This is quite ugly, we should simplify it
83
- const gridTemplateAreas = generateGridLayout(isInlineLayout, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages);
76
+ const gridTemplateAreas = generateGridLayout(isInlineLayout, isGroup, hasVisibleLabel, hasMessages);
84
77
  let gridTemplateColumns = '100%'; // stacked layout
85
78
  if (isInlineLayout) {
86
79
  gridTemplateColumns = '1fr 3fr';
@@ -135,10 +128,10 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
135
128
  [`@media screen and (width < ${sharedTokens.breakpoints.md})`]: {
136
129
  // for small screens use the stacked layout
137
130
  gridTemplateColumns: '100%',
138
- gridTemplateAreas: generateGridLayout(false, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages)
131
+ gridTemplateAreas: generateGridLayout(false, isGroup, hasVisibleLabel, hasMessages)
139
132
  },
140
133
  columnGap: '0.375rem',
141
- rowGap: hasNonEmptyMessages ? componentTheme.gapPrimitives : '0',
134
+ rowGap: hasVisibleMessage ? componentTheme.gapPrimitives : '0',
142
135
  width: '100%',
143
136
  ...(inline && {
144
137
  display: 'inline-grid',
@@ -151,15 +144,11 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
151
144
  // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
152
145
  '&:is(label)': labelStyles,
153
146
  '&:-webkit-any(label)': labelStyles,
154
- paddingBottom: hasNonEmptyMessages ? '0' : componentTheme.gapPrimitives
147
+ paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives
155
148
  },
156
149
  formFieldChildren: {
157
150
  label: 'formFieldLayout__children',
158
151
  gridArea: 'controls',
159
- // add a small margin between the message and the controls
160
- ...(hasMessages && hasErrorMsgAndIsGroup && {
161
- marginTop: '0.375rem'
162
- }),
163
152
  ...(isInlineLayout && inline && {
164
153
  [`@media screen and (width >= ${sharedTokens.breakpoints.md})`]: {
165
154
  justifySelf: 'start'
@@ -68,6 +68,8 @@ let FormFieldMessages = (_dec = withStyleNew(generateStyle), _dec(_class = class
68
68
  messages,
69
69
  styles
70
70
  } = this.props;
71
+ // TODO try to only use this component in FormFieldLayout, then
72
+ // we could move a proper hasMessages logic here (see FormFieldLayout)
71
73
  return messages && messages.length > 0 ? _jsx("div", {
72
74
  css: styles?.formFieldMessages,
73
75
  ...omitProps(this.props, FormFieldMessages.allowedProps),
@@ -79,7 +81,7 @@ let FormFieldMessages = (_dec = withStyleNew(generateStyle), _dec(_class = class
79
81
  variant: msg.type,
80
82
  children: msg.text
81
83
  })
82
- }, `error${i}`);
84
+ }, `FormFieldMessage${i}`);
83
85
  })
84
86
  }) : null;
85
87
  }
@@ -84,18 +84,30 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
84
84
  // Filter out error and success messages when disabled or readOnly
85
85
  const filteredMessages = disabled || readOnly ? messages?.filter(msg => msg.type !== 'error' && msg.type !== 'newError' && msg.type !== 'success') : messages;
86
86
 
87
- // Compute style props
88
- const hasMessages = filteredMessages && filteredMessages.length > 0 ? filteredMessages.some(msg => {
89
- if (msg.text) {
90
- if (typeof msg.text === 'string') {
91
- return msg.text.length > 0;
87
+ // any message, even if it's not visible (screenreader only)
88
+ let hasMessages = false;
89
+ let hasVisibleMessage = false;
90
+ if (filteredMessages) {
91
+ for (const msg of filteredMessages) {
92
+ if (msg.text) {
93
+ if (typeof msg.text === 'string') {
94
+ if (msg.text.length > 0) {
95
+ hasMessages = true;
96
+ if (msg.type !== 'screenreader-only') {
97
+ hasVisibleMessage = true;
98
+ }
99
+ }
100
+ } else {
101
+ // if the message is a React component we just assume its non-empty
102
+ hasMessages = true;
103
+ if (msg.type !== 'screenreader-only') {
104
+ hasVisibleMessage = true;
105
+ }
106
+ }
92
107
  }
93
- return true;
94
108
  }
95
- return false;
96
- }) : false;
109
+ }
97
110
  const hasVisibleLabel = label ? (0, _hasVisibleChildren.hasVisibleChildren)(label) : false;
98
- const hasErrorMsgAndIsGroup = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError') && !!isGroup;
99
111
  const invalid = !!filteredMessages?.find(m => m.type === 'error' || m.type === 'newError');
100
112
 
101
113
  // Styles
@@ -105,13 +117,13 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
105
117
  params: {
106
118
  hasMessages,
107
119
  hasVisibleLabel,
108
- hasErrorMsgAndIsGroup,
120
+ hasVisibleMessage,
121
+ isGroup: !!isGroup,
109
122
  inline,
110
123
  layout,
111
124
  vAlign,
112
125
  labelAlign,
113
126
  margin,
114
- messages: filteredMessages,
115
127
  isRequired,
116
128
  invalid
117
129
  },
@@ -182,7 +194,7 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
182
194
  });
183
195
  } else return null;
184
196
  };
185
- const renderVisibleMessages = () => {
197
+ const renderMessages = () => {
186
198
  return hasMessages ? (0, _jsxRuntime.jsx)(_index.FormFieldMessages, {
187
199
  id: messagesId,
188
200
  messages: filteredMessages,
@@ -198,11 +210,11 @@ const FormFieldLayout = exports.FormFieldLayout = /*#__PURE__*/(0, _react.forwar
198
210
  width
199
211
  },
200
212
  ref: handleRef,
201
- children: [renderLabel(), hasErrorMsgAndIsGroup && renderVisibleMessages(), (0, _jsxRuntime.jsx)("span", {
213
+ children: [renderLabel(), isGroup && renderMessages(), (0, _jsxRuntime.jsx)("span", {
202
214
  css: styles?.formFieldChildren,
203
215
  ref: handleInputContainerRef,
204
216
  children: children
205
- }), !hasErrorMsgAndIsGroup && renderVisibleMessages()]
217
+ }), !isGroup && renderMessages()]
206
218
  });
207
219
  });
208
220
  FormFieldLayout.displayName = 'FormFieldLayout';
@@ -29,22 +29,18 @@ var _emotion = require("@instructure/emotion");
29
29
  * SOFTWARE.
30
30
  */
31
31
 
32
- const generateGridLayout = (isInlineLayout, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages) => {
32
+ const generateGridLayout = (isInlineLayout, isGroup, hasVisibleLabel, hasMessages) => {
33
33
  if (isInlineLayout) {
34
- if (hasErrorMsgAndIsGroup) {
35
- if (hasMessages) {
36
- return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
37
- ". controls"`;
38
- } else {
39
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`;
40
- }
34
+ if (isGroup && hasMessages) {
35
+ return `${hasVisibleLabel ? ' "label messages"' : '". messages"'}
36
+ ${hasVisibleLabel ? ' "label controls"' : '". controls"'}`;
41
37
  } else {
42
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
38
+ return `${hasVisibleLabel ? ' "label controls"' : '". controls"'}
43
39
  ${hasMessages ? ' ". messages"' : ''}`;
44
40
  }
45
41
  }
46
42
  // stacked layout -- in this case we could use a simple `Flex`
47
- if (hasErrorMsgAndIsGroup) {
43
+ if (isGroup) {
48
44
  return `${hasVisibleLabel ? ' "label"' : ''}
49
45
  ${hasMessages ? ' "messages"' : ''}
50
46
  "controls"`;
@@ -71,22 +67,19 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
71
67
  vAlign,
72
68
  labelAlign,
73
69
  margin,
74
- messages
75
- } = params;
76
- const {
77
70
  hasMessages,
71
+ hasVisibleMessage,
78
72
  hasVisibleLabel,
79
- hasErrorMsgAndIsGroup
73
+ isGroup
80
74
  } = params;
81
75
  const cssMargin = (0, _emotion.calcSpacingFromShorthand)(margin, {
82
76
  ...sharedTokens.spacing,
83
77
  ...sharedTokens.legacy.spacing
84
78
  });
85
79
  const isInlineLayout = layout === 'inline';
86
- const hasNonEmptyMessages = messages?.reduce((acc, message) => acc || message.type !== 'screenreader-only', false);
87
80
 
88
81
  // This is quite ugly, we should simplify it
89
- const gridTemplateAreas = generateGridLayout(isInlineLayout, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages);
82
+ const gridTemplateAreas = generateGridLayout(isInlineLayout, isGroup, hasVisibleLabel, hasMessages);
90
83
  let gridTemplateColumns = '100%'; // stacked layout
91
84
  if (isInlineLayout) {
92
85
  gridTemplateColumns = '1fr 3fr';
@@ -141,10 +134,10 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
141
134
  [`@media screen and (width < ${sharedTokens.breakpoints.md})`]: {
142
135
  // for small screens use the stacked layout
143
136
  gridTemplateColumns: '100%',
144
- gridTemplateAreas: generateGridLayout(false, hasErrorMsgAndIsGroup, hasVisibleLabel, hasMessages)
137
+ gridTemplateAreas: generateGridLayout(false, isGroup, hasVisibleLabel, hasMessages)
145
138
  },
146
139
  columnGap: '0.375rem',
147
- rowGap: hasNonEmptyMessages ? componentTheme.gapPrimitives : '0',
140
+ rowGap: hasVisibleMessage ? componentTheme.gapPrimitives : '0',
148
141
  width: '100%',
149
142
  ...(inline && {
150
143
  display: 'inline-grid',
@@ -157,15 +150,11 @@ const generateStyle = (componentTheme, params, sharedTokens) => {
157
150
  // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
158
151
  '&:is(label)': labelStyles,
159
152
  '&:-webkit-any(label)': labelStyles,
160
- paddingBottom: hasNonEmptyMessages ? '0' : componentTheme.gapPrimitives
153
+ paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives
161
154
  },
162
155
  formFieldChildren: {
163
156
  label: 'formFieldLayout__children',
164
157
  gridArea: 'controls',
165
- // add a small margin between the message and the controls
166
- ...(hasMessages && hasErrorMsgAndIsGroup && {
167
- marginTop: '0.375rem'
168
- }),
169
158
  ...(isInlineLayout && inline && {
170
159
  [`@media screen and (width >= ${sharedTokens.breakpoints.md})`]: {
171
160
  justifySelf: 'start'
@@ -74,6 +74,8 @@ let FormFieldMessages = exports.FormFieldMessages = (_dec = (0, _emotion.withSty
74
74
  messages,
75
75
  styles
76
76
  } = this.props;
77
+ // TODO try to only use this component in FormFieldLayout, then
78
+ // we could move a proper hasMessages logic here (see FormFieldLayout)
77
79
  return messages && messages.length > 0 ? (0, _jsxRuntime.jsx)("div", {
78
80
  css: styles?.formFieldMessages,
79
81
  ...(0, _omitProps.omitProps)(this.props, FormFieldMessages.allowedProps),
@@ -85,7 +87,7 @@ let FormFieldMessages = exports.FormFieldMessages = (_dec = (0, _emotion.withSty
85
87
  variant: msg.type,
86
88
  children: msg.text
87
89
  })
88
- }, `error${i}`);
90
+ }, `FormFieldMessage${i}`);
89
91
  })
90
92
  }) : null;
91
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-form-field",
3
- "version": "11.7.8-snapshot-9",
3
+ "version": "11.7.8-snapshot-10",
4
4
  "description": "Form layout components.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -15,22 +15,22 @@
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
17
  "@babel/runtime": "^7.29.7",
18
- "@instructure/emotion": "11.7.8-snapshot-9",
19
- "@instructure/console": "11.7.8-snapshot-9",
20
- "@instructure/shared-types": "11.7.8-snapshot-9",
21
- "@instructure/ui-a11y-utils": "11.7.8-snapshot-9",
22
- "@instructure/ui-a11y-content": "11.7.8-snapshot-9",
23
- "@instructure/ui-grid": "11.7.8-snapshot-9",
24
- "@instructure/ui-icons": "11.7.8-snapshot-9",
25
- "@instructure/ui-react-utils": "11.7.8-snapshot-9",
26
- "@instructure/ui-utils": "11.7.8-snapshot-9",
27
- "@instructure/uid": "11.7.8-snapshot-9",
28
- "@instructure/ui-themes": "11.7.8-snapshot-9"
18
+ "@instructure/emotion": "11.7.8-snapshot-10",
19
+ "@instructure/console": "11.7.8-snapshot-10",
20
+ "@instructure/ui-a11y-content": "11.7.8-snapshot-10",
21
+ "@instructure/shared-types": "11.7.8-snapshot-10",
22
+ "@instructure/ui-a11y-utils": "11.7.8-snapshot-10",
23
+ "@instructure/ui-grid": "11.7.8-snapshot-10",
24
+ "@instructure/ui-react-utils": "11.7.8-snapshot-10",
25
+ "@instructure/ui-icons": "11.7.8-snapshot-10",
26
+ "@instructure/ui-utils": "11.7.8-snapshot-10",
27
+ "@instructure/uid": "11.7.8-snapshot-10",
28
+ "@instructure/ui-themes": "11.7.8-snapshot-10"
29
29
  },
30
30
  "devDependencies": {
31
31
  "vitest": "^5.0.0",
32
- "@instructure/ui-babel-preset": "11.7.8-snapshot-9",
33
- "@instructure/ui-axe-check": "11.7.8-snapshot-9"
32
+ "@instructure/ui-axe-check": "11.7.8-snapshot-10",
33
+ "@instructure/ui-babel-preset": "11.7.8-snapshot-10"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18 <=19"
@@ -81,27 +81,32 @@ const FormFieldLayout = forwardRef<Element, FormFieldLayoutProps>(
81
81
  )
82
82
  : messages
83
83
 
84
- // Compute style props
85
- const hasMessages =
86
- filteredMessages && filteredMessages.length > 0
87
- ? filteredMessages.some((msg) => {
88
- if (msg.text) {
89
- if (typeof msg.text === 'string') {
90
- return msg.text.length > 0
84
+ // any message, even if it's not visible (screenreader only)
85
+ let hasMessages = false
86
+ let hasVisibleMessage = false
87
+ if (filteredMessages) {
88
+ for (const msg of filteredMessages) {
89
+ if (msg.text) {
90
+ if (typeof msg.text === 'string') {
91
+ if (msg.text.length > 0) {
92
+ hasMessages = true
93
+ if (msg.type !== 'screenreader-only') {
94
+ hasVisibleMessage = true
91
95
  }
92
- return true
93
96
  }
94
- return false
95
- })
96
- : false
97
+ } else {
98
+ // if the message is a React component we just assume its non-empty
99
+ hasMessages = true
100
+ if (msg.type !== 'screenreader-only') {
101
+ hasVisibleMessage = true
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
97
107
 
98
108
  const hasVisibleLabel = label ? hasVisibleChildren(label) : false
99
109
 
100
- const hasErrorMsgAndIsGroup =
101
- !!filteredMessages?.find(
102
- (m) => m.type === 'error' || m.type === 'newError'
103
- ) && !!isGroup
104
-
105
110
  const invalid = !!filteredMessages?.find(
106
111
  (m) => m.type === 'error' || m.type === 'newError'
107
112
  )
@@ -113,13 +118,13 @@ const FormFieldLayout = forwardRef<Element, FormFieldLayoutProps>(
113
118
  params: {
114
119
  hasMessages,
115
120
  hasVisibleLabel,
116
- hasErrorMsgAndIsGroup,
121
+ hasVisibleMessage,
122
+ isGroup: !!isGroup,
117
123
  inline,
118
124
  layout,
119
125
  vAlign,
120
126
  labelAlign,
121
127
  margin,
122
- messages: filteredMessages,
123
128
  isRequired,
124
129
  invalid
125
130
  },
@@ -204,7 +209,7 @@ const FormFieldLayout = forwardRef<Element, FormFieldLayoutProps>(
204
209
  } else return null
205
210
  }
206
211
 
207
- const renderVisibleMessages = () => {
212
+ const renderMessages = () => {
208
213
  return hasMessages ? (
209
214
  <FormFieldMessages
210
215
  id={messagesId}
@@ -224,11 +229,11 @@ const FormFieldLayout = forwardRef<Element, FormFieldLayoutProps>(
224
229
  ref={handleRef}
225
230
  >
226
231
  {renderLabel()}
227
- {hasErrorMsgAndIsGroup && renderVisibleMessages()}
232
+ {isGroup && renderMessages()}
228
233
  <span css={styles?.formFieldChildren} ref={handleInputContainerRef}>
229
234
  {children}
230
235
  </span>
231
- {!hasErrorMsgAndIsGroup && renderVisibleMessages()}
236
+ {!isGroup && renderMessages()}
232
237
  </ElementType>
233
238
  )
234
239
  }
@@ -151,8 +151,9 @@ const allowedProps: AllowedPropKeys = [
151
151
 
152
152
  type FormFieldStyleProps = {
153
153
  hasMessages: boolean
154
+ hasVisibleMessage: boolean
154
155
  hasVisibleLabel: boolean
155
- hasErrorMsgAndIsGroup: boolean
156
+ isGroup: boolean
156
157
  }
157
158
 
158
159
  export type {
@@ -36,32 +36,27 @@ type StyleParams = FormFieldStyleProps & {
36
36
  vAlign: FormFieldLayoutProps['vAlign']
37
37
  labelAlign: FormFieldLayoutProps['labelAlign']
38
38
  margin: FormFieldLayoutProps['margin']
39
- messages: FormFieldLayoutProps['messages']
40
39
  isRequired: FormFieldLayoutProps['isRequired']
41
40
  invalid: boolean
42
41
  }
43
42
 
44
43
  const generateGridLayout = (
45
44
  isInlineLayout: boolean,
46
- hasErrorMsgAndIsGroup: boolean,
45
+ isGroup: boolean,
47
46
  hasVisibleLabel: boolean,
48
47
  hasMessages: boolean
49
48
  ) => {
50
49
  if (isInlineLayout) {
51
- if (hasErrorMsgAndIsGroup) {
52
- if (hasMessages) {
53
- return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
54
- ". controls"`
55
- } else {
56
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`
57
- }
50
+ if (isGroup && hasMessages) {
51
+ return `${hasVisibleLabel ? ' "label messages"' : '". messages"'}
52
+ ${hasVisibleLabel ? ' "label controls"' : '". controls"'}`
58
53
  } else {
59
- return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
54
+ return `${hasVisibleLabel ? ' "label controls"' : '". controls"'}
60
55
  ${hasMessages ? ' ". messages"' : ''}`
61
56
  }
62
57
  }
63
58
  // stacked layout -- in this case we could use a simple `Flex`
64
- if (hasErrorMsgAndIsGroup) {
59
+ if (isGroup) {
65
60
  return `${hasVisibleLabel ? ' "label"' : ''}
66
61
  ${hasMessages ? ' "messages"' : ''}
67
62
  "controls"`
@@ -86,23 +81,27 @@ const generateStyle = (
86
81
  params: StyleParams,
87
82
  sharedTokens: SharedTokens
88
83
  ): FormFieldLayoutStyle => {
89
- const { inline, layout, vAlign, labelAlign, margin, messages } = params
90
- const { hasMessages, hasVisibleLabel, hasErrorMsgAndIsGroup } = params
84
+ const {
85
+ inline,
86
+ layout,
87
+ vAlign,
88
+ labelAlign,
89
+ margin,
90
+ hasMessages,
91
+ hasVisibleMessage,
92
+ hasVisibleLabel,
93
+ isGroup
94
+ } = params
91
95
  const cssMargin = calcSpacingFromShorthand(margin, {
92
96
  ...sharedTokens.spacing,
93
97
  ...sharedTokens.legacy.spacing
94
98
  })
95
99
  const isInlineLayout = layout === 'inline'
96
100
 
97
- const hasNonEmptyMessages = messages?.reduce(
98
- (acc, message) => acc || message.type !== 'screenreader-only',
99
- false
100
- )
101
-
102
101
  // This is quite ugly, we should simplify it
103
102
  const gridTemplateAreas = generateGridLayout(
104
103
  isInlineLayout,
105
- hasErrorMsgAndIsGroup,
104
+ isGroup,
106
105
  hasVisibleLabel,
107
106
  hasMessages
108
107
  )
@@ -162,13 +161,13 @@ const generateStyle = (
162
161
  gridTemplateColumns: '100%',
163
162
  gridTemplateAreas: generateGridLayout(
164
163
  false,
165
- hasErrorMsgAndIsGroup,
164
+ isGroup,
166
165
  hasVisibleLabel,
167
166
  hasMessages
168
167
  )
169
168
  },
170
169
  columnGap: '0.375rem',
171
- rowGap: hasNonEmptyMessages ? componentTheme.gapPrimitives : '0',
170
+ rowGap: hasVisibleMessage ? componentTheme.gapPrimitives : '0',
172
171
  width: '100%',
173
172
  ...(inline && {
174
173
  display: 'inline-grid',
@@ -181,14 +180,11 @@ const generateStyle = (
181
180
  // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
182
181
  '&:is(label)': labelStyles,
183
182
  '&:-webkit-any(label)': labelStyles,
184
-
185
- paddingBottom: hasNonEmptyMessages ? '0' : componentTheme.gapPrimitives
183
+ paddingBottom: hasVisibleMessage ? '0' : componentTheme.gapPrimitives
186
184
  },
187
185
  formFieldChildren: {
188
186
  label: 'formFieldLayout__children',
189
187
  gridArea: 'controls',
190
- // add a small margin between the message and the controls
191
- ...(hasMessages && hasErrorMsgAndIsGroup && { marginTop: '0.375rem' }),
192
188
  ...(isInlineLayout &&
193
189
  inline && {
194
190
  [`@media screen and (width >= ${sharedTokens.breakpoints.md})`]: {
@@ -77,7 +77,8 @@ class FormFieldMessages extends Component<FormFieldMessagesProps> {
77
77
 
78
78
  render() {
79
79
  const { messages, styles } = this.props
80
-
80
+ // TODO try to only use this component in FormFieldLayout, then
81
+ // we could move a proper hasMessages logic here (see FormFieldLayout)
81
82
  return messages && messages.length > 0 ? (
82
83
  <div
83
84
  css={styles?.formFieldMessages}
@@ -86,7 +87,7 @@ class FormFieldMessages extends Component<FormFieldMessagesProps> {
86
87
  >
87
88
  {messages.map((msg, i) => {
88
89
  return (
89
- <span key={`error${i}`} css={styles?.message}>
90
+ <span key={`FormFieldMessage${i}`} css={styles?.message}>
90
91
  <FormFieldMessage variant={msg.type}>{msg.text}</FormFieldMessage>
91
92
  </span>
92
93
  )