@instructure/ui-form-field 9.11.0 → 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 +11 -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
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
+ ## [9.11.1](https://github.com/instructure/instructure-ui/compare/v9.11.0...v9.11.1) (2025-03-11)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * fix FormFieldLabel not read on NVDA hover ([c29b6ab](https://github.com/instructure/instructure-ui/commit/c29b6ab989266c6a7875e3f0dbdee3efbf18606a))
12
+
13
+
14
+
15
+
16
+
6
17
  # [9.11.0](https://github.com/instructure/instructure-ui/compare/v9.10.2...v9.11.0) (2025-02-12)
7
18
 
8
19
  **Note:** Version bump only for package @instructure/ui-form-field
@@ -47,7 +47,11 @@ class FormField extends Component {
47
47
  return /*#__PURE__*/React.createElement(FormFieldLayout, Object.assign({}, omitProps(this.props, FormField.allowedProps), pickProps(this.props, FormFieldLayout.allowedProps), {
48
48
  label: this.props.label,
49
49
  vAlign: this.props.vAlign,
50
- as: "label",
50
+ as: "label"
51
+ // This makes the control in focus when the label is clicked
52
+ // This is needed to prevent the wrong element to be focused, e.g.
53
+ // multi selects Tag-s
54
+ ,
51
55
  htmlFor: this.props.id,
52
56
  elementRef: this.handleRef
53
57
  }));
@@ -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 _dec, _class, _FormFieldLabel;
1
+ var _dec, _dec2, _class, _FormFieldLabel;
2
2
  /*
3
3
  * The MIT License (MIT)
4
4
  *
@@ -25,7 +25,7 @@ var _dec, _class, _FormFieldLabel;
25
25
 
26
26
  /** @jsx jsx */
27
27
  import { Component } from 'react';
28
- import { omitProps, getElementType } from '@instructure/ui-react-utils';
28
+ import { omitProps, getElementType, deprecated } from '@instructure/ui-react-utils';
29
29
  import { withStyle, jsx } from '@instructure/emotion';
30
30
  import generateStyle from './styles';
31
31
  import generateComponentTheme from './theme';
@@ -35,8 +35,7 @@ import { propTypes, allowedProps } from './props';
35
35
  parent: FormField
36
36
  ---
37
37
 
38
- This is a helper component that is used by most of the custom form
39
- components. In most cases it shouldn't be used directly.
38
+ This is a helper component that is used by most of the custom form components. In most cases it shouldn't be used directly.
40
39
 
41
40
  ```js
42
41
  ---
@@ -45,8 +44,9 @@ type: example
45
44
  <FormFieldLabel>Hello</FormFieldLabel>
46
45
  ```
47
46
 
47
+ @deprecated This is an internal component that will be removed in the future
48
48
  **/
49
- let FormFieldLabel = (_dec = withStyle(generateStyle, generateComponentTheme), _dec(_class = (_FormFieldLabel = class FormFieldLabel extends Component {
49
+ let FormFieldLabel = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 = deprecated('10', null, 'This component will be removed in a future version'), _dec(_class = _dec2(_class = (_FormFieldLabel = class FormFieldLabel extends Component {
50
50
  constructor(...args) {
51
51
  super(...args);
52
52
  this.ref = null;
@@ -74,6 +74,6 @@ let FormFieldLabel = (_dec = withStyle(generateStyle, generateComponentTheme), _
74
74
  }
75
75
  }, _FormFieldLabel.displayName = "FormFieldLabel", _FormFieldLabel.componentId = 'FormFieldLabel', _FormFieldLabel.propTypes = propTypes, _FormFieldLabel.allowedProps = allowedProps, _FormFieldLabel.defaultProps = {
76
76
  as: 'span'
77
- }, _FormFieldLabel)) || _class);
77
+ }, _FormFieldLabel)) || _class) || _class);
78
78
  export default FormFieldLabel;
79
79
  export { FormFieldLabel };
@@ -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,24 +28,23 @@ 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 { logError as error } from '@instructure/console';
34
- import { omitProps, pickProps, getElementType, withDeterministicId } from '@instructure/ui-react-utils';
31
+ import { omitProps, getElementType, withDeterministicId } from '@instructure/ui-react-utils';
35
32
  import { withStyle, jsx } from '@instructure/emotion';
36
- import { FormFieldLabel } from '../FormFieldLabel';
37
33
  import { FormFieldMessages } from '../FormFieldMessages';
38
34
  import generateStyle from './styles';
39
35
  import { propTypes, allowedProps } from './props';
36
+ import generateComponentTheme from './theme';
37
+
40
38
  /**
41
39
  ---
42
40
  parent: FormField
43
41
  ---
44
42
  **/
45
- let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, null), _dec(_class = _dec2(_class = (_FormFieldLayout = class FormFieldLayout extends Component {
43
+ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateStyle, generateComponentTheme), _dec(_class = _dec2(_class = (_FormFieldLayout = class FormFieldLayout extends Component {
46
44
  constructor(props) {
47
45
  super(props);
48
46
  this._messagesId = void 0;
47
+ this._labelId = void 0;
49
48
  this.ref = null;
50
49
  this.handleRef = el => {
51
50
  const elementRef = this.props.elementRef;
@@ -54,71 +53,99 @@ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateS
54
53
  elementRef(el);
55
54
  }
56
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
+ };
57
66
  this.handleInputContainerRef = node => {
58
67
  if (typeof this.props.inputContainerRef === 'function') {
59
68
  this.props.inputContainerRef(node);
60
69
  }
61
70
  };
62
71
  this._messagesId = props.messagesId || props.deterministicId();
63
- error(typeof props.width !== 'undefined' || !props.inline || props.layout !== 'inline', `[FormFieldLayout] The 'inline' prop is true, and the 'layout' is set to 'inline'.
64
- This will cause a layout issue in Internet Explorer 11 unless you also add a value for the 'width' prop.`);
72
+ this._labelId = props.deterministicId('FormField-Label');
65
73
  }
66
74
  componentDidMount() {
67
75
  var _this$props$makeStyle, _this$props;
68
- (_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());
69
77
  }
70
78
  componentDidUpdate() {
71
79
  var _this$props$makeStyle2, _this$props2;
72
- (_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());
73
81
  }
74
82
  get hasVisibleLabel() {
75
- return this.props.label && hasVisibleChildren(this.props.label);
83
+ return this.props.label ? hasVisibleChildren(this.props.label) : false;
76
84
  }
77
85
  get hasMessages() {
78
- 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;
79
100
  }
80
101
  get elementType() {
81
102
  return getElementType(FormFieldLayout, this.props);
82
103
  }
83
- get inlineContainerAndLabel() {
84
- // Return if both the component container and label will display inline
85
- return this.props.inline && this.props.layout === 'inline';
86
- }
87
104
  renderLabel() {
88
105
  if (this.hasVisibleLabel) {
89
- return jsx(Grid.Col, {
90
- textAlign: this.props.labelAlign,
91
- width: this.inlineContainerAndLabel ? 'auto' : 3
92
- }, jsx(FormFieldLabel, {
93
- "aria-hidden": this.elementType === 'fieldset' ? 'true' : void 0
94
- }, this.props.label));
95
- } else if (this.elementType !== 'fieldset') {
96
- // to avoid duplicate label/legend content
97
- return this.props.label;
98
- } else {
99
- return null;
100
- }
101
- }
102
- renderLegend() {
103
- // note: the legend element must be the first child of a fieldset element for SR
104
- // so we render it twice in that case (once for SR-only and one that is visible)
105
- return jsx(ScreenReaderContent, {
106
- as: "legend"
107
- }, this.props.label, this.hasMessages && jsx(FormFieldMessages, {
108
- messages: this.props.messages
109
- }));
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
+ style: {
135
+ display: 'contents'
136
+ }
137
+ }, this.props.label);
138
+ } else return null;
110
139
  }
111
140
  renderVisibleMessages() {
112
- return this.hasMessages ? jsx(Grid.Row, null, jsx(Grid.Col, {
113
- offset: this.inlineContainerAndLabel ? void 0 : 3,
114
- textAlign: this.inlineContainerAndLabel ? 'end' : void 0
115
- }, jsx(FormFieldMessages, {
141
+ return this.hasMessages ? jsx(FormFieldMessages, {
116
142
  id: this._messagesId,
117
- messages: this.props.messages
118
- }))) : null;
143
+ messages: this.props.messages,
144
+ gridArea: "messages"
145
+ }) : null;
119
146
  }
120
147
  render() {
121
- // any cast is needed to prevent Expression produces a union type that is too complex to represent errors
148
+ // Should be `<label>` if it's a FormField, fieldset if it's a group
122
149
  const ElementType = this.elementType;
123
150
  const _this$props3 = this.props,
124
151
  makeStyles = _this$props3.makeStyles,
@@ -127,26 +154,20 @@ let FormFieldLayout = (_dec = withDeterministicId(), _dec2 = withStyle(generateS
127
154
  isGroup = _this$props3.isGroup,
128
155
  props = _objectWithoutProperties(_this$props3, _excluded);
129
156
  const width = props.width,
130
- layout = props.layout,
131
157
  children = props.children;
132
- const hasNewErrorMsg = !!(messages !== null && messages !== void 0 && messages.find(m => m.type === 'newError')) && isGroup;
133
- return jsx(ElementType, Object.assign({}, omitProps(props, [...FormFieldLayout.allowedProps, ...Grid.allowedProps]), {
158
+ const hasNewErrorMsgAndIsGroup = !!(messages !== null && messages !== void 0 && messages.find(m => m.type === 'newError')) && isGroup;
159
+ return jsx(ElementType, Object.assign({}, omitProps(props, [...FormFieldLayout.allowedProps]), {
134
160
  css: styles === null || styles === void 0 ? void 0 : styles.formFieldLayout,
161
+ "aria-describedby": this.hasMessages ? this._messagesId : void 0,
162
+ "aria-errormessage": this.props['aria-invalid'] ? this._messagesId : void 0,
135
163
  style: {
136
164
  width
137
165
  },
138
- "aria-describedby": this.hasMessages ? this._messagesId : void 0,
139
166
  ref: this.handleRef
140
- }), this.elementType === 'fieldset' && this.renderLegend(), jsx(Grid, Object.assign({
141
- rowSpacing: "small",
142
- colSpacing: "small",
143
- startAt: layout === 'inline' && this.hasVisibleLabel ? 'medium' : null
144
- }, pickProps(props, Grid.allowedProps)), jsx(Grid.Row, null, this.renderLabel(), jsx(Grid.Col, {
145
- width: this.inlineContainerAndLabel ? 'auto' : void 0,
146
- elementRef: this.handleInputContainerRef
147
- }, hasNewErrorMsg && jsx("div", {
148
- css: styles === null || styles === void 0 ? void 0 : styles.groupErrorMessage
149
- }, this.renderVisibleMessages()), children)), !hasNewErrorMsg && this.renderVisibleMessages()));
167
+ }), this.renderLabel(), hasNewErrorMsgAndIsGroup && this.renderVisibleMessages(), jsx("span", {
168
+ css: styles === null || styles === void 0 ? void 0 : styles.formFieldChildren,
169
+ ref: this.handleInputContainerRef
170
+ }, children), !hasNewErrorMsgAndIsGroup && this.renderVisibleMessages());
150
171
  }
151
172
  }, _FormFieldLayout.displayName = "FormFieldLayout", _FormFieldLayout.componentId = 'FormFieldLayout', _FormFieldLayout.propTypes = propTypes, _FormFieldLayout.allowedProps = allowedProps, _FormFieldLayout.defaultProps = {
152
173
  inline: false,
@@ -40,10 +40,5 @@ const propTypes = {
40
40
  elementRef: PropTypes.func,
41
41
  isGroup: PropTypes.bool
42
42
  };
43
- const allowedProps = ['label', 'id', 'as', 'messages', 'messagesId', 'children', 'inline', 'layout', 'labelAlign', 'width', 'inputContainerRef', 'elementRef'
44
-
45
- // added vAlign because FormField and FormFieldGroup passes it, but not adding
46
- // it to allowedProps to prevent it from getting passed through accidentally
47
- //'vAlign'
48
- ];
43
+ const allowedProps = ['label', 'id', 'as', 'messages', 'messagesId', 'children', 'inline', 'layout', 'labelAlign', 'width', 'inputContainerRef', 'elementRef', 'vAlign'];
49
44
  export { propTypes, allowedProps };
@@ -22,6 +22,31 @@
22
22
  * SOFTWARE.
23
23
  */
24
24
 
25
+ const generateGridLayout = (isInlineLayout, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages) => {
26
+ if (isInlineLayout) {
27
+ if (hasNewErrorMsgAndIsGroup) {
28
+ if (hasMessages) {
29
+ return `${hasVisibleLabel ? ' "label messages"' : '. messages'}
30
+ ". controls"`;
31
+ } else {
32
+ return `${hasVisibleLabel ? ' "label controls"' : '. controls'}`;
33
+ }
34
+ } else {
35
+ return `${hasVisibleLabel ? ' "label controls"' : '. controls'}
36
+ ${hasMessages ? ' ". messages"' : ''}`;
37
+ }
38
+ }
39
+ // stacked layout -- in this case we could use a simple `Flex`
40
+ if (hasNewErrorMsgAndIsGroup) {
41
+ return `${hasVisibleLabel ? ' "label"' : ''}
42
+ ${hasMessages ? ' "messages"' : ''}
43
+ "controls"`;
44
+ } else {
45
+ return `${hasVisibleLabel ? ' "label"' : ''}
46
+ "controls"
47
+ ${hasMessages ? ' "messages"' : ''}`;
48
+ }
49
+ };
25
50
  /**
26
51
  * ---
27
52
  * private: true
@@ -29,15 +54,55 @@
29
54
  * Generates the style object from the theme and provided additional information
30
55
  * @param {Object} componentTheme The theme variable object.
31
56
  * @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
57
+ * @param {Object} styleProps
33
58
  * @return {Object} The final style object, which will be used in the component
34
59
  */
35
- const generateStyle = (_componentTheme, props) => {
36
- const inline = props.inline;
60
+ const generateStyle = (componentTheme, props, styleProps) => {
61
+ const inline = props.inline,
62
+ layout = props.layout,
63
+ vAlign = props.vAlign,
64
+ labelAlign = props.labelAlign;
65
+ const hasMessages = styleProps.hasMessages,
66
+ hasVisibleLabel = styleProps.hasVisibleLabel,
67
+ hasNewErrorMsgAndIsGroup = styleProps.hasNewErrorMsgAndIsGroup;
68
+ const isInlineLayout = layout === 'inline';
69
+ // This is quite ugly, we should simplify it
70
+ const gridTemplateAreas = generateGridLayout(isInlineLayout, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages);
71
+ let gridTemplateColumns = '100%'; // stacked layout
72
+ if (isInlineLayout) {
73
+ gridTemplateColumns = '1fr 3fr';
74
+ if (inline) {
75
+ gridTemplateColumns = 'auto 3fr';
76
+ }
77
+ }
78
+ const labelStyles = {
79
+ all: 'initial',
80
+ display: 'block',
81
+ gridArea: 'label',
82
+ color: componentTheme.color,
83
+ fontFamily: componentTheme.fontFamily,
84
+ fontWeight: componentTheme.fontWeight,
85
+ fontSize: componentTheme.fontSize,
86
+ lineHeight: componentTheme.lineHeight,
87
+ ...(isInlineLayout && {
88
+ margin: '0',
89
+ // when inline add a small padding between the label and the control
90
+ paddingRight: componentTheme.inlinePadding,
91
+ // and use the horizontal alignment prop
92
+ [`@media screen and (width >= ${componentTheme.stackedOrInlineBreakpoint})`]: {
93
+ textAlign: labelAlign
94
+ }
95
+ })
96
+ };
97
+ let alignItems = 'start';
98
+ if (vAlign == 'top') {
99
+ alignItems = 'start';
100
+ } else if (vAlign == 'middle') {
101
+ alignItems = 'center';
102
+ } else if (vAlign == 'bottom') {
103
+ alignItems = 'end';
104
+ }
37
105
  return {
38
- groupErrorMessage: {
39
- margin: '0.5rem 0'
40
- },
41
106
  formFieldLayout: {
42
107
  label: 'formFieldLayout',
43
108
  all: 'initial',
@@ -48,13 +113,44 @@ const generateStyle = (_componentTheme, props) => {
48
113
  direction: 'inherit',
49
114
  textAlign: 'start',
50
115
  opacity: 'inherit',
51
- display: 'block',
116
+ display: 'grid',
117
+ alignItems: alignItems,
118
+ verticalAlign: 'middle',
119
+ // removes margin in inline layouts
120
+ gridTemplateColumns: gridTemplateColumns,
121
+ gridTemplateAreas: gridTemplateAreas,
122
+ [`@media screen and (width < ${componentTheme.stackedOrInlineBreakpoint})`]: {
123
+ // for small screens use the stacked layout
124
+ gridTemplateColumns: '100%',
125
+ gridTemplateAreas: generateGridLayout(false, hasNewErrorMsgAndIsGroup, hasVisibleLabel, hasMessages)
126
+ },
127
+ columnGap: '0.375rem',
128
+ rowGap: '0.75rem',
52
129
  width: '100%',
53
130
  ...(inline && {
54
- display: 'inline-block',
55
- verticalAlign: 'middle',
131
+ display: 'inline-grid',
56
132
  width: 'auto'
57
133
  })
134
+ },
135
+ formFieldLabel: {
136
+ label: 'formFieldLayout__label',
137
+ ...labelStyles,
138
+ // NOTE: needs separate groups for `:is()` and `:-webkit-any()` because of css selector group validation (see https://www.w3.org/TR/selectors-3/#grouping)
139
+ '&:is(label)': labelStyles,
140
+ '&:-webkit-any(label)': labelStyles
141
+ },
142
+ formFieldChildren: {
143
+ label: 'formFieldLayout__children',
144
+ gridArea: 'controls',
145
+ // add a small margin between the message and the controls
146
+ ...(hasMessages && hasNewErrorMsgAndIsGroup && {
147
+ marginTop: '0.375rem'
148
+ }),
149
+ ...(isInlineLayout && inline && {
150
+ [`@media screen and (width >= ${componentTheme.stackedOrInlineBreakpoint})`]: {
151
+ justifySelf: 'start'
152
+ }
153
+ })
58
154
  }
59
155
  };
60
156
  };
@@ -0,0 +1,56 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
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
+ */
30
+ const generateComponentTheme = theme => {
31
+ const colors = theme.colors,
32
+ typography = theme.typography,
33
+ spacing = theme.spacing,
34
+ breakpoints = theme.breakpoints,
35
+ themeName = theme.key;
36
+ const themeSpecificStyle = {
37
+ canvas: {
38
+ color: theme['ic-brand-font-color-dark']
39
+ }
40
+ };
41
+ const componentVariables = {
42
+ color: colors === null || colors === void 0 ? void 0 : colors.licorice,
43
+ fontFamily: typography === null || typography === void 0 ? void 0 : typography.fontFamily,
44
+ fontWeight: typography === null || typography === void 0 ? void 0 : typography.fontWeightBold,
45
+ fontSize: typography === null || typography === void 0 ? void 0 : typography.fontSizeMedium,
46
+ lineHeight: typography === null || typography === void 0 ? void 0 : typography.lineHeightFit,
47
+ inlinePadding: spacing === null || spacing === void 0 ? void 0 : spacing.xxSmall,
48
+ stackedOrInlineBreakpoint: breakpoints === null || breakpoints === void 0 ? void 0 : breakpoints.medium,
49
+ spacing: theme.spacing
50
+ };
51
+ return {
52
+ ...componentVariables,
53
+ ...themeSpecificStyle[themeName]
54
+ };
55
+ };
56
+ 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',