@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/es/FormField/index.js +5 -1
  3. package/es/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
  4. package/es/FormFieldGroup/index.js +18 -4
  5. package/es/FormFieldLabel/index.js +6 -6
  6. package/es/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +10 -8
  7. package/es/FormFieldLayout/index.js +79 -58
  8. package/es/FormFieldLayout/props.js +1 -6
  9. package/es/FormFieldLayout/styles.js +105 -9
  10. package/es/FormFieldLayout/theme.js +56 -0
  11. package/es/FormFieldMessages/props.js +3 -2
  12. package/es/FormFieldMessages/styles.js +5 -3
  13. package/es/FormPropTypes.js +6 -0
  14. package/lib/FormField/index.js +5 -1
  15. package/lib/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
  16. package/lib/FormFieldGroup/index.js +18 -4
  17. package/lib/FormFieldLabel/index.js +6 -5
  18. package/lib/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +9 -7
  19. package/lib/FormFieldLayout/index.js +77 -58
  20. package/lib/FormFieldLayout/props.js +1 -6
  21. package/lib/FormFieldLayout/styles.js +105 -9
  22. package/lib/FormFieldLayout/theme.js +62 -0
  23. package/lib/FormFieldMessages/props.js +3 -2
  24. package/lib/FormFieldMessages/styles.js +5 -3
  25. package/lib/FormPropTypes.js +6 -0
  26. package/package.json +15 -15
  27. package/src/FormField/README.md +31 -3
  28. package/src/FormField/index.tsx +3 -0
  29. package/src/FormFieldGroup/__new-tests__/FormFieldGroup.test.tsx +4 -6
  30. package/src/FormFieldGroup/index.tsx +41 -6
  31. package/src/FormFieldLabel/index.tsx +8 -3
  32. package/src/FormFieldLayout/__new-tests__/FormFieldLayout.test.tsx +6 -8
  33. package/src/FormFieldLayout/index.tsx +83 -100
  34. package/src/FormFieldLayout/props.ts +30 -7
  35. package/src/FormFieldLayout/styles.ts +124 -12
  36. package/src/FormFieldLayout/theme.ts +59 -0
  37. package/src/FormFieldMessages/props.ts +8 -2
  38. package/src/FormFieldMessages/styles.ts +5 -4
  39. package/src/FormPropTypes.ts +4 -0
  40. package/tsconfig.build.tsbuildinfo +1 -1
  41. package/types/FormField/index.d.ts.map +1 -1
  42. package/types/FormFieldGroup/index.d.ts +1 -0
  43. package/types/FormFieldGroup/index.d.ts.map +1 -1
  44. package/types/FormFieldLabel/index.d.ts +2 -2
  45. package/types/FormFieldLabel/index.d.ts.map +1 -1
  46. package/types/FormFieldLayout/index.d.ts +8 -7
  47. package/types/FormFieldLayout/index.d.ts.map +1 -1
  48. package/types/FormFieldLayout/props.d.ts +27 -3
  49. package/types/FormFieldLayout/props.d.ts.map +1 -1
  50. package/types/FormFieldLayout/styles.d.ts +4 -3
  51. package/types/FormFieldLayout/styles.d.ts.map +1 -1
  52. package/types/FormFieldLayout/theme.d.ts +10 -0
  53. package/types/FormFieldLayout/theme.d.ts.map +1 -0
  54. package/types/FormFieldMessages/index.d.ts +8 -2
  55. package/types/FormFieldMessages/index.d.ts.map +1 -1
  56. package/types/FormFieldMessages/props.d.ts +5 -0
  57. package/types/FormFieldMessages/props.d.ts.map +1 -1
  58. package/types/FormFieldMessages/styles.d.ts +2 -3
  59. package/types/FormFieldMessages/styles.d.ts.map +1 -1
  60. package/types/FormPropTypes.d.ts +3 -0
  61. 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(-1 * ${componentTheme.topMargin}) 0 0 0`
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',
@@ -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.10.2",
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.10.2",
27
- "@instructure/ui-babel-preset": "9.10.2",
28
- "@instructure/ui-test-utils": "9.10.2",
29
- "@instructure/ui-themes": "9.10.2",
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.10.2",
37
- "@instructure/emotion": "9.10.2",
38
- "@instructure/shared-types": "9.10.2",
39
- "@instructure/ui-a11y-content": "9.10.2",
40
- "@instructure/ui-a11y-utils": "9.10.2",
41
- "@instructure/ui-grid": "9.10.2",
42
- "@instructure/ui-icons": "9.10.2",
43
- "@instructure/ui-react-utils": "9.10.2",
44
- "@instructure/ui-utils": "9.10.2",
45
- "@instructure/uid": "9.10.2",
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": {
@@ -9,7 +9,35 @@ components. In most cases it shouldn't be used directly.
9
9
  ---
10
10
  type: example
11
11
  ---
12
- <FormField id="foo" label="Opacity" width="200px">
13
- <input style={{display: 'block', width: '100%'}}/>
14
- </FormField>
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
  ```
@@ -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
- "fieldset[class$='-formFieldLayout']"
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 legend', () => {
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
- "legend[class$='-screenReaderContent']"
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
- return { invalid: this.invalid }
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={this.invalid ? 'true' : undefined}
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 { omitProps, getElementType } from '@instructure/ui-react-utils'
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, screen } from '@testing-library/react'
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$='-formFieldLabel']"
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
- const input = screen.getByLabelText('Username')
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, null)
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 && hasVisibleChildren(this.props.label)
102
+ return this.props.label ? hasVisibleChildren(this.props.label) : false
106
103
  }
107
104
 
108
105
  get hasMessages() {
109
- return this.props.messages && this.props.messages.length > 0
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
- get inlineContainerAndLabel() {
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
- <Grid.Col
131
- textAlign={this.props.labelAlign}
132
- width={this.inlineContainerAndLabel ? 'auto' : 3}
133
- >
134
- <FormFieldLabel
135
- aria-hidden={this.elementType === 'fieldset' ? 'true' : undefined}
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
- </FormFieldLabel>
139
- </Grid.Col>
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 if (this.elementType !== 'fieldset') {
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
- <Grid.Row>
165
- <Grid.Col
166
- offset={this.inlineContainerAndLabel ? undefined : 3}
167
- textAlign={this.inlineContainerAndLabel ? 'end' : undefined}
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
- // any cast is needed to prevent Expression produces a union type that is too complex to represent errors
180
- const ElementType = this.elementType as any
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, layout, children } = props
181
+ const { width, children } = props
185
182
 
186
- const hasNewErrorMsg =
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.elementType === 'fieldset' && this.renderLegend()}
200
- <Grid
201
- rowSpacing="small"
202
- colSpacing="small"
203
- startAt={
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
- <Grid.Row>
209
- {this.renderLabel()}
210
- <Grid.Col
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
- inputContainerRef?: (element: HTMLSpanElement | null) => void
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' | 'groupErrorMessage'
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