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