@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
|
@@ -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
|
>
|
|
@@ -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,25 +24,17 @@
|
|
|
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
28
|
import {
|
|
32
29
|
omitProps,
|
|
33
|
-
pickProps,
|
|
34
30
|
getElementType,
|
|
35
31
|
withDeterministicId
|
|
36
32
|
} from '@instructure/ui-react-utils'
|
|
37
33
|
|
|
38
34
|
import { withStyle, jsx } from '@instructure/emotion'
|
|
39
|
-
|
|
40
|
-
import { FormFieldLabel } from '../FormFieldLabel'
|
|
41
35
|
import { FormFieldMessages } from '../FormFieldMessages'
|
|
42
|
-
|
|
43
36
|
import generateStyle from './styles'
|
|
44
|
-
|
|
45
|
-
import { propTypes, allowedProps } from './props'
|
|
37
|
+
import { propTypes, allowedProps, FormFieldStyleProps } from './props'
|
|
46
38
|
import type { FormFieldLayoutProps } from './props'
|
|
47
39
|
import generateComponentTheme from './theme'
|
|
48
40
|
|
|
@@ -68,9 +60,11 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
68
60
|
constructor(props: FormFieldLayoutProps) {
|
|
69
61
|
super(props)
|
|
70
62
|
this._messagesId = props.messagesId || props.deterministicId!()
|
|
63
|
+
this._labelId = props.deterministicId!('FormField-Label')
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
private _messagesId: string
|
|
67
|
+
private _labelId: string
|
|
74
68
|
|
|
75
69
|
ref: Element | null = null
|
|
76
70
|
|
|
@@ -85,31 +79,51 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
componentDidMount() {
|
|
88
|
-
this.props.makeStyles?.()
|
|
82
|
+
this.props.makeStyles?.(this.makeStyleProps())
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
componentDidUpdate() {
|
|
92
|
-
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
|
+
}
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
get hasVisibleLabel() {
|
|
96
|
-
return this.props.label
|
|
102
|
+
return this.props.label ? hasVisibleChildren(this.props.label) : false
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
get hasMessages() {
|
|
100
|
-
|
|
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
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
get elementType() {
|
|
104
123
|
return getElementType(FormFieldLayout, this.props)
|
|
105
124
|
}
|
|
106
125
|
|
|
107
|
-
|
|
108
|
-
// Return if both the component container and label will display inline
|
|
109
|
-
return this.props.inline && this.props.layout === 'inline'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
handleInputContainerRef = (node: HTMLSpanElement | null) => {
|
|
126
|
+
handleInputContainerRef = (node: HTMLElement | null) => {
|
|
113
127
|
if (typeof this.props.inputContainerRef === 'function') {
|
|
114
128
|
this.props.inputContainerRef(node)
|
|
115
129
|
}
|
|
@@ -117,101 +131,73 @@ class FormFieldLayout extends Component<FormFieldLayoutProps> {
|
|
|
117
131
|
|
|
118
132
|
renderLabel() {
|
|
119
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
|
+
}
|
|
120
145
|
return (
|
|
121
|
-
<
|
|
122
|
-
textAlign={this.props.labelAlign}
|
|
123
|
-
width={this.inlineContainerAndLabel ? 'auto' : 3}
|
|
124
|
-
>
|
|
125
|
-
<FormFieldLabel
|
|
126
|
-
aria-hidden={this.elementType === 'fieldset' ? 'true' : undefined}
|
|
127
|
-
>
|
|
128
|
-
{this.props.label}
|
|
129
|
-
</FormFieldLabel>
|
|
130
|
-
</Grid.Col>
|
|
146
|
+
<span css={this.props.styles?.formFieldLabel}>{this.props.label}</span>
|
|
131
147
|
)
|
|
132
|
-
} else if (this.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<ScreenReaderContent as="legend">
|
|
145
|
-
{this.props.label}
|
|
146
|
-
{this.hasMessages && (
|
|
147
|
-
<FormFieldMessages messages={this.props.messages} />
|
|
148
|
-
)}
|
|
149
|
-
</ScreenReaderContent>
|
|
150
|
-
)
|
|
148
|
+
} else if (this.props.label) {
|
|
149
|
+
if (this.elementType == 'fieldset') {
|
|
150
|
+
return (
|
|
151
|
+
<legend id={this._labelId} style={{ display: 'contents' }}>
|
|
152
|
+
{this.props.label}
|
|
153
|
+
</legend>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
// needs to be wrapped because it needs an `id`
|
|
157
|
+
return <div id={this._labelId}>{this.props.label}</div>
|
|
158
|
+
} else return null
|
|
151
159
|
}
|
|
152
160
|
|
|
153
161
|
renderVisibleMessages() {
|
|
154
162
|
return this.hasMessages ? (
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
<FormFieldMessages
|
|
161
|
-
id={this._messagesId}
|
|
162
|
-
messages={this.props.messages}
|
|
163
|
-
/>
|
|
164
|
-
</Grid.Col>
|
|
165
|
-
</Grid.Row>
|
|
163
|
+
<FormFieldMessages
|
|
164
|
+
id={this._messagesId}
|
|
165
|
+
messages={this.props.messages}
|
|
166
|
+
gridArea="messages"
|
|
167
|
+
/>
|
|
166
168
|
) : null
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
render() {
|
|
170
|
-
//
|
|
171
|
-
const ElementType = this.elementType
|
|
172
|
+
// Should be `<label>` if it's a FormField, fieldset if it's a group
|
|
173
|
+
const ElementType = this.elementType
|
|
172
174
|
|
|
173
175
|
const { makeStyles, styles, messages, isGroup, ...props } = this.props
|
|
174
176
|
|
|
175
|
-
const { width,
|
|
177
|
+
const { width, children } = props
|
|
176
178
|
|
|
177
|
-
const
|
|
179
|
+
const hasNewErrorMsgAndIsGroup =
|
|
178
180
|
!!messages?.find((m) => m.type === 'newError') && isGroup
|
|
179
181
|
return (
|
|
180
182
|
<ElementType
|
|
181
|
-
{...omitProps(props, [
|
|
182
|
-
...FormFieldLayout.allowedProps,
|
|
183
|
-
...Grid.allowedProps
|
|
184
|
-
])}
|
|
183
|
+
{...omitProps(props, [...FormFieldLayout.allowedProps])}
|
|
185
184
|
css={styles?.formFieldLayout}
|
|
186
|
-
style={{ width }}
|
|
187
185
|
aria-describedby={this.hasMessages ? this._messagesId : undefined}
|
|
186
|
+
aria-errormessage={
|
|
187
|
+
this.props['aria-invalid'] ? this._messagesId : undefined
|
|
188
|
+
}
|
|
189
|
+
style={{ width }}
|
|
188
190
|
ref={this.handleRef}
|
|
189
191
|
>
|
|
190
|
-
{this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
layout === 'inline' && this.hasVisibleLabel ? 'medium' : null
|
|
196
|
-
}
|
|
197
|
-
{...pickProps(props, Grid.allowedProps)}
|
|
192
|
+
{this.renderLabel()}
|
|
193
|
+
{hasNewErrorMsgAndIsGroup && this.renderVisibleMessages()}
|
|
194
|
+
<span
|
|
195
|
+
css={styles?.formFieldChildren}
|
|
196
|
+
ref={this.handleInputContainerRef}
|
|
198
197
|
>
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
width={this.inlineContainerAndLabel ? 'auto' : undefined}
|
|
203
|
-
elementRef={this.handleInputContainerRef}
|
|
204
|
-
>
|
|
205
|
-
{hasNewErrorMsg && (
|
|
206
|
-
<div css={styles?.groupErrorMessage}>
|
|
207
|
-
{this.renderVisibleMessages()}
|
|
208
|
-
</div>
|
|
209
|
-
)}
|
|
210
|
-
{children}
|
|
211
|
-
</Grid.Col>
|
|
212
|
-
</Grid.Row>
|
|
213
|
-
{!hasNewErrorMsg && this.renderVisibleMessages()}
|
|
214
|
-
</Grid>
|
|
198
|
+
{children}
|
|
199
|
+
</span>
|
|
200
|
+
{!hasNewErrorMsgAndIsGroup && this.renderVisibleMessages()}
|
|
215
201
|
</ElementType>
|
|
216
202
|
)
|
|
217
203
|
}
|
|
@@ -61,12 +61,31 @@ type FormFieldLayoutOwnProps = {
|
|
|
61
61
|
*/
|
|
62
62
|
messagesId?: string
|
|
63
63
|
children?: React.ReactNode
|
|
64
|
+
/**
|
|
65
|
+
* If `true` use an inline layout -- content will flow on the left/right side
|
|
66
|
+
* of this component
|
|
67
|
+
*/
|
|
64
68
|
inline?: boolean
|
|
69
|
+
/**
|
|
70
|
+
* In `stacked` mode the container is below the label, in `inline` mode the
|
|
71
|
+
* container is to the right/left (depending on text direction)
|
|
72
|
+
*/
|
|
65
73
|
layout?: 'stacked' | 'inline'
|
|
74
|
+
/**
|
|
75
|
+
* The horizontal alignment of the label. Only works in `inline` layout
|
|
76
|
+
*/
|
|
66
77
|
labelAlign?: 'start' | 'end'
|
|
78
|
+
/**
|
|
79
|
+
* The vertical alignment of the label and the controls.
|
|
80
|
+
* "top" by default
|
|
81
|
+
*/
|
|
67
82
|
vAlign?: 'top' | 'middle' | 'bottom'
|
|
68
83
|
width?: string
|
|
69
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Provides a reference to the container that holds the input element
|
|
86
|
+
* @param element The element that holds the input control as its children
|
|
87
|
+
*/
|
|
88
|
+
inputContainerRef?: (element: HTMLElement | null) => void
|
|
70
89
|
/**
|
|
71
90
|
* provides a reference to the underlying html root element
|
|
72
91
|
*/
|
|
@@ -89,7 +108,7 @@ type FormFieldLayoutProps = FormFieldLayoutOwnProps &
|
|
|
89
108
|
WithDeterministicIdProps
|
|
90
109
|
|
|
91
110
|
type FormFieldLayoutStyle = ComponentStyle<
|
|
92
|
-
'formFieldLayout' | '
|
|
111
|
+
'formFieldLayout' | 'formFieldLabel' | 'formFieldChildren'
|
|
93
112
|
>
|
|
94
113
|
|
|
95
114
|
const propTypes: PropValidators<PropKeys> = {
|
|
@@ -130,7 +149,14 @@ const allowedProps: AllowedPropKeys = [
|
|
|
130
149
|
//'vAlign'
|
|
131
150
|
]
|
|
132
151
|
|
|
152
|
+
type FormFieldStyleProps = {
|
|
153
|
+
hasMessages: boolean
|
|
154
|
+
hasVisibleLabel: boolean
|
|
155
|
+
hasNewErrorMsgAndIsGroup: boolean
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
export type {
|
|
159
|
+
FormFieldStyleProps,
|
|
134
160
|
FormFieldLayoutProps,
|
|
135
161
|
FormFieldLayoutStyle,
|
|
136
162
|
FormFieldLayoutOwnProps
|
|
@@ -22,9 +22,44 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import type {
|
|
26
|
+
FormFieldLayoutProps,
|
|
27
|
+
FormFieldLayoutStyle,
|
|
28
|
+
FormFieldStyleProps
|
|
29
|
+
} from './props'
|
|
30
|
+
import type { FormFieldLayoutTheme } from '@instructure/shared-types'
|
|
25
31
|
import { mapSpacingToShorthand } from '@instructure/emotion'
|
|
26
|
-
import type { FormFieldLayoutProps, FormFieldLayoutStyle } from './props'
|
|
27
32
|
|
|
33
|
+
const generateGridLayout = (
|
|
34
|
+
isInlineLayout: boolean,
|
|
35
|
+
hasNewErrorMsgAndIsGroup: boolean,
|
|
36
|
+
hasVisibleLabel: boolean,
|
|
37
|
+
hasMessages: boolean
|
|
38
|
+
) => {
|
|
39
|
+
if (isInlineLayout) {
|
|
40
|
+
if (hasNewErrorMsgAndIsGroup) {
|
|
41
|
+
if (hasMessages) {
|
|
42
|
+
return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
|
|
43
|
+
". controls"`
|
|
44
|
+
} else {
|
|
45
|
+
return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
|
|
49
|
+
${hasMessages ? ' ". messages"' : ''}`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// stacked layout -- in this case we could use a simple `Flex`
|
|
53
|
+
if (hasNewErrorMsgAndIsGroup) {
|
|
54
|
+
return `${hasVisibleLabel ? ' "label"' : ''}
|
|
55
|
+
${hasMessages ? ' "messages"' : ''}
|
|
56
|
+
"controls"`
|
|
57
|
+
} else {
|
|
58
|
+
return `${hasVisibleLabel ? ' "label"' : ''}
|
|
59
|
+
"controls"
|
|
60
|
+
${hasMessages ? ' "messages"' : ''}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
28
63
|
/**
|
|
29
64
|
* ---
|
|
30
65
|
* private: true
|
|
@@ -32,21 +67,62 @@ import type { FormFieldLayoutProps, FormFieldLayoutStyle } from './props'
|
|
|
32
67
|
* Generates the style object from the theme and provided additional information
|
|
33
68
|
* @param {Object} componentTheme The theme variable object.
|
|
34
69
|
* @param {Object} props the props of the component, the style is applied to
|
|
35
|
-
* @param {Object}
|
|
70
|
+
* @param {Object} styleProps
|
|
36
71
|
* @return {Object} The final style object, which will be used in the component
|
|
37
72
|
*/
|
|
38
73
|
const generateStyle = (
|
|
39
|
-
componentTheme:
|
|
40
|
-
props: FormFieldLayoutProps
|
|
74
|
+
componentTheme: FormFieldLayoutTheme,
|
|
75
|
+
props: FormFieldLayoutProps,
|
|
76
|
+
styleProps: FormFieldStyleProps
|
|
41
77
|
): FormFieldLayoutStyle => {
|
|
42
|
-
const { inline, margin } = props
|
|
43
|
-
const {
|
|
78
|
+
const { inline, layout, vAlign, labelAlign, margin } = props
|
|
79
|
+
const { hasMessages, hasVisibleLabel, hasNewErrorMsgAndIsGroup } = styleProps
|
|
80
|
+
const cssMargin = mapSpacingToShorthand(margin, componentTheme.spacing)
|
|
81
|
+
const isInlineLayout = layout === 'inline'
|
|
82
|
+
// This is quite ugly, we should simplify it
|
|
83
|
+
const gridTemplateAreas = generateGridLayout(
|
|
84
|
+
isInlineLayout,
|
|
85
|
+
hasNewErrorMsgAndIsGroup,
|
|
86
|
+
hasVisibleLabel,
|
|
87
|
+
hasMessages
|
|
88
|
+
)
|
|
89
|
+
let gridTemplateColumns = '100%' // stacked layout
|
|
90
|
+
if (isInlineLayout) {
|
|
91
|
+
gridTemplateColumns = '1fr 3fr'
|
|
92
|
+
if (inline) {
|
|
93
|
+
gridTemplateColumns = 'auto 3fr'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const labelStyles = {
|
|
97
|
+
all: 'initial',
|
|
98
|
+
display: 'block',
|
|
99
|
+
gridArea: 'label',
|
|
100
|
+
color: componentTheme.color,
|
|
101
|
+
fontFamily: componentTheme.fontFamily,
|
|
102
|
+
fontWeight: componentTheme.fontWeight,
|
|
103
|
+
fontSize: componentTheme.fontSize,
|
|
104
|
+
lineHeight: componentTheme.lineHeight,
|
|
105
|
+
margin: '0 0 0.75rem 0',
|
|
106
|
+
...(isInlineLayout && {
|
|
107
|
+
// when inline add a small padding between the label and the control
|
|
108
|
+
paddingRight: componentTheme.inlinePadding,
|
|
109
|
+
// and use the horizontal alignment prop
|
|
110
|
+
[`@media screen and (min-width: ${componentTheme.stackedOrInlineBreakpoint})`]:
|
|
111
|
+
{
|
|
112
|
+
textAlign: labelAlign
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
44
116
|
|
|
45
|
-
|
|
117
|
+
let alignItems = 'start'
|
|
118
|
+
if (vAlign == 'top') {
|
|
119
|
+
alignItems = 'start'
|
|
120
|
+
} else if (vAlign == 'middle') {
|
|
121
|
+
alignItems = 'center'
|
|
122
|
+
} else if (vAlign == 'bottom') {
|
|
123
|
+
alignItems = 'end'
|
|
124
|
+
}
|
|
46
125
|
return {
|
|
47
|
-
groupErrorMessage: {
|
|
48
|
-
margin: '0.5rem 0'
|
|
49
|
-
},
|
|
50
126
|
formFieldLayout: {
|
|
51
127
|
label: 'formFieldLayout',
|
|
52
128
|
all: 'initial',
|
|
@@ -57,14 +133,52 @@ const generateStyle = (
|
|
|
57
133
|
direction: 'inherit',
|
|
58
134
|
textAlign: 'start',
|
|
59
135
|
opacity: 'inherit',
|
|
60
|
-
display: '
|
|
136
|
+
display: 'grid',
|
|
137
|
+
alignItems: alignItems,
|
|
138
|
+
verticalAlign: 'middle', // removes margin in inline layouts
|
|
139
|
+
gridTemplateColumns: gridTemplateColumns,
|
|
140
|
+
gridTemplateAreas: gridTemplateAreas,
|
|
141
|
+
[`@media screen and (max-width: ${componentTheme.stackedOrInlineBreakpoint})`]:
|
|
142
|
+
{
|
|
143
|
+
// for small screens use the stacked layout
|
|
144
|
+
gridTemplateColumns: '100%',
|
|
145
|
+
gridTemplateAreas: generateGridLayout(
|
|
146
|
+
false,
|
|
147
|
+
hasNewErrorMsgAndIsGroup,
|
|
148
|
+
hasVisibleLabel,
|
|
149
|
+
hasMessages
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
columnGap: '0.375rem',
|
|
61
153
|
width: '100%',
|
|
62
|
-
|
|
63
154
|
...(inline && {
|
|
64
|
-
display: 'inline-
|
|
65
|
-
verticalAlign: 'middle',
|
|
155
|
+
display: 'inline-grid',
|
|
66
156
|
width: 'auto'
|
|
67
157
|
})
|
|
158
|
+
},
|
|
159
|
+
formFieldLabel: {
|
|
160
|
+
label: 'formFieldLayout__label',
|
|
161
|
+
...(hasVisibleLabel && {
|
|
162
|
+
...labelStyles,
|
|
163
|
+
// NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
|
|
164
|
+
'&:is(label)': labelStyles,
|
|
165
|
+
'&:-webkit-any(label)': labelStyles
|
|
166
|
+
})
|
|
167
|
+
},
|
|
168
|
+
formFieldChildren: {
|
|
169
|
+
label: 'formFieldLayout__children',
|
|
170
|
+
gridArea: 'controls',
|
|
171
|
+
// add a small margin between the message and the controls
|
|
172
|
+
...(hasMessages && hasNewErrorMsgAndIsGroup && { marginTop: '0.375rem' }),
|
|
173
|
+
...(hasMessages &&
|
|
174
|
+
!hasNewErrorMsgAndIsGroup && { marginBottom: '0.75rem' }),
|
|
175
|
+
...(isInlineLayout &&
|
|
176
|
+
inline && {
|
|
177
|
+
[`@media screen and (min-width: ${componentTheme.stackedOrInlineBreakpoint})`]:
|
|
178
|
+
{
|
|
179
|
+
justifySelf: 'start'
|
|
180
|
+
}
|
|
181
|
+
})
|
|
68
182
|
}
|
|
69
183
|
}
|
|
70
184
|
}
|
|
@@ -22,12 +22,38 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import type { Theme } from '@instructure/ui-themes'
|
|
25
|
+
import type { Theme, ThemeSpecificStyle } from '@instructure/ui-themes'
|
|
26
|
+
import { FormFieldLayoutTheme } from '@instructure/shared-types'
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Generates the theme object for the component from the theme and provided additional information
|
|
30
|
+
* @param {Object} theme The actual theme object.
|
|
31
|
+
* @return {Object} The final theme object with the overrides and component variables
|
|
32
|
+
*/
|
|
33
|
+
const generateComponentTheme = (theme: Theme): FormFieldLayoutTheme => {
|
|
34
|
+
const { colors, typography, spacing, breakpoints, key: themeName } = theme
|
|
35
|
+
|
|
36
|
+
const themeSpecificStyle: ThemeSpecificStyle<FormFieldLayoutTheme> = {
|
|
37
|
+
canvas: {
|
|
38
|
+
color: theme['ic-brand-font-color-dark']
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const componentVariables: FormFieldLayoutTheme = {
|
|
43
|
+
color: colors?.contrasts?.grey125125,
|
|
44
|
+
fontFamily: typography?.fontFamily,
|
|
45
|
+
fontWeight: typography?.fontWeightBold,
|
|
46
|
+
fontSize: typography?.fontSizeMedium,
|
|
47
|
+
lineHeight: typography?.lineHeightFit,
|
|
48
|
+
inlinePadding: spacing?.xxSmall,
|
|
49
|
+
stackedOrInlineBreakpoint: breakpoints?.medium,
|
|
50
|
+
spacing: theme.spacing
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
...componentVariables,
|
|
55
|
+
...themeSpecificStyle[themeName]
|
|
56
|
+
}
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
export default generateComponentTheme
|