@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 +3 -4
- package/es/FormFieldLayout/v2/index.js +26 -14
- package/es/FormFieldLayout/v2/styles.js +12 -23
- package/es/FormFieldMessages/v2/index.js +3 -1
- package/lib/FormFieldLayout/v2/index.js +26 -14
- package/lib/FormFieldLayout/v2/styles.js +12 -23
- package/lib/FormFieldMessages/v2/index.js +3 -1
- package/package.json +14 -14
- package/src/FormFieldLayout/v2/index.tsx +26 -21
- package/src/FormFieldLayout/v2/props.ts +2 -1
- package/src/FormFieldLayout/v2/styles.ts +21 -25
- package/src/FormFieldMessages/v2/index.tsx +3 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/FormFieldLayout/v2/index.d.ts.map +1 -1
- package/types/FormFieldLayout/v2/props.d.ts +2 -1
- package/types/FormFieldLayout/v2/props.d.ts.map +1 -1
- package/types/FormFieldLayout/v2/styles.d.ts +0 -1
- package/types/FormFieldLayout/v2/styles.d.ts.map +1 -1
- package/types/FormFieldMessages/v2/index.d.ts.map +1 -1
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-
|
|
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
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(),
|
|
198
|
+
children: [renderLabel(), isGroup && renderMessages(), _jsx("span", {
|
|
187
199
|
css: styles?.formFieldChildren,
|
|
188
200
|
ref: handleInputContainerRef,
|
|
189
201
|
children: children
|
|
190
|
-
}), !
|
|
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,
|
|
26
|
+
const generateGridLayout = (isInlineLayout, isGroup, hasVisibleLabel, hasMessages) => {
|
|
27
27
|
if (isInlineLayout) {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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,
|
|
131
|
+
gridTemplateAreas: generateGridLayout(false, isGroup, hasVisibleLabel, hasMessages)
|
|
139
132
|
},
|
|
140
133
|
columnGap: '0.375rem',
|
|
141
|
-
rowGap:
|
|
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:
|
|
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
|
-
}, `
|
|
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
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(),
|
|
213
|
+
children: [renderLabel(), isGroup && renderMessages(), (0, _jsxRuntime.jsx)("span", {
|
|
202
214
|
css: styles?.formFieldChildren,
|
|
203
215
|
ref: handleInputContainerRef,
|
|
204
216
|
children: children
|
|
205
|
-
}), !
|
|
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,
|
|
32
|
+
const generateGridLayout = (isInlineLayout, isGroup, hasVisibleLabel, hasMessages) => {
|
|
33
33
|
if (isInlineLayout) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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,
|
|
137
|
+
gridTemplateAreas: generateGridLayout(false, isGroup, hasVisibleLabel, hasMessages)
|
|
145
138
|
},
|
|
146
139
|
columnGap: '0.375rem',
|
|
147
|
-
rowGap:
|
|
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:
|
|
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
|
-
}, `
|
|
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-
|
|
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-
|
|
19
|
-
"@instructure/console": "11.7.8-snapshot-
|
|
20
|
-
"@instructure/
|
|
21
|
-
"@instructure/
|
|
22
|
-
"@instructure/ui-a11y-
|
|
23
|
-
"@instructure/ui-grid": "11.7.8-snapshot-
|
|
24
|
-
"@instructure/ui-
|
|
25
|
-
"@instructure/ui-
|
|
26
|
-
"@instructure/ui-utils": "11.7.8-snapshot-
|
|
27
|
-
"@instructure/uid": "11.7.8-snapshot-
|
|
28
|
-
"@instructure/ui-themes": "11.7.8-snapshot-
|
|
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-
|
|
33
|
-
"@instructure/ui-
|
|
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
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
232
|
+
{isGroup && renderMessages()}
|
|
228
233
|
<span css={styles?.formFieldChildren} ref={handleInputContainerRef}>
|
|
229
234
|
{children}
|
|
230
235
|
</span>
|
|
231
|
-
{!
|
|
236
|
+
{!isGroup && renderMessages()}
|
|
232
237
|
</ElementType>
|
|
233
238
|
)
|
|
234
239
|
}
|
|
@@ -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
|
-
|
|
45
|
+
isGroup: boolean,
|
|
47
46
|
hasVisibleLabel: boolean,
|
|
48
47
|
hasMessages: boolean
|
|
49
48
|
) => {
|
|
50
49
|
if (isInlineLayout) {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
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 {
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
isGroup,
|
|
166
165
|
hasVisibleLabel,
|
|
167
166
|
hasMessages
|
|
168
167
|
)
|
|
169
168
|
},
|
|
170
169
|
columnGap: '0.375rem',
|
|
171
|
-
rowGap:
|
|
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={`
|
|
90
|
+
<span key={`FormFieldMessage${i}`} css={styles?.message}>
|
|
90
91
|
<FormFieldMessage variant={msg.type}>{msg.text}</FormFieldMessage>
|
|
91
92
|
</span>
|
|
92
93
|
)
|