@react-ui-org/react-ui 0.51.0 → 0.52.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. package/dist/lib.development.js +141 -33
  2. package/dist/lib.js +1 -1
  3. package/dist/react-ui.css +40 -0
  4. package/dist/react-ui.js +1 -0
  5. package/package.json +1 -1
  6. package/src/lib/components/Button/Button.jsx +17 -9
  7. package/src/lib/components/Button/_base.scss +21 -12
  8. package/src/lib/components/Button/_priorities.scss +1 -18
  9. package/src/lib/components/Button/_theme.scss +0 -10
  10. package/src/lib/components/ButtonGroup/ButtonGroup.jsx +5 -3
  11. package/src/lib/components/ButtonGroup/ButtonGroup.scss +26 -1
  12. package/src/lib/components/ButtonGroup/README.mdx +11 -1
  13. package/src/lib/components/ButtonGroup/_theme.scss +13 -0
  14. package/src/lib/components/FormLayout/README.mdx +5 -0
  15. package/src/lib/components/InputGroup/InputGroup.jsx +170 -0
  16. package/src/lib/components/InputGroup/InputGroup.scss +92 -0
  17. package/src/lib/components/InputGroup/InputGroupContext.js +3 -0
  18. package/src/lib/components/InputGroup/README.mdx +278 -0
  19. package/src/lib/components/InputGroup/_theme.scss +2 -0
  20. package/src/lib/components/InputGroup/index.js +2 -0
  21. package/src/lib/components/Modal/Modal.jsx +58 -97
  22. package/src/lib/components/Modal/README.mdx +288 -15
  23. package/src/lib/components/Modal/_helpers/getPositionClassName.js +7 -0
  24. package/src/lib/components/Modal/_helpers/getSizeClassName.js +19 -0
  25. package/src/lib/components/Modal/_hooks/useModalFocus.js +126 -0
  26. package/src/lib/components/Modal/_hooks/useModalScrollPrevention.js +35 -0
  27. package/src/lib/components/Modal/_settings.scss +1 -1
  28. package/src/lib/components/Radio/README.mdx +9 -1
  29. package/src/lib/components/Radio/Radio.jsx +39 -31
  30. package/src/lib/components/Radio/Radio.scss +12 -2
  31. package/src/lib/components/SelectField/SelectField.jsx +21 -8
  32. package/src/lib/components/SelectField/SelectField.scss +5 -0
  33. package/src/lib/components/TextField/TextField.jsx +21 -8
  34. package/src/lib/components/TextField/TextField.scss +5 -0
  35. package/src/lib/index.js +1 -0
  36. package/src/lib/styles/theme/_borders.scss +2 -1
  37. package/src/lib/styles/tools/form-fields/_box-field-elements.scss +19 -2
  38. package/src/lib/styles/tools/form-fields/_box-field-layout.scss +26 -14
  39. package/src/lib/styles/tools/form-fields/_box-field-sizes.scss +11 -8
  40. package/src/lib/styles/tools/form-fields/_foundation.scss +7 -0
  41. package/src/lib/theme.scss +23 -11
  42. /package/src/lib/components/{Button/helpers → _helpers}/getRootPriorityClassName.js +0 -0
@@ -0,0 +1,126 @@
1
+ import { useEffect } from 'react';
2
+
3
+ export const useModalFocus = (
4
+ autoFocus,
5
+ childrenWrapperRef,
6
+ primaryButtonRef,
7
+ closeButtonRef,
8
+ ) => {
9
+ useEffect(
10
+ () => {
11
+ // Following code finds all focusable elements and among them first not disabled form
12
+ // field element (input, textarea or select) or primary button and focuses it. This is
13
+ // necessary to have focus on one of those elements to be able to submit the form
14
+ // by pressing Enter key. If there are neither, it tries to focus any other focusable
15
+ // elements. In case there are none or `autoFocus` is disabled, childrenWrapperElement
16
+ // (Modal itself) is focused.
17
+
18
+ const childrenWrapperElement = childrenWrapperRef.current;
19
+
20
+ if (childrenWrapperElement == null) {
21
+ return () => {};
22
+ }
23
+
24
+ const childrenFocusableElements = Array.from(
25
+ childrenWrapperElement.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
26
+ );
27
+
28
+ const firstFocusableElement = childrenFocusableElements[0];
29
+ const lastFocusableElement = childrenFocusableElements[childrenFocusableElements.length - 1];
30
+
31
+ const resolveFocusBeforeListener = () => {
32
+ if (!autoFocus || childrenFocusableElements.length === 0) {
33
+ childrenWrapperElement.tabIndex = -1;
34
+ childrenWrapperElement.focus();
35
+ return;
36
+ }
37
+
38
+ const firstFormFieldEl = childrenFocusableElements.find(
39
+ (element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled,
40
+ );
41
+
42
+ if (firstFormFieldEl) {
43
+ firstFormFieldEl.focus();
44
+ return;
45
+ }
46
+
47
+ if (primaryButtonRef?.current != null) {
48
+ primaryButtonRef.current.focus();
49
+ return;
50
+ }
51
+
52
+ firstFocusableElement.focus();
53
+ };
54
+
55
+ const keyPressHandler = (e) => {
56
+ if (e.key === 'Escape' && closeButtonRef?.current != null) {
57
+ closeButtonRef.current.click();
58
+ return;
59
+ }
60
+
61
+ if (
62
+ e.key === 'Enter'
63
+ && e.target.nodeName !== 'BUTTON'
64
+ && e.target.nodeName !== 'TEXTAREA'
65
+ && e.target.nodeName !== 'A'
66
+ && primaryButtonRef?.current != null
67
+ ) {
68
+ primaryButtonRef.current.click();
69
+ return;
70
+ }
71
+
72
+ // Following code traps focus inside Modal
73
+
74
+ if (e.key !== 'Tab') {
75
+ return;
76
+ }
77
+
78
+ if (childrenFocusableElements.length === 0) {
79
+ childrenWrapperElement.focus();
80
+ e.preventDefault();
81
+ return;
82
+ }
83
+
84
+ if (
85
+ ![
86
+ ...childrenFocusableElements,
87
+ childrenWrapperElement,
88
+ ]
89
+ .includes(window.document.activeElement)
90
+ ) {
91
+ firstFocusableElement.focus();
92
+ e.preventDefault();
93
+ return;
94
+ }
95
+
96
+ if (!e.shiftKey && window.document.activeElement === lastFocusableElement) {
97
+ firstFocusableElement.focus();
98
+ e.preventDefault();
99
+ return;
100
+ }
101
+
102
+ if (e.shiftKey
103
+ && (
104
+ window.document.activeElement === firstFocusableElement
105
+ || window.document.activeElement === childrenWrapperElement
106
+ )
107
+ ) {
108
+ lastFocusableElement.focus();
109
+ e.preventDefault();
110
+ }
111
+ };
112
+
113
+ resolveFocusBeforeListener();
114
+
115
+ window.document.addEventListener('keydown', keyPressHandler, false);
116
+
117
+ return () => window.document.removeEventListener('keydown', keyPressHandler, false);
118
+ },
119
+ [
120
+ autoFocus,
121
+ childrenWrapperRef,
122
+ primaryButtonRef,
123
+ closeButtonRef,
124
+ ],
125
+ );
126
+ };
@@ -0,0 +1,35 @@
1
+ import { useLayoutEffect } from 'react';
2
+
3
+ export const useModalScrollPrevention = (preventScrollUnderneath) => {
4
+ useLayoutEffect(
5
+ () => {
6
+ if (preventScrollUnderneath === 'off') {
7
+ return () => {};
8
+ }
9
+
10
+ if (preventScrollUnderneath === 'default') {
11
+ const scrollbarWidth = Math.abs(window.innerWidth - window.document.documentElement.clientWidth);
12
+ const prevOverflow = window.document.body.style.overflow;
13
+ const prevPaddingRight = window.document.body.style.paddingRight;
14
+
15
+ window.document.body.style.overflow = 'hidden';
16
+
17
+ if (Number.isNaN(parseInt(prevPaddingRight, 10))) {
18
+ window.document.body.style.paddingRight = `${scrollbarWidth}px`;
19
+ } else {
20
+ window.document.body.style.paddingRight = `calc(${prevPaddingRight} + ${scrollbarWidth}px)`;
21
+ }
22
+
23
+ return () => {
24
+ window.document.body.style.overflow = prevOverflow;
25
+ window.document.body.style.paddingRight = prevPaddingRight;
26
+ };
27
+ }
28
+
29
+ preventScrollUnderneath?.start();
30
+
31
+ return preventScrollUnderneath?.reset;
32
+ },
33
+ [preventScrollUnderneath],
34
+ );
35
+ };
@@ -3,7 +3,7 @@
3
3
  @use "../../styles/theme/borders";
4
4
  @use "../../styles/theme/typography";
5
5
 
6
- $border-radius: borders.$radius;
6
+ $border-radius: borders.$radius-2;
7
7
  $z-index: z-indexes.$modal;
8
8
  $backdrop-z-index: z-indexes.$modal-backdrop;
9
9
  $title-font-size: map.get(typography.$font-size-values, 2);
@@ -77,7 +77,12 @@ See [API](#api) for all available options.
77
77
  - Use **clear, calm error messages** when there's a problem with what they
78
78
  entered.
79
79
 
80
- 📖 [Read more about checkboxes and radios at Nielsen Norman Group.](https://www.nngroup.com/articles/checkboxes-vs-radio-buttons/)
80
+ - In the background, Radio uses the [`fieldset`][fieldset] element. Not only it
81
+ improves the [accessibility] of the group, it also allows you to make use of
82
+ its built-in features like disabling all nested inputs or pairing the group
83
+ with a form outside. Consult [the MDN docs][fieldset] to learn more.
84
+
85
+ 📖 [Read more about checkboxes and radios at Nielsen Norman Group.][nng-radio]
81
86
 
82
87
  ## Invisible Label
83
88
 
@@ -308,5 +313,8 @@ options. On top of that, the following options are available for Radio.
308
313
  | `--rui-FormField--check__input--radio__border-radius` | Input corner radius |
309
314
  | `--rui-FormField--check__input--radio--checked__background-image` | Checked input background image (inline, URL, …) |
310
315
 
316
+ [nng-radio]: https://www.nngroup.com/articles/checkboxes-vs-radio-buttons/
317
+ [fieldset]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset
318
+ [accessibility]: https://www.w3.org/WAI/tutorials/forms/grouping/
311
319
  [React synthetic events]: https://reactjs.org/docs/events.html
312
320
  [radio]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio#additional_attributes
@@ -25,7 +25,8 @@ export const Radio = ({
25
25
  const context = useContext(FormLayoutContext);
26
26
 
27
27
  return (
28
- <div
28
+ <fieldset
29
+ {...transferProps(restProps)}
29
30
  className={classNames(
30
31
  styles.root,
31
32
  context && styles.isRootInFormLayout,
@@ -36,53 +37,59 @@ export const Radio = ({
36
37
  required && styles.isRootRequired,
37
38
  getRootValidationStateClassName(validationState, styles),
38
39
  )}
40
+ disabled={disabled}
39
41
  id={id}
40
42
  >
43
+ <legend
44
+ className={styles.legend}
45
+ id={id && `${id}__label`}
46
+ >
47
+ {label}
48
+ </legend>
41
49
  <div
50
+ aria-hidden
42
51
  className={classNames(
43
52
  styles.label,
44
53
  !isLabelVisible && styles.isLabelHidden,
45
54
  )}
46
- id={id && `${id}__labelText`}
55
+ id={id && `${id}__displayLabel`}
47
56
  >
48
57
  {label}
49
58
  </div>
50
59
  <div className={styles.field}>
51
- <ul className={styles.list}>
60
+ <div className={styles.options}>
52
61
  {
53
62
  options.map((option) => {
54
63
  const key = option.key ?? option.value;
55
64
  return (
56
- <li key={key}>
57
- <label
58
- className={styles.option}
59
- htmlFor={id && `${id}__item__${key}`}
60
- id={id && `${id}__item__${key}__label`}
65
+ <label
66
+ className={styles.option}
67
+ htmlFor={id && `${id}__item__${key}`}
68
+ id={id && `${id}__item__${key}__label`}
69
+ key={key}
70
+ >
71
+ <input
72
+ className={styles.input}
73
+ checked={restProps.onChange
74
+ ? (value === option.value) || false
75
+ : undefined}
76
+ disabled={disabled || option.disabled}
77
+ id={id && `${id}__item__${key}`}
78
+ name={id}
79
+ type="radio"
80
+ value={option.value}
81
+ />
82
+ <span
83
+ className={styles.optionLabel}
84
+ id={id && `${id}__item__${key}__labelText`}
61
85
  >
62
- <input
63
- {...transferProps(restProps)}
64
- className={styles.input}
65
- checked={restProps.onChange
66
- ? (value === option.value) || false
67
- : undefined}
68
- disabled={disabled || option.disabled}
69
- id={id && `${id}__item__${key}`}
70
- name={id}
71
- type="radio"
72
- value={option.value}
73
- />
74
- <span
75
- className={styles.optionLabel}
76
- id={id && `${id}__item__${key}__labelText`}
77
- >
78
- { option.label }
79
- </span>
80
- </label>
81
- </li>
86
+ { option.label }
87
+ </span>
88
+ </label>
82
89
  );
83
90
  })
84
91
  }
85
- </ul>
92
+ </div>
86
93
  {helpText && (
87
94
  <div
88
95
  className={styles.helpText}
@@ -100,7 +107,7 @@ export const Radio = ({
100
107
  </div>
101
108
  )}
102
109
  </div>
103
- </div>
110
+ </fieldset>
104
111
  );
105
112
  };
106
113
 
@@ -129,7 +136,8 @@ Radio.propTypes = {
129
136
  * ID of the root HTML element.
130
137
  *
131
138
  * Also serves as base for ids of nested elements:
132
- * * `<ID>__labelText`
139
+ * * `<ID>__label`
140
+ * * `<ID>__displayLabel`
133
141
  * * `<ID>__helpText`
134
142
  * * `<ID>__validationText`
135
143
  *
@@ -1,3 +1,6 @@
1
+ // 1. Legends are tricky to style, let's use a `div` instead.
2
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset#styling_with_css
3
+
1
4
  @use "../../styles/tools/form-fields/box-field-elements";
2
5
  @use "../../styles/tools/form-fields/box-field-layout";
3
6
  @use "../../styles/tools/form-fields/foundation";
@@ -11,15 +14,22 @@
11
14
  // Foundation
12
15
  .root {
13
16
  @include foundation.root();
17
+ @include foundation.fieldset();
14
18
  @include variants.visual(check);
15
19
  }
16
20
 
21
+ // 1.
22
+ .legend {
23
+ @include accessibility.hide-text();
24
+ }
25
+
26
+ // 1.
17
27
  .label,
18
28
  .optionLabel {
19
29
  @include foundation.label();
20
30
  }
21
31
 
22
- .list {
32
+ .options {
23
33
  @include reset.list();
24
34
  }
25
35
 
@@ -74,5 +84,5 @@
74
84
  }
75
85
 
76
86
  .isRootInFormLayout {
77
- @include box-field-layout.in-form-layout();
87
+ @include box-field-layout.in-form-layout($is-fieldset: true);
78
88
  }
@@ -7,6 +7,7 @@ import { getRootValidationStateClassName } from '../_helpers/getRootValidationSt
7
7
  import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
8
8
  import { transferProps } from '../_helpers/transferProps';
9
9
  import { FormLayoutContext } from '../FormLayout';
10
+ import { InputGroupContext } from '../InputGroup/InputGroupContext';
10
11
  import { Option } from './_components/Option';
11
12
  import styles from './SelectField.scss';
12
13
 
@@ -28,20 +29,25 @@ export const SelectField = React.forwardRef((props, ref) => {
28
29
  ...restProps
29
30
  } = props;
30
31
 
31
- const context = useContext(FormLayoutContext);
32
+ const formLayoutContext = useContext(FormLayoutContext);
33
+ const inputGroupContext = useContext(InputGroupContext);
32
34
 
33
35
  return (
34
36
  <label
35
37
  className={classNames(
36
38
  styles.root,
37
39
  fullWidth && styles.isRootFullWidth,
38
- context && styles.isRootInFormLayout,
39
- resolveContextOrProp(context && context.layout, layout) === 'horizontal'
40
+ formLayoutContext && styles.isRootInFormLayout,
41
+ resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
42
+ resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
40
43
  ? styles.isRootLayoutHorizontal
41
44
  : styles.isRootLayoutVertical,
42
- disabled && styles.isRootDisabled,
45
+ inputGroupContext && styles.isRootGrouped,
43
46
  required && styles.isRootRequired,
44
- getRootSizeClassName(size, styles),
47
+ getRootSizeClassName(
48
+ resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
49
+ styles,
50
+ ),
45
51
  getRootValidationStateClassName(validationState, styles),
46
52
  variant === 'filled' ? styles.isRootVariantFilled : styles.isRootVariantOutline,
47
53
  )}
@@ -51,7 +57,7 @@ export const SelectField = React.forwardRef((props, ref) => {
51
57
  <div
52
58
  className={classNames(
53
59
  styles.label,
54
- !isLabelVisible && styles.isLabelHidden,
60
+ (!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
55
61
  )}
56
62
  id={id && `${id}__labelText`}
57
63
  >
@@ -62,7 +68,7 @@ export const SelectField = React.forwardRef((props, ref) => {
62
68
  <select
63
69
  {...transferProps(restProps)}
64
70
  className={styles.input}
65
- disabled={disabled}
71
+ disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
66
72
  id={id}
67
73
  ref={ref}
68
74
  required={required}
@@ -110,7 +116,7 @@ export const SelectField = React.forwardRef((props, ref) => {
110
116
  {helpText}
111
117
  </div>
112
118
  )}
113
- {validationText && (
119
+ {(validationText && !inputGroupContext) && (
114
120
  <div
115
121
  className={styles.validationText}
116
122
  id={id && `${id}__validationText`}
@@ -169,6 +175,8 @@ SelectField.propTypes = {
169
175
  /**
170
176
  * If `false`, the label will be visually hidden (but remains accessible by assistive
171
177
  * technologies).
178
+ *
179
+ * Automatically set to `false` when the component is rendered within `InputGroup` component.
172
180
  */
173
181
  isLabelVisible: PropTypes.bool,
174
182
  /**
@@ -224,6 +232,8 @@ SelectField.propTypes = {
224
232
  required: PropTypes.bool,
225
233
  /**
226
234
  * Size of the field.
235
+ *
236
+ * Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
227
237
  */
228
238
  size: PropTypes.oneOf(['small', 'medium', 'large']),
229
239
  /**
@@ -232,6 +242,9 @@ SelectField.propTypes = {
232
242
  validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
233
243
  /**
234
244
  * Validation message to be displayed.
245
+ *
246
+ * Validation text is never rendered when the component is placed into `InputGroup`. Instead, the `InputGroup`
247
+ * component itself renders all validation texts of its nested components.
235
248
  */
236
249
  validationText: PropTypes.node,
237
250
  /**
@@ -102,3 +102,8 @@
102
102
  .isRootSizeLarge {
103
103
  @include box-field-sizes.size(large);
104
104
  }
105
+
106
+ // Groups
107
+ .isRootGrouped {
108
+ @include box-field-elements.in-group-layout();
109
+ }
@@ -7,6 +7,7 @@ import { getRootValidationStateClassName } from '../_helpers/getRootValidationSt
7
7
  import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
8
8
  import { transferProps } from '../_helpers/transferProps';
9
9
  import { FormLayoutContext } from '../FormLayout';
10
+ import { InputGroupContext } from '../InputGroup/InputGroupContext';
10
11
  import styles from './TextField.scss';
11
12
 
12
13
  const SMALL_INPUT_SIZE = 10;
@@ -29,7 +30,8 @@ export const TextField = React.forwardRef((props, ref) => {
29
30
  variant,
30
31
  ...restProps
31
32
  } = props;
32
- const context = useContext(FormLayoutContext);
33
+ const formLayoutContext = useContext(FormLayoutContext);
34
+ const inputGroupContext = useContext(InputGroupContext);
33
35
  const hasSmallInput = (inputSize !== null) && (inputSize <= SMALL_INPUT_SIZE);
34
36
 
35
37
  return (
@@ -39,13 +41,17 @@ export const TextField = React.forwardRef((props, ref) => {
39
41
  fullWidth && styles.isRootFullWidth,
40
42
  hasSmallInput && styles.hasRootSmallInput,
41
43
  inputSize && styles.hasRootCustomInputSize,
42
- context && styles.isRootInFormLayout,
43
- resolveContextOrProp(context && context.layout, layout) === 'horizontal'
44
+ formLayoutContext && styles.isRootInFormLayout,
45
+ resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
46
+ resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
44
47
  ? styles.isRootLayoutHorizontal
45
48
  : styles.isRootLayoutVertical,
46
- disabled && styles.isRootDisabled,
49
+ inputGroupContext && styles.isRootGrouped,
47
50
  required && styles.isRootRequired,
48
- getRootSizeClassName(size, styles),
51
+ getRootSizeClassName(
52
+ resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
53
+ styles,
54
+ ),
49
55
  getRootValidationStateClassName(validationState, styles),
50
56
  variant === 'filled' ? styles.isRootVariantFilled : styles.isRootVariantOutline,
51
57
  )}
@@ -56,7 +62,7 @@ export const TextField = React.forwardRef((props, ref) => {
56
62
  <div
57
63
  className={classNames(
58
64
  styles.label,
59
- !isLabelVisible && styles.isLabelHidden,
65
+ (!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
60
66
  )}
61
67
  id={id && `${id}__labelText`}
62
68
  >
@@ -67,7 +73,7 @@ export const TextField = React.forwardRef((props, ref) => {
67
73
  <input
68
74
  {...transferProps(restProps)}
69
75
  className={styles.input}
70
- disabled={disabled}
76
+ disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
71
77
  id={id}
72
78
  ref={ref}
73
79
  required={required}
@@ -86,7 +92,7 @@ export const TextField = React.forwardRef((props, ref) => {
86
92
  {helpText}
87
93
  </div>
88
94
  )}
89
- {validationText && (
95
+ {(validationText && !inputGroupContext) && (
90
96
  <div
91
97
  className={styles.validationText}
92
98
  id={id && `${id}__validationText`}
@@ -143,6 +149,8 @@ TextField.propTypes = {
143
149
  /**
144
150
  * If `false`, the label will be visually hidden (but remains accessible by assistive
145
151
  * technologies).
152
+ *
153
+ * Automatically set to `false` when the component is rendered within `InputGroup` component.
146
154
  */
147
155
  isLabelVisible: PropTypes.bool,
148
156
  /**
@@ -162,6 +170,8 @@ TextField.propTypes = {
162
170
  required: PropTypes.bool,
163
171
  /**
164
172
  * Size of the field.
173
+ *
174
+ * Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
165
175
  */
166
176
  size: PropTypes.oneOf(['small', 'medium', 'large']),
167
177
  /**
@@ -174,6 +184,9 @@ TextField.propTypes = {
174
184
  validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
175
185
  /**
176
186
  * Validation message to be displayed.
187
+ *
188
+ * Validation text is never rendered when the component is placed into `InputGroup`. Instead, the `InputGroup`
189
+ * component itself renders all validation texts of its nested components.
177
190
  */
178
191
  validationText: PropTypes.node,
179
192
  /**
@@ -100,3 +100,8 @@
100
100
  .isRootSizeLarge {
101
101
  @include box-field-sizes.size(large);
102
102
  }
103
+
104
+ // Groups
105
+ .isRootGrouped {
106
+ @include box-field-elements.in-group-layout();
107
+ }
package/src/lib/index.js CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  Grid,
19
19
  GridSpan,
20
20
  } from './components/Grid';
21
+ export { InputGroup } from './components/InputGroup';
21
22
  export {
22
23
  Modal,
23
24
  ModalBody,
@@ -1,2 +1,3 @@
1
1
  $width: var(--rui-dimension-border-width-1);
2
- $radius: var(--rui-dimension-radius-1);
2
+ $radius-1: var(--rui-dimension-radius-1);
3
+ $radius-2: var(--rui-dimension-radius-2);
@@ -3,6 +3,7 @@
3
3
  // result width across browsers.
4
4
  // 3. Let inputs properly fit various layout scenarios.
5
5
  // 4. Leave out space for SelectField caret.
6
+ // 5. Use a block-level display mode to prevent extra white space below grouped inputs in Safari.
6
7
 
7
8
  @use "../../settings/form-fields" as settings;
8
9
  @use "../../theme/form-fields" as theme;
@@ -93,8 +94,8 @@
93
94
  align-items: center;
94
95
  justify-content: center;
95
96
  width: calc(#{settings.$box-field-caret-size} - 2 * #{theme.$box-border-width});
96
- border-top-right-radius: theme.$box-border-radius;
97
- border-bottom-right-radius: theme.$box-border-radius;
97
+ border-start-end-radius: theme.$box-border-radius;
98
+ border-end-end-radius: theme.$box-border-radius;
98
99
  pointer-events: none;
99
100
  }
100
101
 
@@ -122,3 +123,19 @@
122
123
  transform: scaleX(1);
123
124
  }
124
125
  }
126
+
127
+ @mixin in-group-layout() {
128
+ .inputContainer {
129
+ display: block; // 5.
130
+ }
131
+
132
+ &:not(:first-child) .input {
133
+ border-start-start-radius: var(--rui-local-inner-border-radius);
134
+ border-end-start-radius: var(--rui-local-inner-border-radius);
135
+ }
136
+
137
+ &:not(:last-child) .input {
138
+ border-start-end-radius: var(--rui-local-inner-border-radius);
139
+ border-end-end-radius: var(--rui-local-inner-border-radius);
140
+ }
141
+ }
@@ -26,8 +26,12 @@
26
26
  // Reverted for full-width fields.
27
27
  //
28
28
  // 8. Grid settings are inherited from horizontal FormLayout and applied using `subgrid`.
29
- // A fallback is supplied to browsers that don't support `subgrid` yet. See FormLayout styles
30
- // for more.
29
+ // A fallback is supplied to browsers that don't support `subgrid` yet.
30
+ //
31
+ // Chrome 117+ supports `subgrid` but it doesn't work for `<fieldset>`. This is why we always
32
+ // use the fallback for `<fieldset>`.
33
+ //
34
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1473242
31
35
  // https://github.com/react-ui-org/react-ui/issues/232
32
36
  //
33
37
  // 9. Help texts and validation messages can take up full width of FormLayout. There is no reason
@@ -180,7 +184,7 @@
180
184
  }
181
185
  }
182
186
 
183
- @mixin in-form-layout() {
187
+ @mixin in-form-layout($is-fieldset: false) {
184
188
  justify-self: start; // 12.
185
189
 
186
190
  .field {
@@ -192,19 +196,27 @@
192
196
  width: auto; // 11.
193
197
  }
194
198
 
195
- &.isRootLayoutHorizontal,
196
- &.isRootLayoutHorizontal.hasRootSmallInput {
197
- grid: inherit; // 8.
198
- grid-template-columns: subgrid; // 8.
199
- grid-column: span 2; // 8.
200
-
201
- @supports not (grid-template-columns: subgrid) {
202
- display: contents; // 8.
199
+ // 8.
200
+ @if $is-fieldset {
201
+ &.isRootLayoutHorizontal,
202
+ &.isRootLayoutHorizontal.hasRootSmallInput {
203
+ display: contents;
204
+ }
205
+ } @else {
206
+ &.isRootLayoutHorizontal,
207
+ &.isRootLayoutHorizontal.hasRootSmallInput {
208
+ grid: inherit;
209
+ grid-template-columns: subgrid;
210
+ grid-column: span 2;
211
+
212
+ @supports not (grid-template-columns: subgrid) {
213
+ display: contents;
214
+ }
203
215
  }
204
- }
205
216
 
206
- &.isRootLayoutHorizontal.isRootFullWidth {
207
- grid-template-columns: subgrid; // 8.
217
+ &.isRootLayoutHorizontal.isRootFullWidth {
218
+ grid-template-columns: subgrid;
219
+ }
208
220
  }
209
221
 
210
222
  &.isRootLayoutHorizontal .label,