@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/es/FormField/index.js +0 -1
  3. package/es/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
  4. package/es/FormFieldGroup/index.js +18 -4
  5. package/es/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +10 -8
  6. package/es/FormFieldLayout/index.js +73 -54
  7. package/es/FormFieldLayout/styles.js +109 -10
  8. package/es/FormFieldLayout/theme.js +28 -2
  9. package/es/FormFieldMessages/props.js +3 -2
  10. package/es/FormFieldMessages/styles.js +5 -3
  11. package/es/FormPropTypes.js +6 -0
  12. package/es/index.js +0 -1
  13. package/lib/FormField/index.js +0 -1
  14. package/lib/FormFieldGroup/__new-tests__/FormFieldGroup.test.js +4 -4
  15. package/lib/FormFieldGroup/index.js +18 -4
  16. package/lib/FormFieldLayout/__new-tests__/FormFieldLayout.test.js +9 -7
  17. package/lib/FormFieldLayout/index.js +72 -54
  18. package/lib/FormFieldLayout/styles.js +109 -10
  19. package/lib/FormFieldLayout/theme.js +28 -2
  20. package/lib/FormFieldMessages/props.js +3 -2
  21. package/lib/FormFieldMessages/styles.js +5 -3
  22. package/lib/FormPropTypes.js +6 -0
  23. package/lib/index.js +0 -7
  24. package/package.json +15 -15
  25. package/src/FormField/README.md +31 -3
  26. package/src/FormField/index.tsx +0 -1
  27. package/src/FormFieldGroup/__new-tests__/FormFieldGroup.test.tsx +4 -6
  28. package/src/FormFieldGroup/index.tsx +41 -6
  29. package/src/FormFieldLayout/__new-tests__/FormFieldLayout.test.tsx +6 -8
  30. package/src/FormFieldLayout/index.tsx +78 -92
  31. package/src/FormFieldLayout/props.ts +28 -2
  32. package/src/FormFieldLayout/styles.ts +128 -14
  33. package/src/FormFieldLayout/theme.ts +30 -4
  34. package/src/FormFieldMessages/props.ts +8 -2
  35. package/src/FormFieldMessages/styles.ts +5 -4
  36. package/src/FormPropTypes.ts +4 -0
  37. package/src/index.ts +0 -2
  38. package/tsconfig.build.tsbuildinfo +1 -1
  39. package/types/FormField/index.d.ts.map +1 -1
  40. package/types/FormFieldGroup/index.d.ts +1 -0
  41. package/types/FormFieldGroup/index.d.ts.map +1 -1
  42. package/types/FormFieldLayout/index.d.ts +8 -7
  43. package/types/FormFieldLayout/index.d.ts.map +1 -1
  44. package/types/FormFieldLayout/props.d.ts +27 -3
  45. package/types/FormFieldLayout/props.d.ts.map +1 -1
  46. package/types/FormFieldLayout/styles.d.ts +4 -3
  47. package/types/FormFieldLayout/styles.d.ts.map +1 -1
  48. package/types/FormFieldLayout/theme.d.ts +7 -1
  49. package/types/FormFieldLayout/theme.d.ts.map +1 -1
  50. package/types/FormFieldMessages/index.d.ts +8 -2
  51. package/types/FormFieldMessages/index.d.ts.map +1 -1
  52. package/types/FormFieldMessages/props.d.ts +5 -0
  53. package/types/FormFieldMessages/props.d.ts.map +1 -1
  54. package/types/FormFieldMessages/styles.d.ts +2 -3
  55. package/types/FormFieldMessages/styles.d.ts.map +1 -1
  56. package/types/FormPropTypes.d.ts +3 -0
  57. package/types/FormPropTypes.d.ts.map +1 -1
  58. package/types/index.d.ts +0 -2
  59. package/types/index.d.ts.map +1 -1
  60. package/es/FormFieldLabel/__new-tests__/FormFieldLabel.test.js +0 -66
  61. package/es/FormFieldLabel/index.js +0 -79
  62. package/es/FormFieldLabel/props.js +0 -31
  63. package/es/FormFieldLabel/styles.js +0 -62
  64. package/es/FormFieldLabel/theme.js +0 -52
  65. package/lib/FormFieldLabel/__new-tests__/FormFieldLabel.test.js +0 -68
  66. package/lib/FormFieldLabel/index.js +0 -85
  67. package/lib/FormFieldLabel/props.js +0 -37
  68. package/lib/FormFieldLabel/styles.js +0 -68
  69. package/lib/FormFieldLabel/theme.js +0 -58
  70. package/src/FormFieldLabel/__new-tests__/FormFieldLabel.test.tsx +0 -79
  71. package/src/FormFieldLabel/index.tsx +0 -95
  72. package/src/FormFieldLabel/props.ts +0 -58
  73. package/src/FormFieldLabel/styles.ts +0 -74
  74. package/src/FormFieldLabel/theme.ts +0 -56
  75. package/types/FormFieldLabel/__new-tests__/FormFieldLabel.test.d.ts +0 -2
  76. package/types/FormFieldLabel/__new-tests__/FormFieldLabel.test.d.ts.map +0 -1
  77. package/types/FormFieldLabel/index.d.ts +0 -42
  78. package/types/FormFieldLabel/index.d.ts.map +0 -1
  79. package/types/FormFieldLabel/props.d.ts +0 -15
  80. package/types/FormFieldLabel/props.d.ts.map +0 -1
  81. package/types/FormFieldLabel/styles.d.ts +0 -15
  82. package/types/FormFieldLabel/styles.d.ts.map +0 -1
  83. package/types/FormFieldLabel/theme.d.ts +0 -10
  84. 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
- "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
  >
@@ -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,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 && hasVisibleChildren(this.props.label)
102
+ return this.props.label ? hasVisibleChildren(this.props.label) : false
97
103
  }
98
104
 
99
105
  get hasMessages() {
100
- 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
101
120
  }
102
121
 
103
122
  get elementType() {
104
123
  return getElementType(FormFieldLayout, this.props)
105
124
  }
106
125
 
107
- get inlineContainerAndLabel() {
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
- <Grid.Col
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.elementType !== 'fieldset') {
133
- // to avoid duplicate label/legend content
134
- return this.props.label
135
- } else {
136
- return null
137
- }
138
- }
139
-
140
- renderLegend() {
141
- // note: the legend element must be the first child of a fieldset element for SR
142
- // so we render it twice in that case (once for SR-only and one that is visible)
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
- <Grid.Row>
156
- <Grid.Col
157
- offset={this.inlineContainerAndLabel ? undefined : 3}
158
- textAlign={this.inlineContainerAndLabel ? 'end' : undefined}
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
- // any cast is needed to prevent Expression produces a union type that is too complex to represent errors
171
- const ElementType = this.elementType as any
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, layout, children } = props
177
+ const { width, children } = props
176
178
 
177
- const hasNewErrorMsg =
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.elementType === 'fieldset' && this.renderLegend()}
191
- <Grid
192
- rowSpacing="small"
193
- colSpacing="small"
194
- startAt={
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
- <Grid.Row>
200
- {this.renderLabel()}
201
- <Grid.Col
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
- inputContainerRef?: (element: HTMLSpanElement | null) => void
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' | 'groupErrorMessage'
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} state the state of the component, the style is applied to
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: any,
40
- props: FormFieldLayoutProps
74
+ componentTheme: FormFieldLayoutTheme,
75
+ props: FormFieldLayoutProps,
76
+ styleProps: FormFieldStyleProps
41
77
  ): FormFieldLayoutStyle => {
42
- const { inline, margin } = props
43
- const { spacing } = componentTheme
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
- const cssMargin = mapSpacingToShorthand(margin, spacing)
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: 'block',
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-block',
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
- const generateComponentTheme = (theme: Theme): any => {
28
- const { spacing } = theme
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 { spacing }
53
+ return {
54
+ ...componentVariables,
55
+ ...themeSpecificStyle[themeName]
56
+ }
31
57
  }
32
58
 
33
59
  export default generateComponentTheme