@instructure/ui-form-field 9.10.2 → 9.11.1
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 +19 -0
- package/es/FormField/index.js +5 -1
- package/es/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
- package/es/FormFieldGroup/index.js +18 -4
- package/es/FormFieldLabel/index.js +6 -6
- package/es/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +10 -8
- package/es/FormFieldLayout/index.js +79 -58
- package/es/FormFieldLayout/props.js +1 -6
- package/es/FormFieldLayout/styles.js +105 -9
- package/es/FormFieldLayout/theme.js +56 -0
- package/es/FormFieldMessages/props.js +3 -2
- package/es/FormFieldMessages/styles.js +5 -3
- package/es/FormPropTypes.js +6 -0
- package/lib/FormField/index.js +5 -1
- package/lib/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
- package/lib/FormFieldGroup/index.js +18 -4
- package/lib/FormFieldLabel/index.js +6 -5
- package/lib/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +9 -7
- package/lib/FormFieldLayout/index.js +77 -58
- package/lib/FormFieldLayout/props.js +1 -6
- package/lib/FormFieldLayout/styles.js +105 -9
- package/lib/FormFieldLayout/theme.js +62 -0
- package/lib/FormFieldMessages/props.js +3 -2
- package/lib/FormFieldMessages/styles.js +5 -3
- package/lib/FormPropTypes.js +6 -0
- package/package.json +15 -15
- package/src/FormField/README.md +31 -3
- package/src/FormField/index.tsx +3 -0
- package/src/FormFieldGroup/__new-tests__/FormFieldGroup.test.tsx +4 -6
- package/src/FormFieldGroup/index.tsx +41 -6
- package/src/FormFieldLabel/index.tsx +8 -3
- package/src/FormFieldLayout/__new-tests__/FormFieldLayout.test.tsx +6 -8
- package/src/FormFieldLayout/index.tsx +83 -100
- package/src/FormFieldLayout/props.ts +30 -7
- package/src/FormFieldLayout/styles.ts +124 -12
- package/src/FormFieldLayout/theme.ts +59 -0
- package/src/FormFieldMessages/props.ts +8 -2
- package/src/FormFieldMessages/styles.ts +5 -4
- package/src/FormPropTypes.ts +4 -0
- 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/FormFieldLabel/index.d.ts +2 -2
- package/types/FormFieldLabel/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 +10 -0
- package/types/FormFieldLayout/theme.d.ts.map +1 -0
- 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
|
@@ -35,16 +35,18 @@ exports.default = void 0;
|
|
|
35
35
|
* Generates the style object from the theme and provided additional information
|
|
36
36
|
* @param {Object} componentTheme The theme variable object.
|
|
37
37
|
* @param {Object} props the props of the component, the style is applied to
|
|
38
|
-
* @param {Object} state the state of the component, the style is applied to
|
|
39
38
|
* @return {Object} The final style object, which will be used in the component
|
|
40
39
|
*/
|
|
41
|
-
const generateStyle = componentTheme => {
|
|
40
|
+
const generateStyle = (componentTheme, props) => {
|
|
42
41
|
return {
|
|
43
42
|
formFieldMessages: {
|
|
44
43
|
label: 'formFieldMessages',
|
|
45
44
|
padding: 0,
|
|
46
45
|
display: 'block',
|
|
47
|
-
margin: `calc(
|
|
46
|
+
margin: `calc(-${componentTheme.topMargin}) 0 0 0`,
|
|
47
|
+
...(props.gridArea && {
|
|
48
|
+
gridArea: props.gridArea
|
|
49
|
+
})
|
|
48
50
|
},
|
|
49
51
|
message: {
|
|
50
52
|
label: 'formFieldMessages__message',
|
package/lib/FormPropTypes.js
CHANGED
|
@@ -32,6 +32,12 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
|
|
|
32
32
|
|
|
33
33
|
const formMessageTypePropType = exports.formMessageTypePropType = _propTypes.default.oneOf(['error', 'newError', 'hint', 'success', 'screenreader-only']);
|
|
34
34
|
const formMessageChildPropType = exports.formMessageChildPropType = _propTypes.default.node;
|
|
35
|
+
|
|
36
|
+
// TODO it will be easier if this would be just a string
|
|
37
|
+
/**
|
|
38
|
+
* The text to display in the form message
|
|
39
|
+
*/
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* ---
|
|
37
43
|
* category: utilities/form
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instructure/ui-form-field",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.11.1",
|
|
4
4
|
"description": "Form layout components.",
|
|
5
5
|
"author": "Instructure, Inc. Engineering and Product Design",
|
|
6
6
|
"module": "./es/index.js",
|
|
@@ -23,26 +23,26 @@
|
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@instructure/ui-axe-check": "9.
|
|
27
|
-
"@instructure/ui-babel-preset": "9.
|
|
28
|
-
"@instructure/ui-test-utils": "9.
|
|
29
|
-
"@instructure/ui-themes": "9.
|
|
26
|
+
"@instructure/ui-axe-check": "9.11.1",
|
|
27
|
+
"@instructure/ui-babel-preset": "9.11.1",
|
|
28
|
+
"@instructure/ui-test-utils": "9.11.1",
|
|
29
|
+
"@instructure/ui-themes": "9.11.1",
|
|
30
30
|
"@testing-library/jest-dom": "^6.4.6",
|
|
31
31
|
"@testing-library/react": "^15.0.7",
|
|
32
32
|
"vitest": "^2.0.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@babel/runtime": "^7.24.5",
|
|
36
|
-
"@instructure/console": "9.
|
|
37
|
-
"@instructure/emotion": "9.
|
|
38
|
-
"@instructure/shared-types": "9.
|
|
39
|
-
"@instructure/ui-a11y-content": "9.
|
|
40
|
-
"@instructure/ui-a11y-utils": "9.
|
|
41
|
-
"@instructure/ui-grid": "9.
|
|
42
|
-
"@instructure/ui-icons": "9.
|
|
43
|
-
"@instructure/ui-react-utils": "9.
|
|
44
|
-
"@instructure/ui-utils": "9.
|
|
45
|
-
"@instructure/uid": "9.
|
|
36
|
+
"@instructure/console": "9.11.1",
|
|
37
|
+
"@instructure/emotion": "9.11.1",
|
|
38
|
+
"@instructure/shared-types": "9.11.1",
|
|
39
|
+
"@instructure/ui-a11y-content": "9.11.1",
|
|
40
|
+
"@instructure/ui-a11y-utils": "9.11.1",
|
|
41
|
+
"@instructure/ui-grid": "9.11.1",
|
|
42
|
+
"@instructure/ui-icons": "9.11.1",
|
|
43
|
+
"@instructure/ui-react-utils": "9.11.1",
|
|
44
|
+
"@instructure/ui-utils": "9.11.1",
|
|
45
|
+
"@instructure/uid": "9.11.1",
|
|
46
46
|
"prop-types": "^15.8.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
package/src/FormField/README.md
CHANGED
|
@@ -9,7 +9,35 @@ components. In most cases it shouldn't be used directly.
|
|
|
9
9
|
---
|
|
10
10
|
type: example
|
|
11
11
|
---
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
12
|
+
<div>
|
|
13
|
+
<FormField id="_foo121" label="Stacked layout" width="400px" layout="stacked"
|
|
14
|
+
messages={[{type:'success', text: 'This is a success message'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
|
|
15
|
+
<TextInput id="_foo121"/>
|
|
16
|
+
</FormField>
|
|
17
|
+
test
|
|
18
|
+
<hr/>
|
|
19
|
+
<FormField id="_foo122" label="Stacked layout (inline=true)" width="400px" layout="stacked" inline
|
|
20
|
+
messages={[{type:'success', text: 'This is a success message'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
|
|
21
|
+
<TextInput id="_foo122"/>
|
|
22
|
+
</FormField>
|
|
23
|
+
test
|
|
24
|
+
<hr/>
|
|
25
|
+
<FormField id="_foo123" label="Inline layout" width="400px" layout="inline"
|
|
26
|
+
messages={[{type:'success', text: 'success!'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
|
|
27
|
+
<TextInput id="_foo123"/>
|
|
28
|
+
</FormField>
|
|
29
|
+
test
|
|
30
|
+
<hr/>
|
|
31
|
+
<FormField id="_foo124" label="Inline layout (inline=true)" width="400px" layout="inline" inline
|
|
32
|
+
messages={[{type:'success', text: 'success!'}, {type:'newError', text: 'An error message. It will wrap if the text is longer than the width of the container.'}]}>
|
|
33
|
+
<TextInput id="_foo124"/>
|
|
34
|
+
</FormField>
|
|
35
|
+
test
|
|
36
|
+
<hr/>
|
|
37
|
+
<FormField id="_foo121" label={<ScreenReaderContent>hidden text</ScreenReaderContent>} width="400px" layout="stacked">
|
|
38
|
+
<TextInput id="_foo121" />
|
|
39
|
+
</FormField>
|
|
40
|
+
test
|
|
41
|
+
<hr/>
|
|
42
|
+
</div>
|
|
15
43
|
```
|
package/src/FormField/index.tsx
CHANGED
|
@@ -68,6 +68,9 @@ class FormField extends Component<FormFieldProps> {
|
|
|
68
68
|
label={this.props.label}
|
|
69
69
|
vAlign={this.props.vAlign}
|
|
70
70
|
as="label"
|
|
71
|
+
// This makes the control in focus when the label is clicked
|
|
72
|
+
// This is needed to prevent the wrong element to be focused, e.g.
|
|
73
|
+
// multi selects Tag-s
|
|
71
74
|
htmlFor={this.props.id}
|
|
72
75
|
elementRef={this.handleRef}
|
|
73
76
|
/>
|
|
@@ -66,7 +66,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
const formFieldGroup = container.querySelector(
|
|
69
|
-
"
|
|
69
|
+
"span[class$='-formFieldLayout__label']"
|
|
70
70
|
)
|
|
71
71
|
const firstNameInput = screen.getByLabelText('First:')
|
|
72
72
|
const middleNameInput = screen.getByLabelText('Middle:')
|
|
@@ -94,9 +94,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
94
94
|
</FormFieldGroup>
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
const formFieldGroup = container.querySelector(
|
|
98
|
-
"fieldset[class$='-formFieldLayout']"
|
|
99
|
-
)
|
|
97
|
+
const formFieldGroup = container.querySelector('label')
|
|
100
98
|
|
|
101
99
|
expect(formFieldGroup).toBeInTheDocument()
|
|
102
100
|
})
|
|
@@ -136,7 +134,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
136
134
|
expect(message).toHaveAttribute('id', messagesId)
|
|
137
135
|
})
|
|
138
136
|
|
|
139
|
-
it('displays description message inside the
|
|
137
|
+
it('displays description message inside the label', () => {
|
|
140
138
|
const description = 'Please enter your full name'
|
|
141
139
|
|
|
142
140
|
const { container } = render(
|
|
@@ -154,7 +152,7 @@ describe('<FormFieldGroup />', () => {
|
|
|
154
152
|
)
|
|
155
153
|
|
|
156
154
|
const legend = container.querySelector(
|
|
157
|
-
"
|
|
155
|
+
"span[class$='-formFieldLayout__label']"
|
|
158
156
|
)
|
|
159
157
|
|
|
160
158
|
expect(legend).toBeInTheDocument()
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/** @jsx jsx */
|
|
26
|
-
import { Component, Children, ReactElement } from 'react'
|
|
26
|
+
import { Component, Children, ReactElement, AriaAttributes } from 'react'
|
|
27
27
|
|
|
28
28
|
import { Grid } from '@instructure/ui-grid'
|
|
29
29
|
import { pickProps, omitProps } from '@instructure/ui-react-utils'
|
|
@@ -53,7 +53,8 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
|
|
|
53
53
|
disabled: false,
|
|
54
54
|
rowSpacing: 'medium',
|
|
55
55
|
colSpacing: 'small',
|
|
56
|
-
vAlign: 'middle'
|
|
56
|
+
vAlign: 'middle',
|
|
57
|
+
isGroup: true
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
ref: Element | null = null
|
|
@@ -77,14 +78,20 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
get makeStylesVariables(): FormFieldGroupStyleProps {
|
|
80
|
-
|
|
81
|
+
// new form errors dont need borders
|
|
82
|
+
const oldInvalid =
|
|
83
|
+
!!this.props.messages &&
|
|
84
|
+
this.props.messages.findIndex((message) => {
|
|
85
|
+
return message.type === 'error'
|
|
86
|
+
}) >= 0
|
|
87
|
+
return { invalid: oldInvalid }
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
get invalid() {
|
|
84
91
|
return (
|
|
85
92
|
!!this.props.messages &&
|
|
86
93
|
this.props.messages.findIndex((message) => {
|
|
87
|
-
return message.type === 'error'
|
|
94
|
+
return message.type === 'error' || message.type === 'newError'
|
|
88
95
|
}) >= 0
|
|
89
96
|
)
|
|
90
97
|
}
|
|
@@ -134,7 +141,35 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
|
|
|
134
141
|
|
|
135
142
|
render() {
|
|
136
143
|
const { styles, makeStyles, isGroup, ...props } = this.props
|
|
137
|
-
|
|
144
|
+
// This is quite ugly, but according to ARIA spec the `aria-invalid` prop
|
|
145
|
+
// can only be used with certain roles see
|
|
146
|
+
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-invalid#associated_roles
|
|
147
|
+
// `aria-invalid` is put on in FormFieldLayout because the error message
|
|
148
|
+
// DOM part gets there its ID.
|
|
149
|
+
let ariaInvalid: AriaAttributes['aria-invalid'] = undefined
|
|
150
|
+
if (
|
|
151
|
+
this.props.role &&
|
|
152
|
+
this.invalid &&
|
|
153
|
+
[
|
|
154
|
+
'application',
|
|
155
|
+
'checkbox',
|
|
156
|
+
'combobox',
|
|
157
|
+
'gridcell',
|
|
158
|
+
'listbox',
|
|
159
|
+
'radiogroup',
|
|
160
|
+
'slider',
|
|
161
|
+
'spinbutton',
|
|
162
|
+
'textbox',
|
|
163
|
+
'tree',
|
|
164
|
+
'columnheader',
|
|
165
|
+
'rowheader',
|
|
166
|
+
'searchbox',
|
|
167
|
+
'switch',
|
|
168
|
+
'treegrid'
|
|
169
|
+
].includes(this.props.role)
|
|
170
|
+
) {
|
|
171
|
+
ariaInvalid = 'true'
|
|
172
|
+
}
|
|
138
173
|
return (
|
|
139
174
|
<FormFieldLayout
|
|
140
175
|
{...omitProps(props, FormFieldGroup.allowedProps)}
|
|
@@ -143,7 +178,7 @@ class FormFieldGroup extends Component<FormFieldGroupProps> {
|
|
|
143
178
|
layout={props.layout === 'inline' ? 'inline' : 'stacked'}
|
|
144
179
|
label={props.description}
|
|
145
180
|
aria-disabled={props.disabled ? 'true' : undefined}
|
|
146
|
-
aria-invalid={
|
|
181
|
+
aria-invalid={ariaInvalid}
|
|
147
182
|
elementRef={this.handleRef}
|
|
148
183
|
isGroup={isGroup}
|
|
149
184
|
>
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
/** @jsx jsx */
|
|
26
26
|
import { Component } from 'react'
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
omitProps,
|
|
30
|
+
getElementType,
|
|
31
|
+
deprecated
|
|
32
|
+
} from '@instructure/ui-react-utils'
|
|
29
33
|
import { withStyle, jsx } from '@instructure/emotion'
|
|
30
34
|
|
|
31
35
|
import generateStyle from './styles'
|
|
@@ -39,8 +43,7 @@ import type { FormFieldLabelProps } from './props'
|
|
|
39
43
|
parent: FormField
|
|
40
44
|
---
|
|
41
45
|
|
|
42
|
-
This is a helper component that is used by most of the custom form
|
|
43
|
-
components. In most cases it shouldn't be used directly.
|
|
46
|
+
This is a helper component that is used by most of the custom form components. In most cases it shouldn't be used directly.
|
|
44
47
|
|
|
45
48
|
```js
|
|
46
49
|
---
|
|
@@ -49,8 +52,10 @@ type: example
|
|
|
49
52
|
<FormFieldLabel>Hello</FormFieldLabel>
|
|
50
53
|
```
|
|
51
54
|
|
|
55
|
+
@deprecated This is an internal component that will be removed in the future
|
|
52
56
|
**/
|
|
53
57
|
@withStyle(generateStyle, generateComponentTheme)
|
|
58
|
+
@deprecated('10', null, 'This component will be removed in a future version')
|
|
54
59
|
class FormFieldLabel extends Component<FormFieldLabelProps> {
|
|
55
60
|
static readonly componentId = 'FormFieldLabel'
|
|
56
61
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import React from 'react'
|
|
26
|
-
import { render
|
|
26
|
+
import { render } from '@testing-library/react'
|
|
27
27
|
import { vi } from 'vitest'
|
|
28
28
|
import { runAxeCheck } from '@instructure/ui-axe-check'
|
|
29
29
|
import '@testing-library/jest-dom'
|
|
@@ -56,7 +56,7 @@ describe('<FormFieldLayout />', () => {
|
|
|
56
56
|
"label[class$='-formFieldLayout']"
|
|
57
57
|
)
|
|
58
58
|
const formFieldLabel = container.querySelector(
|
|
59
|
-
"span[class$='-
|
|
59
|
+
"span[class$='-formFieldLayout__label']"
|
|
60
60
|
)
|
|
61
61
|
|
|
62
62
|
expect(formFieldLayout).toBeInTheDocument()
|
|
@@ -74,15 +74,13 @@ describe('<FormFieldLayout />', () => {
|
|
|
74
74
|
|
|
75
75
|
it('should provide a ref to the input container', () => {
|
|
76
76
|
const inputContainerRef = vi.fn()
|
|
77
|
-
|
|
77
|
+
const ref = React.createRef<HTMLInputElement>()
|
|
78
78
|
render(
|
|
79
79
|
<FormFieldLayout label="Username" inputContainerRef={inputContainerRef}>
|
|
80
|
-
<input type="text" />
|
|
80
|
+
<input type="text" ref={ref} />
|
|
81
81
|
</FormFieldLayout>
|
|
82
82
|
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expect(inputContainerRef).toHaveBeenCalledWith(input.parentElement)
|
|
83
|
+
expect(ref.current).toBeInstanceOf(HTMLInputElement)
|
|
84
|
+
expect(inputContainerRef).toHaveBeenCalledWith(ref.current!.parentElement)
|
|
87
85
|
})
|
|
88
86
|
})
|
|
@@ -24,27 +24,19 @@
|
|
|
24
24
|
|
|
25
25
|
/** @jsx jsx */
|
|
26
26
|
import { Component } from 'react'
|
|
27
|
-
|
|
28
27
|
import { hasVisibleChildren } from '@instructure/ui-a11y-utils'
|
|
29
|
-
import { ScreenReaderContent } from '@instructure/ui-a11y-content'
|
|
30
|
-
import { Grid } from '@instructure/ui-grid'
|
|
31
|
-
import { logError as error } from '@instructure/console'
|
|
32
28
|
import {
|
|
33
29
|
omitProps,
|
|
34
|
-
pickProps,
|
|
35
30
|
getElementType,
|
|
36
31
|
withDeterministicId
|
|
37
32
|
} from '@instructure/ui-react-utils'
|
|
38
33
|
|
|
39
34
|
import { withStyle, jsx } from '@instructure/emotion'
|
|
40
|
-
|
|
41
|
-
import { FormFieldLabel } from '../FormFieldLabel'
|
|
42
35
|
import { FormFieldMessages } from '../FormFieldMessages'
|
|
43
|
-
|
|
44
36
|
import generateStyle from './styles'
|
|
45
|
-
|
|
46
|
-
import { propTypes, allowedProps } from './props'
|
|
37
|
+
import { propTypes, allowedProps, FormFieldStyleProps } from './props'
|
|
47
38
|
import type { FormFieldLayoutProps } from './props'
|
|
39
|
+
import generateComponentTheme from './theme'
|
|
48
40
|
|
|
49
41
|
/**
|
|
50
42
|
---
|
|
@@ -52,7 +44,7 @@ parent: FormField
|
|
|
52
44
|
---
|
|
53
45
|
**/
|
|
54
46
|
@withDeterministicId()
|
|
55
|
-
@withStyle(generateStyle,
|
|
47
|
+
@withStyle(generateStyle, generateComponentTheme)
|
|
56
48
|
class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
57
49
|
static readonly componentId = 'FormFieldLayout'
|
|
58
50
|
|
|
@@ -67,19 +59,12 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
67
59
|
|
|
68
60
|
constructor(props: FormFieldLayoutProps) {
|
|
69
61
|
super(props)
|
|
70
|
-
|
|
71
62
|
this._messagesId = props.messagesId || props.deterministicId!()
|
|
72
|
-
|
|
73
|
-
error(
|
|
74
|
-
typeof props.width !== 'undefined' ||
|
|
75
|
-
!props.inline ||
|
|
76
|
-
props.layout !== 'inline',
|
|
77
|
-
`[FormFieldLayout] The 'inline' prop is true, and the 'layout' is set to 'inline'.
|
|
78
|
-
This will cause a layout issue in Internet Explorer 11 unless you also add a value for the 'width' prop.`
|
|
79
|
-
)
|
|
63
|
+
this._labelId = props.deterministicId!('FormField-Label')
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
private _messagesId: string
|
|
67
|
+
private _labelId: string
|
|
83
68
|
|
|
84
69
|
ref: Element | null = null
|
|
85
70
|
|
|
@@ -94,31 +79,51 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
94
79
|
}
|
|
95
80
|
|
|
96
81
|
componentDidMount() {
|
|
97
|
-
this.props.makeStyles?.()
|
|
82
|
+
this.props.makeStyles?.(this.makeStyleProps())
|
|
98
83
|
}
|
|
99
84
|
|
|
100
85
|
componentDidUpdate() {
|
|
101
|
-
this.props.makeStyles?.()
|
|
86
|
+
this.props.makeStyles?.(this.makeStyleProps())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
makeStyleProps = (): FormFieldStyleProps => {
|
|
90
|
+
const hasNewErrorMsgAndIsGroup =
|
|
91
|
+
!!this.props.messages?.find((m) => m.type === 'newError') &&
|
|
92
|
+
!!this.props.isGroup
|
|
93
|
+
return {
|
|
94
|
+
hasMessages: this.hasMessages,
|
|
95
|
+
hasVisibleLabel: this.hasVisibleLabel,
|
|
96
|
+
// if true render error message above the controls (and below the label)
|
|
97
|
+
hasNewErrorMsgAndIsGroup: hasNewErrorMsgAndIsGroup
|
|
98
|
+
}
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
get hasVisibleLabel() {
|
|
105
|
-
return this.props.label
|
|
102
|
+
return this.props.label ? hasVisibleChildren(this.props.label) : false
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
get hasMessages() {
|
|
109
|
-
|
|
106
|
+
if (!this.props.messages || this.props.messages.length == 0) {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
for (const msg of this.props.messages) {
|
|
110
|
+
if (msg.text) {
|
|
111
|
+
if (typeof msg.text === 'string') {
|
|
112
|
+
return msg.text.length > 0
|
|
113
|
+
}
|
|
114
|
+
// this is more complicated (e.g. an array, a React component,...)
|
|
115
|
+
// but we don't try to optimize here for these cases
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
get elementType() {
|
|
113
123
|
return getElementType(FormFieldLayout, this.props)
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
// Return if both the component container and label will display inline
|
|
118
|
-
return this.props.inline && this.props.layout === 'inline'
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
handleInputContainerRef = (node: HTMLSpanElement | null) => {
|
|
126
|
+
handleInputContainerRef = (node: HTMLElement | null) => {
|
|
122
127
|
if (typeof this.props.inputContainerRef === 'function') {
|
|
123
128
|
this.props.inputContainerRef(node)
|
|
124
129
|
}
|
|
@@ -126,99 +131,77 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
126
131
|
|
|
127
132
|
renderLabel() {
|
|
128
133
|
if (this.hasVisibleLabel) {
|
|
134
|
+
if (this.elementType == 'fieldset') {
|
|
135
|
+
// `legend` has some special built in CSS, this can only be reset
|
|
136
|
+
// this way https://stackoverflow.com/a/65866981/319473
|
|
137
|
+
return (
|
|
138
|
+
<legend style={{ display: 'contents' }}>
|
|
139
|
+
<span css={this.props.styles?.formFieldLabel}>
|
|
140
|
+
{this.props.label}
|
|
141
|
+
</span>
|
|
142
|
+
</legend>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
129
145
|
return (
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
>
|
|
146
|
+
<span css={this.props.styles?.formFieldLabel}>{this.props.label}</span>
|
|
147
|
+
)
|
|
148
|
+
} else if (this.props.label) {
|
|
149
|
+
if (this.elementType == 'fieldset') {
|
|
150
|
+
return (
|
|
151
|
+
<legend id={this._labelId} style={{ display: 'contents' }}>
|
|
137
152
|
{this.props.label}
|
|
138
|
-
</
|
|
139
|
-
|
|
153
|
+
</legend>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
// needs to be wrapped because it needs an `id`
|
|
157
|
+
return (
|
|
158
|
+
<div id={this._labelId} style={{ display: 'contents' }}>
|
|
159
|
+
{this.props.label}
|
|
160
|
+
</div>
|
|
140
161
|
)
|
|
141
|
-
} else
|
|
142
|
-
// to avoid duplicate label/legend content
|
|
143
|
-
return this.props.label
|
|
144
|
-
} else {
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
renderLegend() {
|
|
150
|
-
// note: the legend element must be the first child of a fieldset element for SR
|
|
151
|
-
// so we render it twice in that case (once for SR-only and one that is visible)
|
|
152
|
-
return (
|
|
153
|
-
<ScreenReaderContent as="legend">
|
|
154
|
-
{this.props.label}
|
|
155
|
-
{this.hasMessages && (
|
|
156
|
-
<FormFieldMessages messages={this.props.messages} />
|
|
157
|
-
)}
|
|
158
|
-
</ScreenReaderContent>
|
|
159
|
-
)
|
|
162
|
+
} else return null
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
renderVisibleMessages() {
|
|
163
166
|
return this.hasMessages ? (
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<FormFieldMessages
|
|
170
|
-
id={this._messagesId}
|
|
171
|
-
messages={this.props.messages}
|
|
172
|
-
/>
|
|
173
|
-
</Grid.Col>
|
|
174
|
-
</Grid.Row>
|
|
167
|
+
<FormFieldMessages
|
|
168
|
+
id={this._messagesId}
|
|
169
|
+
messages={this.props.messages}
|
|
170
|
+
gridArea="messages"
|
|
171
|
+
/>
|
|
175
172
|
) : null
|
|
176
173
|
}
|
|
177
174
|
|
|
178
175
|
render() {
|
|
179
|
-
//
|
|
180
|
-
const ElementType = this.elementType
|
|
176
|
+
// Should be `<label>` if it's a FormField, fieldset if it's a group
|
|
177
|
+
const ElementType = this.elementType
|
|
181
178
|
|
|
182
179
|
const { makeStyles, styles, messages, isGroup, ...props } = this.props
|
|
183
180
|
|
|
184
|
-
const { width,
|
|
181
|
+
const { width, children } = props
|
|
185
182
|
|
|
186
|
-
const
|
|
183
|
+
const hasNewErrorMsgAndIsGroup =
|
|
187
184
|
!!messages?.find((m) => m.type === 'newError') && isGroup
|
|
188
185
|
return (
|
|
189
186
|
<ElementType
|
|
190
|
-
{...omitProps(props, [
|
|
191
|
-
...FormFieldLayout.allowedProps,
|
|
192
|
-
...Grid.allowedProps
|
|
193
|
-
])}
|
|
187
|
+
{...omitProps(props, [...FormFieldLayout.allowedProps])}
|
|
194
188
|
css={styles?.formFieldLayout}
|
|
195
|
-
style={{ width }}
|
|
196
189
|
aria-describedby={this.hasMessages ? this._messagesId : undefined}
|
|
190
|
+
aria-errormessage={
|
|
191
|
+
this.props['aria-invalid'] ? this._messagesId : undefined
|
|
192
|
+
}
|
|
193
|
+
style={{ width }}
|
|
197
194
|
ref={this.handleRef}
|
|
198
195
|
>
|
|
199
|
-
{this.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
layout === 'inline' && this.hasVisibleLabel ? 'medium' : null
|
|
205
|
-
}
|
|
206
|
-
{...pickProps(props, Grid.allowedProps)}
|
|
196
|
+
{this.renderLabel()}
|
|
197
|
+
{hasNewErrorMsgAndIsGroup && this.renderVisibleMessages()}
|
|
198
|
+
<span
|
|
199
|
+
css={styles?.formFieldChildren}
|
|
200
|
+
ref={this.handleInputContainerRef}
|
|
207
201
|
>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
width={this.inlineContainerAndLabel ? 'auto' : undefined}
|
|
212
|
-
elementRef={this.handleInputContainerRef}
|
|
213
|
-
>
|
|
214
|
-
{hasNewErrorMsg && (
|
|
215
|
-
<div css={styles?.groupErrorMessage}>{this.renderVisibleMessages()}</div>
|
|
216
|
-
)}
|
|
217
|
-
{children}
|
|
218
|
-
</Grid.Col>
|
|
219
|
-
</Grid.Row>
|
|
220
|
-
{!hasNewErrorMsg && this.renderVisibleMessages()}
|
|
221
|
-
</Grid>
|
|
202
|
+
{children}
|
|
203
|
+
</span>
|
|
204
|
+
{!hasNewErrorMsgAndIsGroup && this.renderVisibleMessages()}
|
|
222
205
|
</ElementType>
|
|
223
206
|
)
|
|
224
207
|
}
|
|
@@ -57,12 +57,31 @@ type FormFieldLayoutOwnProps = {
|
|
|
57
57
|
*/
|
|
58
58
|
messagesId?: string
|
|
59
59
|
children?: React.ReactNode
|
|
60
|
+
/**
|
|
61
|
+
* If `true` use an inline layout -- content will flow on the left/right side
|
|
62
|
+
* of this component
|
|
63
|
+
*/
|
|
60
64
|
inline?: boolean
|
|
65
|
+
/**
|
|
66
|
+
* In `stacked` mode the container is below the label, in `inline` mode the
|
|
67
|
+
* container is to the right/left (depending on text direction)
|
|
68
|
+
*/
|
|
61
69
|
layout?: 'stacked' | 'inline'
|
|
70
|
+
/**
|
|
71
|
+
* The horizontal alignment of the label. Only works in `inline` layout
|
|
72
|
+
*/
|
|
62
73
|
labelAlign?: 'start' | 'end'
|
|
74
|
+
/**
|
|
75
|
+
* The vertical alignment of the label and the controls.
|
|
76
|
+
* "top" by default
|
|
77
|
+
*/
|
|
63
78
|
vAlign?: 'top' | 'middle' | 'bottom'
|
|
64
79
|
width?: string
|
|
65
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Provides a reference to the container that holds the input element
|
|
82
|
+
* @param element The element that holds the input control as its children
|
|
83
|
+
*/
|
|
84
|
+
inputContainerRef?: (element: HTMLElement | null) => void
|
|
66
85
|
/**
|
|
67
86
|
* provides a reference to the underlying html root element
|
|
68
87
|
*/
|
|
@@ -80,7 +99,7 @@ type FormFieldLayoutProps = FormFieldLayoutOwnProps &
|
|
|
80
99
|
WithDeterministicIdProps
|
|
81
100
|
|
|
82
101
|
type FormFieldLayoutStyle = ComponentStyle<
|
|
83
|
-
'formFieldLayout' | '
|
|
102
|
+
'formFieldLayout' | 'formFieldLabel' | 'formFieldChildren'
|
|
84
103
|
>
|
|
85
104
|
|
|
86
105
|
const propTypes: PropValidators<PropKeys> = {
|
|
@@ -112,14 +131,18 @@ const allowedProps: AllowedPropKeys = [
|
|
|
112
131
|
'labelAlign',
|
|
113
132
|
'width',
|
|
114
133
|
'inputContainerRef',
|
|
115
|
-
'elementRef'
|
|
116
|
-
|
|
117
|
-
// added vAlign because FormField and FormFieldGroup passes it, but not adding
|
|
118
|
-
// it to allowedProps to prevent it from getting passed through accidentally
|
|
119
|
-
//'vAlign'
|
|
134
|
+
'elementRef',
|
|
135
|
+
'vAlign'
|
|
120
136
|
]
|
|
121
137
|
|
|
138
|
+
type FormFieldStyleProps = {
|
|
139
|
+
hasMessages: boolean
|
|
140
|
+
hasVisibleLabel: boolean
|
|
141
|
+
hasNewErrorMsgAndIsGroup: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
export type {
|
|
145
|
+
FormFieldStyleProps,
|
|
123
146
|
FormFieldLayoutProps,
|
|
124
147
|
FormFieldLayoutStyle,
|
|
125
148
|
FormFieldLayoutOwnProps
|