@instructure/ui-form-field 10.12.0 → 10.12.1-snapshot-0
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 +11 -0
- package/es/FormField/index.js +0 -1
- package/es/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
- package/es/FormFieldGroup/index.js +18 -4
- package/es/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +10 -8
- package/es/FormFieldLayout/index.js +73 -54
- package/es/FormFieldLayout/styles.js +109 -10
- package/es/FormFieldLayout/theme.js +28 -2
- package/es/FormFieldMessages/props.js +3 -2
- package/es/FormFieldMessages/styles.js +5 -3
- package/es/FormPropTypes.js +6 -0
- package/es/index.js +0 -1
- package/lib/FormField/index.js +0 -1
- package/lib/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
- package/lib/FormFieldGroup/index.js +18 -4
- package/lib/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +9 -7
- package/lib/FormFieldLayout/index.js +72 -54
- package/lib/FormFieldLayout/styles.js +109 -10
- package/lib/FormFieldLayout/theme.js +28 -2
- package/lib/FormFieldMessages/props.js +3 -2
- package/lib/FormFieldMessages/styles.js +5 -3
- package/lib/FormPropTypes.js +6 -0
- package/lib/index.js +0 -7
- package/package.json +15 -15
- package/src/FormField/README.md +31 -3
- package/src/FormField/index.tsx +0 -1
- package/src/FormFieldGroup/__new-tests__/FormFieldGroup.test.tsx +4 -6
- package/src/FormFieldGroup/index.tsx +41 -6
- package/src/FormFieldLayout/__new-tests__/FormFieldLayout.test.tsx +6 -8
- package/src/FormFieldLayout/index.tsx +78 -92
- package/src/FormFieldLayout/props.ts +28 -2
- package/src/FormFieldLayout/styles.ts +128 -14
- package/src/FormFieldLayout/theme.ts +30 -4
- package/src/FormFieldMessages/props.ts +8 -2
- package/src/FormFieldMessages/styles.ts +5 -4
- package/src/FormPropTypes.ts +4 -0
- package/src/index.ts +0 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/types/FormField/index.d.ts.map +1 -1
- package/types/FormFieldGroup/index.d.ts +1 -0
- package/types/FormFieldGroup/index.d.ts.map +1 -1
- package/types/FormFieldLayout/index.d.ts +8 -7
- package/types/FormFieldLayout/index.d.ts.map +1 -1
- package/types/FormFieldLayout/props.d.ts +27 -3
- package/types/FormFieldLayout/props.d.ts.map +1 -1
- package/types/FormFieldLayout/styles.d.ts +4 -3
- package/types/FormFieldLayout/styles.d.ts.map +1 -1
- package/types/FormFieldLayout/theme.d.ts +7 -1
- package/types/FormFieldLayout/theme.d.ts.map +1 -1
- package/types/FormFieldMessages/index.d.ts +8 -2
- package/types/FormFieldMessages/index.d.ts.map +1 -1
- package/types/FormFieldMessages/props.d.ts +5 -0
- package/types/FormFieldMessages/props.d.ts.map +1 -1
- package/types/FormFieldMessages/styles.d.ts +2 -3
- package/types/FormFieldMessages/styles.d.ts.map +1 -1
- package/types/FormPropTypes.d.ts +3 -0
- package/types/FormPropTypes.d.ts.map +1 -1
- package/types/index.d.ts +0 -2
- package/types/index.d.ts.map +1 -1
- package/es/FormFieldLabel/__new-tests__/FormFieldLabel.test.js +0 -66
- package/es/FormFieldLabel/index.js +0 -79
- package/es/FormFieldLabel/props.js +0 -31
- package/es/FormFieldLabel/styles.js +0 -62
- package/es/FormFieldLabel/theme.js +0 -52
- package/lib/FormFieldLabel/__new-tests__/FormFieldLabel.test.js +0 -68
- package/lib/FormFieldLabel/index.js +0 -85
- package/lib/FormFieldLabel/props.js +0 -37
- package/lib/FormFieldLabel/styles.js +0 -68
- package/lib/FormFieldLabel/theme.js +0 -58
- package/src/FormFieldLabel/__new-tests__/FormFieldLabel.test.tsx +0 -79
- package/src/FormFieldLabel/index.tsx +0 -95
- package/src/FormFieldLabel/props.ts +0 -58
- package/src/FormFieldLabel/styles.ts +0 -74
- package/src/FormFieldLabel/theme.ts +0 -56
- package/types/FormFieldLabel/__new-tests__/FormFieldLabel.test.d.ts +0 -2
- package/types/FormFieldLabel/__new-tests__/FormFieldLabel.test.d.ts.map +0 -1
- package/types/FormFieldLabel/index.d.ts +0 -42
- package/types/FormFieldLabel/index.d.ts.map +0 -1
- package/types/FormFieldLabel/props.d.ts +0 -15
- package/types/FormFieldLabel/props.d.ts.map +0 -1
- package/types/FormFieldLabel/styles.d.ts +0 -15
- package/types/FormFieldLabel/styles.d.ts.map +0 -1
- package/types/FormFieldLabel/theme.d.ts +0 -10
- package/types/FormFieldLabel/theme.d.ts.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
## [10.12.1-snapshot-0](https://github.com/instructure/instructure-ui/compare/v10.12.0...v10.12.1-snapshot-0) (2025-02-26)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **many:** fix form label not read by NVDA in hover mode and other layout issues ([ef77281](https://github.com/instructure/instructure-ui/commit/ef77281890511e8eea794196445d3ef2454537ba))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [10.12.0](https://github.com/instructure/instructure-ui/compare/v10.11.0...v10.12.0) (2025-02-24)
|
|
7
18
|
|
|
8
19
|
|
package/es/FormField/index.js
CHANGED
|
@@ -46,7 +46,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
46
46
|
description: "Please enter your full name"
|
|
47
47
|
}, /*#__PURE__*/React.createElement("label", null, "First: ", /*#__PURE__*/React.createElement("input", null)), /*#__PURE__*/React.createElement("label", null, "Middle: ", /*#__PURE__*/React.createElement("input", null)), /*#__PURE__*/React.createElement("label", null, "Last: ", /*#__PURE__*/React.createElement("input", null))))),
|
|
48
48
|
container = _render.container;
|
|
49
|
-
const formFieldGroup = container.querySelector("
|
|
49
|
+
const formFieldGroup = container.querySelector("span[class$='-formFieldLayout__label']");
|
|
50
50
|
const firstNameInput = screen.getByLabelText('First:');
|
|
51
51
|
const middleNameInput = screen.getByLabelText('Middle:');
|
|
52
52
|
const lastNameInput = screen.getByLabelText('Last:');
|
|
@@ -64,7 +64,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
64
64
|
description: "Please enter your full name"
|
|
65
65
|
}, children)),
|
|
66
66
|
container = _render2.container;
|
|
67
|
-
const formFieldGroup = container.querySelector(
|
|
67
|
+
const formFieldGroup = container.querySelector('label');
|
|
68
68
|
expect(formFieldGroup).toBeInTheDocument();
|
|
69
69
|
});
|
|
70
70
|
it('links the messages to the fieldset via aria-describedby', () => {
|
|
@@ -86,13 +86,13 @@ describe('<FormFieldGroup />', () => {
|
|
|
86
86
|
expect(message).toHaveTextContent('Invalid name');
|
|
87
87
|
expect(message).toHaveAttribute('id', messagesId);
|
|
88
88
|
});
|
|
89
|
-
it('displays description message inside the
|
|
89
|
+
it('displays description message inside the label', () => {
|
|
90
90
|
const description = 'Please enter your full name';
|
|
91
91
|
const _render4 = render(/*#__PURE__*/React.createElement(FormFieldGroup, {
|
|
92
92
|
description: description
|
|
93
93
|
}, _label5 || (_label5 = /*#__PURE__*/React.createElement("label", null, "First: ", /*#__PURE__*/React.createElement("input", null))), _label6 || (_label6 = /*#__PURE__*/React.createElement("label", null, "Middle: ", /*#__PURE__*/React.createElement("input", null))), _label7 || (_label7 = /*#__PURE__*/React.createElement("label", null, "Last: ", /*#__PURE__*/React.createElement("input", null))))),
|
|
94
94
|
container = _render4.container;
|
|
95
|
-
const legend = container.querySelector("
|
|
95
|
+
const legend = container.querySelector("span[class$='-formFieldLayout__label']");
|
|
96
96
|
expect(legend).toBeInTheDocument();
|
|
97
97
|
expect(legend).toHaveTextContent(description);
|
|
98
98
|
});
|
|
@@ -60,13 +60,17 @@ let FormFieldGroup = (_dec = withStyle(generateStyle, generateComponentTheme), _
|
|
|
60
60
|
(_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2, this.makeStylesVariables);
|
|
61
61
|
}
|
|
62
62
|
get makeStylesVariables() {
|
|
63
|
+
// new form errors dont need borders
|
|
64
|
+
const oldInvalid = !!this.props.messages && this.props.messages.findIndex(message => {
|
|
65
|
+
return message.type === 'error';
|
|
66
|
+
}) >= 0;
|
|
63
67
|
return {
|
|
64
|
-
invalid:
|
|
68
|
+
invalid: oldInvalid
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
get invalid() {
|
|
68
72
|
return !!this.props.messages && this.props.messages.findIndex(message => {
|
|
69
|
-
return message.type === 'error';
|
|
73
|
+
return message.type === 'error' || message.type === 'newError';
|
|
70
74
|
}) >= 0;
|
|
71
75
|
}
|
|
72
76
|
renderColumns() {
|
|
@@ -98,12 +102,21 @@ let FormFieldGroup = (_dec = withStyle(generateStyle, generateComponentTheme), _
|
|
|
98
102
|
makeStyles = _this$props3.makeStyles,
|
|
99
103
|
isGroup = _this$props3.isGroup,
|
|
100
104
|
props = _objectWithoutProperties(_this$props3, _excluded);
|
|
105
|
+
// This is quite ugly, but according to ARIA spec the `aria-invalid` prop
|
|
106
|
+
// can only be used with certain roles see
|
|
107
|
+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid#associated_roles
|
|
108
|
+
// `aria-invalid` is put on in FormFieldLayout because the error message
|
|
109
|
+
// DOM part gets there its ID.
|
|
110
|
+
let ariaInvalid = void 0;
|
|
111
|
+
if (this.props.role && this.invalid && ['application', 'checkbox', 'combobox', 'gridcell', 'listbox', 'radiogroup', 'slider', 'spinbutton', 'textbox', 'tree', 'columnheader', 'rowheader', 'searchbox', 'switch', 'treegrid'].includes(this.props.role)) {
|
|
112
|
+
ariaInvalid = 'true';
|
|
113
|
+
}
|
|
101
114
|
return jsx(FormFieldLayout, Object.assign({}, omitProps(props, FormFieldGroup.allowedProps), pickProps(props, FormFieldLayout.allowedProps), {
|
|
102
115
|
vAlign: props.vAlign,
|
|
103
116
|
layout: props.layout === 'inline' ? 'inline' : 'stacked',
|
|
104
117
|
label: props.description,
|
|
105
118
|
"aria-disabled": props.disabled ? 'true' : void 0,
|
|
106
|
-
"aria-invalid":
|
|
119
|
+
"aria-invalid": ariaInvalid,
|
|
107
120
|
elementRef: this.handleRef,
|
|
108
121
|
isGroup: isGroup
|
|
109
122
|
}), this.renderFields());
|
|
@@ -113,7 +126,8 @@ let FormFieldGroup = (_dec = withStyle(generateStyle, generateComponentTheme), _
|
|
|
113
126
|
disabled: false,
|
|
114
127
|
rowSpacing: 'medium',
|
|
115
128
|
colSpacing: 'small',
|
|
116
|
-
vAlign: 'middle'
|
|
129
|
+
vAlign: 'middle',
|
|
130
|
+
isGroup: true
|
|
117
131
|
}, _FormFieldGroup)) || _class);
|
|
118
132
|
export default FormFieldGroup;
|
|
119
133
|
export { FormFieldGroup };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var _FormFieldLayout, _FormFieldLayout2
|
|
1
|
+
var _FormFieldLayout, _FormFieldLayout2;
|
|
2
2
|
/*
|
|
3
3
|
* The MIT License (MIT)
|
|
4
4
|
*
|
|
@@ -24,7 +24,7 @@ var _FormFieldLayout, _FormFieldLayout2, _input;
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import React from 'react';
|
|
27
|
-
import { render
|
|
27
|
+
import { render } from '@testing-library/react';
|
|
28
28
|
import { vi } from 'vitest';
|
|
29
29
|
import { runAxeCheck } from '@instructure/ui-axe-check';
|
|
30
30
|
import '@testing-library/jest-dom';
|
|
@@ -47,7 +47,7 @@ describe('<FormFieldLayout />', () => {
|
|
|
47
47
|
}))),
|
|
48
48
|
container = _render.container;
|
|
49
49
|
const formFieldLayout = container.querySelector("label[class$='-formFieldLayout']");
|
|
50
|
-
const formFieldLabel = container.querySelector("span[class$='-
|
|
50
|
+
const formFieldLabel = container.querySelector("span[class$='-formFieldLayout__label']");
|
|
51
51
|
expect(formFieldLayout).toBeInTheDocument();
|
|
52
52
|
expect(formFieldLabel).toBeInTheDocument();
|
|
53
53
|
expect(formFieldLabel).toHaveTextContent('Username');
|
|
@@ -62,13 +62,15 @@ describe('<FormFieldLayout />', () => {
|
|
|
62
62
|
});
|
|
63
63
|
it('should provide a ref to the input container', () => {
|
|
64
64
|
const inputContainerRef = vi.fn();
|
|
65
|
+
const ref = /*#__PURE__*/React.createRef();
|
|
65
66
|
render(/*#__PURE__*/React.createElement(FormFieldLayout, {
|
|
66
67
|
label: "Username",
|
|
67
68
|
inputContainerRef: inputContainerRef
|
|
68
|
-
},
|
|
69
|
-
type: "text"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
expect(
|
|
69
|
+
}, /*#__PURE__*/React.createElement("input", {
|
|
70
|
+
type: "text",
|
|
71
|
+
ref: ref
|
|
72
|
+
})));
|
|
73
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement);
|
|
74
|
+
expect(inputContainerRef).toHaveBeenCalledWith(ref.current.parentElement);
|
|
73
75
|
});
|
|
74
76
|
});
|
|
@@ -28,11 +28,8 @@ var _dec, _dec2, _class, _FormFieldLayout;
|
|
|
28
28
|
/** @jsx jsx */
|
|
29
29
|
import { Component } from 'react';
|
|
30
30
|
import { hasVisibleChildren } from '@instructure/ui-a11y-utils';
|
|
31
|
-
import {
|
|
32
|
-
import { Grid } from '@instructure/ui-grid';
|
|
33
|
-
import { omitProps, pickProps, getElementType, withDeterministicId } from '@instructure/ui-react-utils';
|
|
31
|
+
import { omitProps, getElementType, withDeterministicId } from '@instructure/ui-react-utils';
|
|
34
32
|
import { withStyle, jsx } from '@instructure/emotion';
|
|
35
|
-
import { FormFieldLabel } from '../FormFieldLabel';
|
|
36
33
|
import { FormFieldMessages } from '../FormFieldMessages';
|
|
37
34
|
import generateStyle from './styles';
|
|
38
35
|
import { propTypes, allowedProps } from './props';
|
|
@@ -47,6 +44,7 @@ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateS
|
|
|
47
44
|
constructor(props) {
|
|
48
45
|
super(props);
|
|
49
46
|
this._messagesId = void 0;
|
|
47
|
+
this._labelId = void 0;
|
|
50
48
|
this.ref = null;
|
|
51
49
|
this.handleRef = el => {
|
|
52
50
|
const elementRef = this.props.elementRef;
|
|
@@ -55,69 +53,96 @@ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateS
|
|
|
55
53
|
elementRef(el);
|
|
56
54
|
}
|
|
57
55
|
};
|
|
56
|
+
this.makeStyleProps = () => {
|
|
57
|
+
var _this$props$messages;
|
|
58
|
+
const hasNewErrorMsgAndIsGroup = !!((_this$props$messages = this.props.messages) !== null && _this$props$messages !== void 0 && _this$props$messages.find(m => m.type === 'newError')) && !!this.props.isGroup;
|
|
59
|
+
return {
|
|
60
|
+
hasMessages: this.hasMessages,
|
|
61
|
+
hasVisibleLabel: this.hasVisibleLabel,
|
|
62
|
+
// if true render error message above the controls (and below the label)
|
|
63
|
+
hasNewErrorMsgAndIsGroup: hasNewErrorMsgAndIsGroup
|
|
64
|
+
};
|
|
65
|
+
};
|
|
58
66
|
this.handleInputContainerRef = node => {
|
|
59
67
|
if (typeof this.props.inputContainerRef === 'function') {
|
|
60
68
|
this.props.inputContainerRef(node);
|
|
61
69
|
}
|
|
62
70
|
};
|
|
63
71
|
this._messagesId = props.messagesId || props.deterministicId();
|
|
72
|
+
this._labelId = props.deterministicId('FormField-Label');
|
|
64
73
|
}
|
|
65
74
|
componentDidMount() {
|
|
66
75
|
var _this$props$makeStyle, _this$props;
|
|
67
|
-
(_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props);
|
|
76
|
+
(_this$props$makeStyle = (_this$props = this.props).makeStyles) === null || _this$props$makeStyle === void 0 ? void 0 : _this$props$makeStyle.call(_this$props, this.makeStyleProps());
|
|
68
77
|
}
|
|
69
78
|
componentDidUpdate() {
|
|
70
79
|
var _this$props$makeStyle2, _this$props2;
|
|
71
|
-
(_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2);
|
|
80
|
+
(_this$props$makeStyle2 = (_this$props2 = this.props).makeStyles) === null || _this$props$makeStyle2 === void 0 ? void 0 : _this$props$makeStyle2.call(_this$props2, this.makeStyleProps());
|
|
72
81
|
}
|
|
73
82
|
get hasVisibleLabel() {
|
|
74
|
-
return this.props.label
|
|
83
|
+
return this.props.label ? hasVisibleChildren(this.props.label) : false;
|
|
75
84
|
}
|
|
76
85
|
get hasMessages() {
|
|
77
|
-
|
|
86
|
+
if (!this.props.messages || this.props.messages.length == 0) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
for (const msg of this.props.messages) {
|
|
90
|
+
if (msg.text) {
|
|
91
|
+
if (typeof msg.text === 'string') {
|
|
92
|
+
return msg.text.length > 0;
|
|
93
|
+
}
|
|
94
|
+
// this is more complicated (e.g. an array, a React component,...)
|
|
95
|
+
// but we don't try to optimize here for these cases
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
78
100
|
}
|
|
79
101
|
get elementType() {
|
|
80
102
|
return getElementType(FormFieldLayout, this.props);
|
|
81
103
|
}
|
|
82
|
-
get inlineContainerAndLabel() {
|
|
83
|
-
// Return if both the component container and label will display inline
|
|
84
|
-
return this.props.inline && this.props.layout === 'inline';
|
|
85
|
-
}
|
|
86
104
|
renderLabel() {
|
|
87
105
|
if (this.hasVisibleLabel) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
var _this$props$styles2;
|
|
107
|
+
if (this.elementType == 'fieldset') {
|
|
108
|
+
var _this$props$styles;
|
|
109
|
+
// `legend` has some special built in CSS, this can only be reset
|
|
110
|
+
// this way https://stackoverflow.com/a/65866981/319473
|
|
111
|
+
return jsx("legend", {
|
|
112
|
+
style: {
|
|
113
|
+
display: 'contents'
|
|
114
|
+
}
|
|
115
|
+
}, jsx("span", {
|
|
116
|
+
css: (_this$props$styles = this.props.styles) === null || _this$props$styles === void 0 ? void 0 : _this$props$styles.formFieldLabel
|
|
117
|
+
}, this.props.label));
|
|
118
|
+
}
|
|
119
|
+
return jsx("span", {
|
|
120
|
+
css: (_this$props$styles2 = this.props.styles) === null || _this$props$styles2 === void 0 ? void 0 : _this$props$styles2.formFieldLabel
|
|
121
|
+
}, this.props.label);
|
|
122
|
+
} else if (this.props.label) {
|
|
123
|
+
if (this.elementType == 'fieldset') {
|
|
124
|
+
return jsx("legend", {
|
|
125
|
+
id: this._labelId,
|
|
126
|
+
style: {
|
|
127
|
+
display: 'contents'
|
|
128
|
+
}
|
|
129
|
+
}, this.props.label);
|
|
130
|
+
}
|
|
131
|
+
// needs to be wrapped because it needs an `id`
|
|
132
|
+
return jsx("div", {
|
|
133
|
+
id: this._labelId
|
|
134
|
+
}, this.props.label);
|
|
135
|
+
} else return null;
|
|
109
136
|
}
|
|
110
137
|
renderVisibleMessages() {
|
|
111
|
-
return this.hasMessages ? jsx(
|
|
112
|
-
offset: this.inlineContainerAndLabel ? void 0 : 3,
|
|
113
|
-
textAlign: this.inlineContainerAndLabel ? 'end' : void 0
|
|
114
|
-
}, jsx(FormFieldMessages, {
|
|
138
|
+
return this.hasMessages ? jsx(FormFieldMessages, {
|
|
115
139
|
id: this._messagesId,
|
|
116
|
-
messages: this.props.messages
|
|
117
|
-
|
|
140
|
+
messages: this.props.messages,
|
|
141
|
+
gridArea: "messages"
|
|
142
|
+
}) : null;
|
|
118
143
|
}
|
|
119
144
|
render() {
|
|
120
|
-
//
|
|
145
|
+
// Should be `<label>` if it's a FormField, fieldset if it's a group
|
|
121
146
|
const ElementType = this.elementType;
|
|
122
147
|
const _this$props3 = this.props,
|
|
123
148
|
makeStyles = _this$props3.makeStyles,
|
|
@@ -126,26 +151,20 @@ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateS
|
|
|
126
151
|
isGroup = _this$props3.isGroup,
|
|
127
152
|
props = _objectWithoutProperties(_this$props3, _excluded);
|
|
128
153
|
const width = props.width,
|
|
129
|
-
layout = props.layout,
|
|
130
154
|
children = props.children;
|
|
131
|
-
const
|
|
132
|
-
return jsx(ElementType, Object.assign({}, omitProps(props, [...FormFieldLayout.allowedProps
|
|
155
|
+
const hasNewErrorMsgAndIsGroup = !!(messages !== null && messages !== void 0 && messages.find(m => m.type === 'newError')) && isGroup;
|
|
156
|
+
return jsx(ElementType, Object.assign({}, omitProps(props, [...FormFieldLayout.allowedProps]), {
|
|
133
157
|
css: styles === null || styles === void 0 ? void 0 : styles.formFieldLayout,
|
|
158
|
+
"aria-describedby": this.hasMessages ? this._messagesId : void 0,
|
|
159
|
+
"aria-errormessage": this.props['aria-invalid'] ? this._messagesId : void 0,
|
|
134
160
|
style: {
|
|
135
161
|
width
|
|
136
162
|
},
|
|
137
|
-
"aria-describedby": this.hasMessages ? this._messagesId : void 0,
|
|
138
163
|
ref: this.handleRef
|
|
139
|
-
}), this.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}, pickProps(props, Grid.allowedProps)), jsx(Grid.Row, null, this.renderLabel(), jsx(Grid.Col, {
|
|
144
|
-
width: this.inlineContainerAndLabel ? 'auto' : void 0,
|
|
145
|
-
elementRef: this.handleInputContainerRef
|
|
146
|
-
}, hasNewErrorMsg && jsx("div", {
|
|
147
|
-
css: styles === null || styles === void 0 ? void 0 : styles.groupErrorMessage
|
|
148
|
-
}, this.renderVisibleMessages()), children)), !hasNewErrorMsg && this.renderVisibleMessages()));
|
|
164
|
+
}), this.renderLabel(), hasNewErrorMsgAndIsGroup && this.renderVisibleMessages(), jsx("span", {
|
|
165
|
+
css: styles === null || styles === void 0 ? void 0 : styles.formFieldChildren,
|
|
166
|
+
ref: this.handleInputContainerRef
|
|
167
|
+
}, children), !hasNewErrorMsgAndIsGroup && this.renderVisibleMessages());
|
|
149
168
|
}
|
|
150
169
|
}, _FormFieldLayout.displayName = "FormFieldLayout", _FormFieldLayout.componentId = 'FormFieldLayout', _FormFieldLayout.propTypes = propTypes, _FormFieldLayout.allowedProps = allowedProps, _FormFieldLayout.defaultProps = {
|
|
151
170
|
inline: false,
|
|
@@ -23,6 +23,31 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { mapSpacingToShorthand } from '@instructure/emotion';
|
|
26
|
+
const generateGridLayout = (isInlineLayout, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages) => {
|
|
27
|
+
if (isInlineLayout) {
|
|
28
|
+
if (hasNewErrorMsgAndIsGroup) {
|
|
29
|
+
if (hasMessages) {
|
|
30
|
+
return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
|
|
31
|
+
". controls"`;
|
|
32
|
+
} else {
|
|
33
|
+
return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
|
|
37
|
+
${hasMessages ? ' ". messages"' : ''}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// stacked layout -- in this case we could use a simple `Flex`
|
|
41
|
+
if (hasNewErrorMsgAndIsGroup) {
|
|
42
|
+
return `${hasVisibleLabel ? ' "label"' : ''}
|
|
43
|
+
${hasMessages ? ' "messages"' : ''}
|
|
44
|
+
"controls"`;
|
|
45
|
+
} else {
|
|
46
|
+
return `${hasVisibleLabel ? ' "label"' : ''}
|
|
47
|
+
"controls"
|
|
48
|
+
${hasMessages ? ' "messages"' : ''}`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
26
51
|
/**
|
|
27
52
|
* ---
|
|
28
53
|
* private: true
|
|
@@ -30,18 +55,57 @@ import { mapSpacingToShorthand } from '@instructure/emotion';
|
|
|
30
55
|
* Generates the style object from the theme and provided additional information
|
|
31
56
|
* @param {Object} componentTheme The theme variable object.
|
|
32
57
|
* @param {Object} props the props of the component, the style is applied to
|
|
33
|
-
* @param {Object}
|
|
58
|
+
* @param {Object} styleProps
|
|
34
59
|
* @return {Object} The final style object, which will be used in the component
|
|
35
60
|
*/
|
|
36
|
-
const generateStyle = (componentTheme, props) => {
|
|
61
|
+
const generateStyle = (componentTheme, props, styleProps) => {
|
|
37
62
|
const inline = props.inline,
|
|
63
|
+
layout = props.layout,
|
|
64
|
+
vAlign = props.vAlign,
|
|
65
|
+
labelAlign = props.labelAlign,
|
|
38
66
|
margin = props.margin;
|
|
39
|
-
const
|
|
40
|
-
|
|
67
|
+
const hasMessages = styleProps.hasMessages,
|
|
68
|
+
hasVisibleLabel = styleProps.hasVisibleLabel,
|
|
69
|
+
hasNewErrorMsgAndIsGroup = styleProps.hasNewErrorMsgAndIsGroup;
|
|
70
|
+
const cssMargin = mapSpacingToShorthand(margin, componentTheme.spacing);
|
|
71
|
+
const isInlineLayout = layout === 'inline';
|
|
72
|
+
// This is quite ugly, we should simplify it
|
|
73
|
+
const gridTemplateAreas = generateGridLayout(isInlineLayout, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages);
|
|
74
|
+
let gridTemplateColumns = '100%'; // stacked layout
|
|
75
|
+
if (isInlineLayout) {
|
|
76
|
+
gridTemplateColumns = '1fr 3fr';
|
|
77
|
+
if (inline) {
|
|
78
|
+
gridTemplateColumns = 'auto 3fr';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const labelStyles = {
|
|
82
|
+
all: 'initial',
|
|
83
|
+
display: 'block',
|
|
84
|
+
gridArea: 'label',
|
|
85
|
+
color: componentTheme.color,
|
|
86
|
+
fontFamily: componentTheme.fontFamily,
|
|
87
|
+
fontWeight: componentTheme.fontWeight,
|
|
88
|
+
fontSize: componentTheme.fontSize,
|
|
89
|
+
lineHeight: componentTheme.lineHeight,
|
|
90
|
+
margin: '0 0 0.75rem 0',
|
|
91
|
+
...(isInlineLayout && {
|
|
92
|
+
// when inline add a small padding between the label and the control
|
|
93
|
+
paddingRight: componentTheme.inlinePadding,
|
|
94
|
+
// and use the horizontal alignment prop
|
|
95
|
+
[`@media screen and (min-width: ${componentTheme.stackedOrInlineBreakpoint})`]: {
|
|
96
|
+
textAlign: labelAlign
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
let alignItems = 'start';
|
|
101
|
+
if (vAlign == 'top') {
|
|
102
|
+
alignItems = 'start';
|
|
103
|
+
} else if (vAlign == 'middle') {
|
|
104
|
+
alignItems = 'center';
|
|
105
|
+
} else if (vAlign == 'bottom') {
|
|
106
|
+
alignItems = 'end';
|
|
107
|
+
}
|
|
41
108
|
return {
|
|
42
|
-
groupErrorMessage: {
|
|
43
|
-
margin: '0.5rem 0'
|
|
44
|
-
},
|
|
45
109
|
formFieldLayout: {
|
|
46
110
|
label: 'formFieldLayout',
|
|
47
111
|
all: 'initial',
|
|
@@ -52,13 +116,48 @@ const generateStyle = (componentTheme, props) => {
|
|
|
52
116
|
direction: 'inherit',
|
|
53
117
|
textAlign: 'start',
|
|
54
118
|
opacity: 'inherit',
|
|
55
|
-
display: '
|
|
119
|
+
display: 'grid',
|
|
120
|
+
alignItems: alignItems,
|
|
121
|
+
verticalAlign: 'middle',
|
|
122
|
+
// removes margin in inline layouts
|
|
123
|
+
gridTemplateColumns: gridTemplateColumns,
|
|
124
|
+
gridTemplateAreas: gridTemplateAreas,
|
|
125
|
+
[`@media screen and (max-width: ${componentTheme.stackedOrInlineBreakpoint})`]: {
|
|
126
|
+
// for small screens use the stacked layout
|
|
127
|
+
gridTemplateColumns: '100%',
|
|
128
|
+
gridTemplateAreas: generateGridLayout(false, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages)
|
|
129
|
+
},
|
|
130
|
+
columnGap: '0.375rem',
|
|
56
131
|
width: '100%',
|
|
57
132
|
...(inline && {
|
|
58
|
-
display: 'inline-
|
|
59
|
-
verticalAlign: 'middle',
|
|
133
|
+
display: 'inline-grid',
|
|
60
134
|
width: 'auto'
|
|
61
135
|
})
|
|
136
|
+
},
|
|
137
|
+
formFieldLabel: {
|
|
138
|
+
label: 'formFieldLayout__label',
|
|
139
|
+
...(hasVisibleLabel && {
|
|
140
|
+
...labelStyles,
|
|
141
|
+
// NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
|
|
142
|
+
'&:is(label)': labelStyles,
|
|
143
|
+
'&:-webkit-any(label)': labelStyles
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
formFieldChildren: {
|
|
147
|
+
label: 'formFieldLayout__children',
|
|
148
|
+
gridArea: 'controls',
|
|
149
|
+
// add a small margin between the message and the controls
|
|
150
|
+
...(hasMessages && hasNewErrorMsgAndIsGroup && {
|
|
151
|
+
marginTop: '0.375rem'
|
|
152
|
+
}),
|
|
153
|
+
...(hasMessages && !hasNewErrorMsgAndIsGroup && {
|
|
154
|
+
marginBottom: '0.75rem'
|
|
155
|
+
}),
|
|
156
|
+
...(isInlineLayout && inline && {
|
|
157
|
+
[`@media screen and (min-width: ${componentTheme.stackedOrInlineBreakpoint})`]: {
|
|
158
|
+
justifySelf: 'start'
|
|
159
|
+
}
|
|
160
|
+
})
|
|
62
161
|
}
|
|
63
162
|
};
|
|
64
163
|
};
|
|
@@ -22,10 +22,36 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Generates the theme object for the component from the theme and provided additional information
|
|
27
|
+
* @param {Object} theme The actual theme object.
|
|
28
|
+
* @return {Object} The final theme object with the overrides and component variables
|
|
29
|
+
*/
|
|
25
30
|
const generateComponentTheme = theme => {
|
|
26
|
-
|
|
31
|
+
var _colors$contrasts;
|
|
32
|
+
const colors = theme.colors,
|
|
33
|
+
typography = theme.typography,
|
|
34
|
+
spacing = theme.spacing,
|
|
35
|
+
breakpoints = theme.breakpoints,
|
|
36
|
+
themeName = theme.key;
|
|
37
|
+
const themeSpecificStyle = {
|
|
38
|
+
canvas: {
|
|
39
|
+
color: theme['ic-brand-font-color-dark']
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const componentVariables = {
|
|
43
|
+
color: colors === null || colors === void 0 ? void 0 : (_colors$contrasts = colors.contrasts) === null || _colors$contrasts === void 0 ? void 0 : _colors$contrasts.grey125125,
|
|
44
|
+
fontFamily: typography === null || typography === void 0 ? void 0 : typography.fontFamily,
|
|
45
|
+
fontWeight: typography === null || typography === void 0 ? void 0 : typography.fontWeightBold,
|
|
46
|
+
fontSize: typography === null || typography === void 0 ? void 0 : typography.fontSizeMedium,
|
|
47
|
+
lineHeight: typography === null || typography === void 0 ? void 0 : typography.lineHeightFit,
|
|
48
|
+
inlinePadding: spacing === null || spacing === void 0 ? void 0 : spacing.xxSmall,
|
|
49
|
+
stackedOrInlineBreakpoint: breakpoints === null || breakpoints === void 0 ? void 0 : breakpoints.medium,
|
|
50
|
+
spacing: theme.spacing
|
|
51
|
+
};
|
|
27
52
|
return {
|
|
28
|
-
|
|
53
|
+
...componentVariables,
|
|
54
|
+
...themeSpecificStyle[themeName]
|
|
29
55
|
};
|
|
30
56
|
};
|
|
31
57
|
export default generateComponentTheme;
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
import PropTypes from 'prop-types';
|
|
26
26
|
import { FormPropTypes } from '../FormPropTypes';
|
|
27
27
|
const propTypes = {
|
|
28
|
-
messages: PropTypes.arrayOf(FormPropTypes.message)
|
|
28
|
+
messages: PropTypes.arrayOf(FormPropTypes.message),
|
|
29
|
+
gridArea: PropTypes.string
|
|
29
30
|
};
|
|
30
|
-
const allowedProps = ['messages'];
|
|
31
|
+
const allowedProps = ['messages', 'gridArea'];
|
|
31
32
|
export { propTypes, allowedProps };
|
|
@@ -29,16 +29,18 @@
|
|
|
29
29
|
* Generates the style object from the theme and provided additional information
|
|
30
30
|
* @param {Object} componentTheme The theme variable object.
|
|
31
31
|
* @param {Object} props the props of the component, the style is applied to
|
|
32
|
-
* @param {Object} state the state of the component, the style is applied to
|
|
33
32
|
* @return {Object} The final style object, which will be used in the component
|
|
34
33
|
*/
|
|
35
|
-
const generateStyle = componentTheme => {
|
|
34
|
+
const generateStyle = (componentTheme, props) => {
|
|
36
35
|
return {
|
|
37
36
|
formFieldMessages: {
|
|
38
37
|
label: 'formFieldMessages',
|
|
39
38
|
padding: 0,
|
|
40
39
|
display: 'block',
|
|
41
|
-
margin: `calc(
|
|
40
|
+
margin: `calc(-${componentTheme.topMargin}) 0 0 0`,
|
|
41
|
+
...(props.gridArea && {
|
|
42
|
+
gridArea: props.gridArea
|
|
43
|
+
})
|
|
42
44
|
},
|
|
43
45
|
message: {
|
|
44
46
|
label: 'formFieldMessages__message',
|
package/es/FormPropTypes.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
import PropTypes from 'prop-types';
|
|
26
26
|
const formMessageTypePropType = PropTypes.oneOf(['error', 'newError', 'hint', 'success', 'screenreader-only']);
|
|
27
27
|
const formMessageChildPropType = PropTypes.node;
|
|
28
|
+
|
|
29
|
+
// TODO it will be easier if this would be just a string
|
|
30
|
+
/**
|
|
31
|
+
* The text to display in the form message
|
|
32
|
+
*/
|
|
33
|
+
|
|
28
34
|
/**
|
|
29
35
|
* ---
|
|
30
36
|
* category: utilities/form
|
package/es/index.js
CHANGED
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
export { FormField } from './FormField';
|
|
26
|
-
export { FormFieldLabel } from './FormFieldLabel';
|
|
27
26
|
export { FormFieldMessage } from './FormFieldMessage';
|
|
28
27
|
export { FormFieldMessages } from './FormFieldMessages';
|
|
29
28
|
export { FormFieldLayout } from './FormFieldLayout';
|
package/lib/FormField/index.js
CHANGED
|
@@ -48,7 +48,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
48
48
|
description: "Please enter your full name"
|
|
49
49
|
}, /*#__PURE__*/_react.default.createElement("label", null, "First: ", /*#__PURE__*/_react.default.createElement("input", null)), /*#__PURE__*/_react.default.createElement("label", null, "Middle: ", /*#__PURE__*/_react.default.createElement("input", null)), /*#__PURE__*/_react.default.createElement("label", null, "Last: ", /*#__PURE__*/_react.default.createElement("input", null))))),
|
|
50
50
|
container = _render.container;
|
|
51
|
-
const formFieldGroup = container.querySelector("
|
|
51
|
+
const formFieldGroup = container.querySelector("span[class$='-formFieldLayout__label']");
|
|
52
52
|
const firstNameInput = _react2.screen.getByLabelText('First:');
|
|
53
53
|
const middleNameInput = _react2.screen.getByLabelText('Middle:');
|
|
54
54
|
const lastNameInput = _react2.screen.getByLabelText('Last:');
|
|
@@ -66,7 +66,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
66
66
|
description: "Please enter your full name"
|
|
67
67
|
}, children)),
|
|
68
68
|
container = _render2.container;
|
|
69
|
-
const formFieldGroup = container.querySelector(
|
|
69
|
+
const formFieldGroup = container.querySelector('label');
|
|
70
70
|
expect(formFieldGroup).toBeInTheDocument();
|
|
71
71
|
});
|
|
72
72
|
it('links the messages to the fieldset via aria-describedby', () => {
|
|
@@ -88,13 +88,13 @@ describe('<FormFieldGroup />', () => {
|
|
|
88
88
|
expect(message).toHaveTextContent('Invalid name');
|
|
89
89
|
expect(message).toHaveAttribute('id', messagesId);
|
|
90
90
|
});
|
|
91
|
-
it('displays description message inside the
|
|
91
|
+
it('displays description message inside the label', () => {
|
|
92
92
|
const description = 'Please enter your full name';
|
|
93
93
|
const _render4 = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_index.FormFieldGroup, {
|
|
94
94
|
description: description
|
|
95
95
|
}, _label5 || (_label5 = /*#__PURE__*/_react.default.createElement("label", null, "First: ", /*#__PURE__*/_react.default.createElement("input", null))), _label6 || (_label6 = /*#__PURE__*/_react.default.createElement("label", null, "Middle: ", /*#__PURE__*/_react.default.createElement("input", null))), _label7 || (_label7 = /*#__PURE__*/_react.default.createElement("label", null, "Last: ", /*#__PURE__*/_react.default.createElement("input", null))))),
|
|
96
96
|
container = _render4.container;
|
|
97
|
-
const legend = container.querySelector("
|
|
97
|
+
const legend = container.querySelector("span[class$='-formFieldLayout__label']");
|
|
98
98
|
expect(legend).toBeInTheDocument();
|
|
99
99
|
expect(legend).toHaveTextContent(description);
|
|
100
100
|
});
|