@react-ui-org/react-ui 0.56.0 → 0.58.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 (103) hide show
  1. package/.nvmrc +1 -1
  2. package/dist/react-ui.css +17 -17
  3. package/dist/react-ui.development.css +2551 -831
  4. package/dist/react-ui.development.js +106 -66
  5. package/dist/react-ui.js +1 -1
  6. package/package.json +13 -2
  7. package/src/CNAME +1 -0
  8. package/src/components/Alert/Alert.jsx +5 -6
  9. package/src/components/Alert/Alert.module.scss +12 -34
  10. package/src/components/Alert/README.md +28 -15
  11. package/src/components/Alert/_settings.scss +5 -0
  12. package/src/components/Alert/_theme.scss +0 -43
  13. package/src/components/Badge/Badge.jsx +5 -3
  14. package/src/components/Badge/Badge.module.scss +29 -74
  15. package/src/components/Badge/README.md +19 -2
  16. package/src/components/Badge/_settings.scss +8 -0
  17. package/src/components/Button/Button.jsx +4 -3
  18. package/src/components/Button/Button.module.scss +183 -2
  19. package/src/components/Button/README.md +8 -6
  20. package/src/components/Button/_settings.scss +8 -3
  21. package/src/components/Button/_theme.scss +0 -3
  22. package/src/components/Button/_tools.scss +7 -71
  23. package/src/components/ButtonGroup/ButtonGroup.jsx +2 -2
  24. package/src/components/Card/Card.jsx +3 -2
  25. package/src/components/Card/Card.module.scss +13 -33
  26. package/src/components/Card/CardBody.jsx +1 -1
  27. package/src/components/Card/CardFooter.jsx +1 -1
  28. package/src/components/Card/README.md +28 -6
  29. package/src/components/Card/_settings.scss +5 -0
  30. package/src/components/Card/_theme.scss +0 -43
  31. package/src/components/CheckboxField/CheckboxField.jsx +9 -3
  32. package/src/components/CheckboxField/README.md +110 -5
  33. package/src/components/FileInputField/FileInputField.jsx +1 -1
  34. package/src/components/FormLayout/FormLayout.jsx +1 -1
  35. package/src/components/FormLayout/FormLayoutCustomField.jsx +1 -1
  36. package/src/components/Grid/Grid.jsx +1 -1
  37. package/src/components/Grid/Grid.module.scss +9 -2
  38. package/src/components/Grid/GridSpan.jsx +1 -1
  39. package/src/components/InputGroup/InputGroup.jsx +2 -2
  40. package/src/components/InputGroup/InputGroup.module.scss +9 -5
  41. package/src/components/Modal/Modal.jsx +1 -1
  42. package/src/components/Modal/ModalBody.jsx +1 -1
  43. package/src/components/Modal/ModalCloseButton.jsx +3 -5
  44. package/src/components/Modal/ModalContent.jsx +1 -1
  45. package/src/components/Modal/ModalFooter.jsx +1 -1
  46. package/src/components/Modal/ModalHeader.jsx +1 -1
  47. package/src/components/Modal/ModalTitle.jsx +1 -1
  48. package/src/components/Modal/README.md +18 -18
  49. package/src/components/Paper/Paper.jsx +1 -1
  50. package/src/components/Popover/Popover.jsx +58 -13
  51. package/src/components/Popover/Popover.module.scss +51 -23
  52. package/src/components/Popover/PopoverWrapper.jsx +1 -1
  53. package/src/components/Popover/README.md +60 -3
  54. package/src/components/Popover/_helpers/cleanPlacementStyle.js +20 -0
  55. package/src/components/Popover/_theme.scss +4 -2
  56. package/src/components/Radio/README.md +103 -0
  57. package/src/components/Radio/Radio.jsx +9 -3
  58. package/src/components/Radio/Radio.module.scss +4 -0
  59. package/src/components/ScrollView/ScrollView.jsx +3 -5
  60. package/src/components/SelectField/README.md +103 -0
  61. package/src/components/SelectField/SelectField.jsx +9 -3
  62. package/src/components/Table/Table.jsx +1 -1
  63. package/src/components/Tabs/Tabs.jsx +1 -1
  64. package/src/components/Tabs/TabsItem.jsx +1 -1
  65. package/src/components/Text/Text.jsx +1 -1
  66. package/src/components/TextArea/TextArea.jsx +1 -1
  67. package/src/components/TextField/README.md +14 -2
  68. package/src/components/TextField/TextField.jsx +1 -1
  69. package/src/components/TextLink/README.md +10 -3
  70. package/src/components/TextLink/TextLink.jsx +1 -1
  71. package/src/components/TextLink/_theme.scss +3 -3
  72. package/src/components/Toggle/README.md +83 -1
  73. package/src/components/Toggle/Toggle.jsx +9 -3
  74. package/src/components/Toolbar/Toolbar.jsx +1 -1
  75. package/src/components/Toolbar/ToolbarGroup.jsx +1 -1
  76. package/src/components/Toolbar/ToolbarItem.jsx +1 -1
  77. package/src/components/_helpers/getRootPriorityClassName.js +1 -1
  78. package/src/components/_helpers/resolveContextOrProp.js +6 -3
  79. package/src/index.js +3 -2
  80. package/src/providers/globalProps/GlobalPropsContext.jsx +5 -0
  81. package/src/providers/globalProps/GlobalPropsProvider.jsx +33 -0
  82. package/src/providers/globalProps/index.js +3 -0
  83. package/src/{provider → providers/globalProps}/withGlobalProps.jsx +16 -16
  84. package/src/providers/translations/TranslationsContext.jsx +6 -0
  85. package/src/providers/translations/TranslationsProvider.jsx +33 -0
  86. package/src/providers/translations/index.js +2 -0
  87. package/src/styles/elements/_links.scss +7 -2
  88. package/src/styles/settings/_collections.scss +9 -0
  89. package/src/styles/theme/_form-fields.scss +19 -0
  90. package/src/styles/theme/_links.scss +4 -3
  91. package/src/styles/tools/_collections.scss +265 -0
  92. package/src/styles/tools/_string.scss +5 -2
  93. package/src/styles/tools/form-fields/_box-field-layout.scss +7 -1
  94. package/src/styles/tools/form-fields/_foundation.scss +6 -4
  95. package/src/styles/tools/form-fields/_variants.scss +5 -1
  96. package/src/theme.scss +66 -1
  97. package/src/components/Alert/_tools.scss +0 -10
  98. package/src/components/Button/_base.scss +0 -156
  99. package/src/components/Button/_priorities.scss +0 -149
  100. package/src/components/Card/_tools.scss +0 -10
  101. package/src/provider/RUIContext.jsx +0 -9
  102. package/src/provider/RUIProvider.jsx +0 -42
  103. package/src/provider/index.js +0 -3
@@ -1,6 +1,12 @@
1
- // 1. Reset positioning for controlled variant.
2
- // 2. Shift Popover so there is space for the arrow between Popover and reference element.
3
- // 3. Add top offset in case it's not defined by external library.
1
+ // 1. Hide the popover by default. This is needed because the popover is
2
+ // controlled via CSS with the help of the helper popover. The popover can't
3
+ // be displayed directly, because relative positioning doesn't work with
4
+ // elements on the top-layer, so this CSS hack is needed.
5
+ // 2. Hide the popover helper element.
6
+ // 3. If the popover helper is open, show the actual popover.
7
+ // 4. Reset positioning for controlled variant.
8
+ // 5. Shift Popover so there is space for the arrow between Popover and reference element.
9
+ // 6. Add top offset in case it's not defined by external library.
4
10
 
5
11
  @use "theme";
6
12
 
@@ -49,54 +55,76 @@
49
55
  }
50
56
  }
51
57
 
58
+ // Controlled popover
59
+ .controlledPopover {
60
+ display: none; // 1.
61
+ }
62
+
63
+ .helper {
64
+ position: fixed; // 2.
65
+ inset: unset;
66
+ top: 0;
67
+ right: 0;
68
+ width: auto;
69
+ height: auto;
70
+ padding: 0;
71
+ border: none;
72
+ background: transparent;
73
+ pointer-events: none;
74
+ }
75
+
76
+ .helper:popover-open ~ .controlledPopover {
77
+ display: block; // 3.
78
+ }
79
+
52
80
  // Sides
53
81
  .isRootAtTop {
54
- bottom: 100%;
82
+ bottom: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
55
83
  }
56
84
 
57
85
  .isRootAtBottom {
58
- top: 100%;
86
+ top: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
59
87
  }
60
88
 
61
89
  .isRootAtLeft {
62
- right: 100%;
90
+ right: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
63
91
  }
64
92
 
65
93
  .isRootAtRight {
66
- left: 100%;
94
+ left: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
67
95
  }
68
96
 
69
97
  // Arrows
70
98
  .isRootAtTop > .arrow {
71
- top: 100%;
99
+ top: calc(100% - #{theme.$arrow-safe-rendering-overlap});
72
100
  }
73
101
 
74
102
  .isRootAtBottom > .arrow {
75
- bottom: 100%;
103
+ bottom: calc(100% - #{theme.$arrow-safe-rendering-overlap});
76
104
  }
77
105
 
78
106
  .isRootAtLeft > .arrow {
79
- left: 100%;
107
+ left: calc(100% - #{theme.$arrow-safe-rendering-overlap});
80
108
  }
81
109
 
82
110
  .isRootAtRight > .arrow {
83
- right: 100%;
111
+ right: calc(100% - #{theme.$arrow-safe-rendering-overlap});
84
112
  }
85
113
 
86
114
  // Side alignments: top
87
115
  .isRootAtTop.isRootAtCenter {
88
116
  left: 50%;
89
- transform: translate(-50%, #{-1 * theme.$arrow-height});
117
+ transform: translate(-50%, calc(-1 * #{theme.$arrow-height}));
90
118
  }
91
119
 
92
120
  .isRootAtTop.isRootAtStart {
93
121
  left: 0;
94
- transform: translate(0, #{-1 * theme.$arrow-height});
122
+ transform: translate(0, calc(-1 * #{theme.$arrow-height}));
95
123
  }
96
124
 
97
125
  .isRootAtTop.isRootAtEnd {
98
126
  right: 0;
99
- transform: translate(0, #{-1 * theme.$arrow-height});
127
+ transform: translate(0, calc(-1 * #{theme.$arrow-height}));
100
128
  }
101
129
 
102
130
  .isRootAtTop.isRootAtCenter > .arrow {
@@ -148,17 +176,17 @@
148
176
  // Side alignments: left
149
177
  .isRootAtLeft.isRootAtCenter {
150
178
  top: 50%;
151
- transform: translate(#{-1 * theme.$arrow-height}, -50%);
179
+ transform: translate(calc(-1 * #{theme.$arrow-height}), -50%);
152
180
  }
153
181
 
154
182
  .isRootAtLeft.isRootAtStart {
155
183
  top: 0;
156
- transform: translate(#{-1 * theme.$arrow-height}, 0);
184
+ transform: translate(calc(-1 * #{theme.$arrow-height}), 0);
157
185
  }
158
186
 
159
187
  .isRootAtLeft.isRootAtEnd {
160
188
  bottom: 0;
161
- transform: translate(#{-1 * theme.$arrow-height}, 0);
189
+ transform: translate(calc(-1 * #{theme.$arrow-height}), 0);
162
190
  }
163
191
 
164
192
  .isRootAtLeft.isRootAtCenter > .arrow {
@@ -212,27 +240,27 @@
212
240
  .isRootControlled.isRootAtBottom,
213
241
  .isRootControlled.isRootAtLeft,
214
242
  .isRootControlled.isRootAtRight {
215
- inset: unset; // 1.
243
+ inset: unset; // 4.
216
244
  }
217
245
 
218
246
  .isRootControlled.isRootAtTop {
219
- transform: translate(0, #{-1 * theme.$arrow-height}); // 2.
247
+ transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 5.
220
248
  }
221
249
 
222
250
  .isRootControlled.isRootAtBottom {
223
- transform: translate(0, #{theme.$arrow-height}); // 2.
251
+ transform: translate(0, #{theme.$arrow-height}); // 5.
224
252
  }
225
253
 
226
254
  .isRootControlled.isRootAtLeft {
227
- transform: translate(#{-1 * theme.$arrow-height}, 0); // 2.
255
+ transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 5.
228
256
  }
229
257
 
230
258
  .isRootControlled.isRootAtRight {
231
- transform: translate(#{theme.$arrow-height}, 0); // 2.
259
+ transform: translate(#{theme.$arrow-height}, 0); // 5.
232
260
  }
233
261
 
234
262
  .isRootControlled.isRootAtLeft.isRootAtStart,
235
263
  .isRootControlled.isRootAtRight.isRootAtStart {
236
- top: 0; // 3.
264
+ top: 0; // 6.
237
265
  }
238
266
  }
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { transferProps } from '../../utils/transferProps';
5
5
  import styles from './PopoverWrapper.module.scss';
6
6
 
@@ -162,6 +162,29 @@ automatically, including smart position updates to ensure Popover visibility,
162
162
  we recommend to involve an external library designed specifically for this
163
163
  purpose.
164
164
 
165
+ To position the popover, you need to provide the `placementStyle` prop with the
166
+ style you want to apply to the popover. This prop should only be used to
167
+ position the popover. The allowed props are:
168
+
169
+ - `position`
170
+ - `inset`
171
+ - `inset-inline-start`
172
+ - `inset-inline-end`
173
+ - `inset-block-start`
174
+ - `inset-block-end`
175
+ - `top`
176
+ - `right`
177
+ - `bottom`
178
+ - `left`
179
+ - `translate`
180
+ - `transform-origin`
181
+
182
+ ⚠️ [`inset`][mdn-inset] is a shorthand for `top right bottom left`, not for
183
+ `inset-*` properties.
184
+
185
+ As opposed to `top right bottom left` and the `inset` shorthand, `inset-*`
186
+ properties are writing-direction aware.
187
+
165
188
  ℹ️ The following example is using external library [Floating UI]. To use
166
189
  Floating UI, install it first:
167
190
 
@@ -267,10 +290,10 @@ React.createElement(() => {
267
290
  <Popover
268
291
  id="my-advanced-popover"
269
292
  placement={finalPlacement}
270
- style={{
293
+ placementStyle={{
271
294
  position: strategy,
272
- top: y ? y : '',
273
- left: x ? x : '',
295
+ top: `${y}px`,
296
+ left: `${x}px`,
274
297
  }}
275
298
  ref={floating}
276
299
  >
@@ -284,6 +307,39 @@ React.createElement(() => {
284
307
  });
285
308
  ```
286
309
 
310
+ ## Controlled Popover
311
+
312
+ Popover API can be used to control visibility of Popover component. You need to
313
+ set `id` on the trigger element and matching `popoverTargetId` attribute on the
314
+ Popover component. This leverages the browser's Popover API to control the
315
+ popover, automatically closing it when the trigger or the backdrop is pressed.
316
+
317
+ ```docoff-react-preview
318
+ React.createElement(() => {
319
+ // All inline styles in this example are for demonstration purposes only.
320
+ return (
321
+ <div
322
+ style={{
323
+ display: 'grid',
324
+ placeContent: 'center',
325
+ minWidth: '20rem',
326
+ minHeight: '10rem',
327
+ }}
328
+ >
329
+ <PopoverWrapper>
330
+ <Button
331
+ label="Want to see a popover? Click me!"
332
+ popovertarget="my-popover-helper"
333
+ />
334
+ <Popover id="my-popover" popoverTargetId="my-popover-helper">
335
+ Hello there!
336
+ </Popover>
337
+ </PopoverWrapper>
338
+ </div>
339
+ );
340
+ });
341
+ ```
342
+
287
343
  ## Forwarding HTML Attributes
288
344
 
289
345
  In addition to the options below in the [component's API](#api) section, you
@@ -326,5 +382,6 @@ which enables [Advanced Positioning](#advanced-positioning).
326
382
 
327
383
  [div-attributes]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div#attributes
328
384
  [Floating UI]: https://floating-ui.com/docs/react-dom
385
+ [mdn-inset]: https://developer.mozilla.org/en-US/docs/Web/CSS/inset
329
386
  [React common props]: https://react.dev/reference/react-dom/components/common#common-props
330
387
  [ref]: https://reactjs.org/docs/refs-and-the-dom.html
@@ -0,0 +1,20 @@
1
+ export default (placementStyle) => {
2
+ const validProps = [
3
+ 'position',
4
+ 'inset',
5
+ 'inset-inline-start',
6
+ 'inset-inline-end',
7
+ 'inset-block-start',
8
+ 'inset-block-end',
9
+ 'top',
10
+ 'right',
11
+ 'bottom',
12
+ 'left',
13
+ 'translate',
14
+ 'transform-origin',
15
+ ];
16
+
17
+ return Object.fromEntries(
18
+ Object.entries(placementStyle).filter(([prop]) => validProps.includes(prop)),
19
+ );
20
+ };
@@ -11,6 +11,8 @@ $color: var(--rui-Popover__color);
11
11
  $background-color: var(--rui-Popover__background-color);
12
12
  $box-shadow: var(--rui-Popover__box-shadow);
13
13
 
14
- $arrow-width: 1rem;
15
- $arrow-height: math.div($arrow-width, 2); // 1.
14
+ $arrow-safe-rendering-overlap: 1px;
15
+ $arrow-gap: 1px;
16
+ $arrow-width: calc(1rem + #{$arrow-safe-rendering-overlap * 2});
17
+ $arrow-height: calc($arrow-width / 2); // 1.
16
18
  $arrow-corner-offset: 0.75rem;
@@ -237,6 +237,109 @@ have.
237
237
  })
238
238
  ```
239
239
 
240
+ ### Required State
241
+
242
+ The required state indicates that the input is mandatory.
243
+
244
+ ```docoff-react-preview
245
+ React.createElement(() => {
246
+ const [fruit, setFruit] = React.useState('apple');
247
+ return (
248
+ <Radio
249
+ label="Your favourite fruit"
250
+ onChange={(e) => setFruit(e.target.value)}
251
+ options={[
252
+ {
253
+ label: 'Apple',
254
+ value: 'apple',
255
+ },
256
+ {
257
+ label: 'Banana',
258
+ value: 'banana',
259
+ },
260
+ {
261
+ label: 'Grapefruit',
262
+ value: 'grapefruit',
263
+ },
264
+ ]}
265
+ value={fruit}
266
+ required
267
+ />
268
+ );
269
+ })
270
+ ```
271
+
272
+ #### Styling the Required State
273
+
274
+ All form fields in React UI can be
275
+ [styled](/docs/customize/theming/forms/#required-state)
276
+ to indicate the required state.
277
+
278
+ However, you may find yourself in a situation where a form field is valid in
279
+ both selected and unselected states, for example to turn on or off a feature.
280
+ If your project uses the label color as the primary means to indicate the
281
+ required state of input fields and the usual asterisk `*` is omitted, you may
282
+ want to keep the label color consistent for both states to avoid confusion.
283
+
284
+ For this edge case, there is the `renderAsRequired` prop:
285
+
286
+ ```docoff-react-preview
287
+ React.createElement(() => {
288
+ const [fruit, setFruit] = React.useState('apple');
289
+ const options = [
290
+ {
291
+ label: 'Apple',
292
+ value: 'apple',
293
+ },
294
+ {
295
+ label: 'Banana',
296
+ value: 'banana',
297
+ },
298
+ {
299
+ label: 'Grapefruit',
300
+ value: 'grapefruit',
301
+ },
302
+ ];
303
+ return (
304
+ <React.Fragment>
305
+ <style>
306
+ {`
307
+ .example {
308
+ display: flex;
309
+ flex-wrap: wrap;
310
+ gap: 1rem 0.5rem;
311
+ }
312
+
313
+ .example--themed-form-fields {
314
+ --rui-FormField__label__color: var(--rui-color-text-secondary);
315
+ --rui-FormField--required__label__color: var(--rui-color-text-primary);
316
+ --rui-FormField--required__sign: '';
317
+ }
318
+ `}
319
+ </style>
320
+ <div class="example example--themed-form-fields">
321
+ <Radio
322
+ label="This field is optional"
323
+ onChange={(e) => setFruit(e.target.value)}
324
+ options={options}
325
+ value={fruit}
326
+ />
327
+ <Radio
328
+ label="This field is optional but looks like required"
329
+ onChange={(e) => setFruit(e.target.value)}
330
+ options={options}
331
+ value={fruit}
332
+ renderAsRequired
333
+ />
334
+ </div>
335
+ </React.Fragment>
336
+ );
337
+ })
338
+ ```
339
+
340
+ It renders the field as if it was required, but doesn't add the `required`
341
+ attribute to the actual input.
342
+
240
343
  ### Disabled State
241
344
 
242
345
  It's possible to disable just some options or the whole set.
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { useContext } from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { classNames } from '../../utils/classNames';
5
5
  import { transferProps } from '../../utils/transferProps';
6
6
  import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
@@ -16,6 +16,7 @@ export const Radio = ({
16
16
  label,
17
17
  layout,
18
18
  options,
19
+ renderAsRequired,
19
20
  required,
20
21
  validationState,
21
22
  validationText,
@@ -33,7 +34,7 @@ export const Radio = ({
33
34
  ? styles.isRootLayoutHorizontal
34
35
  : styles.isRootLayoutVertical,
35
36
  disabled && styles.isRootDisabled,
36
- required && styles.isRootRequired,
37
+ (renderAsRequired || required) && styles.isRootRequired,
37
38
  getRootValidationStateClassName(validationState, styles),
38
39
  )}
39
40
  disabled={disabled}
@@ -116,6 +117,7 @@ Radio.defaultProps = {
116
117
  id: undefined,
117
118
  isLabelVisible: true,
118
119
  layout: 'vertical',
120
+ renderAsRequired: false,
119
121
  required: false,
120
122
  validationState: null,
121
123
  validationText: null,
@@ -181,7 +183,11 @@ Radio.propTypes = {
181
183
  ]),
182
184
  })).isRequired,
183
185
  /**
184
- * If `true`, the input will be required.
186
+ * If `true`, the input will be rendered as if it was required.
187
+ */
188
+ renderAsRequired: PropTypes.bool,
189
+ /**
190
+ * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
185
191
  */
186
192
  required: PropTypes.bool,
187
193
  /**
@@ -52,6 +52,10 @@
52
52
  @include foundation.label-required();
53
53
  }
54
54
 
55
+ .isRootRequired .optionLabel {
56
+ @include foundation.label-required($show-require-sign: false);
57
+ }
58
+
55
59
  // States
56
60
  .isRootStateInvalid {
57
61
  @include variants.validation(invalid);
@@ -6,10 +6,8 @@ import React, {
6
6
  useRef,
7
7
  useState,
8
8
  } from 'react';
9
- import {
10
- RUIContext,
11
- withGlobalProps,
12
- } from '../../provider';
9
+ import { TranslationsContext } from '../../providers/translations';
10
+ import { withGlobalProps } from '../../providers/globalProps';
13
11
  import { classNames } from '../../utils/classNames';
14
12
  import { transferProps } from '../../utils/transferProps';
15
13
  import { getElementsPositionDifference } from './_helpers/getElementsPositionDifference';
@@ -48,7 +46,7 @@ export const ScrollView = React.forwardRef((props, ref) => {
48
46
  ...restProps
49
47
  } = props;
50
48
 
51
- const { translations } = useContext(RUIContext);
49
+ const translations = useContext(TranslationsContext);
52
50
 
53
51
  const [isAutoScrollInProgress, setIsAutoScrollInProgress] = useState(false);
54
52
  const [isScrolledAtStart, setIsScrolledAtStart] = useState(false);
@@ -592,6 +592,109 @@ React.createElement(() => {
592
592
  })
593
593
  ```
594
594
 
595
+ ### Required State
596
+
597
+ The required state indicates that the input is mandatory.
598
+
599
+ ```docoff-react-preview
600
+ React.createElement(() => {
601
+ const [fruit, setFruit] = React.useState('apple');
602
+ return (
603
+ <SelectField
604
+ label="Your favourite fruit"
605
+ onChange={(e) => setFruit(e.target.value)}
606
+ options={[
607
+ {
608
+ label: 'Apple',
609
+ value: 'apple',
610
+ },
611
+ {
612
+ label: 'Banana',
613
+ value: 'banana',
614
+ },
615
+ {
616
+ label: 'Grapefruit',
617
+ value: 'grapefruit',
618
+ },
619
+ ]}
620
+ value={fruit}
621
+ required
622
+ />
623
+ );
624
+ });
625
+ ```
626
+
627
+ #### Styling the Required State
628
+
629
+ All form fields in React UI can be
630
+ [styled](/docs/customize/theming/forms/#required-state)
631
+ to indicate the required state.
632
+
633
+ However, you may find yourself in a situation where a form field is valid in
634
+ both selected and unselected states, for example to turn on or off a feature.
635
+ If your project uses the label color as the primary means to indicate the
636
+ required state of input fields and the usual asterisk `*` is omitted, you may
637
+ want to keep the label color consistent for both states to avoid confusion.
638
+
639
+ For this edge case, there is the `renderAsRequired` prop:
640
+
641
+ ```docoff-react-preview
642
+ React.createElement(() => {
643
+ const [fruit, setFruit] = React.useState('apple');
644
+ const options = [
645
+ {
646
+ label: 'Apple',
647
+ value: 'apple',
648
+ },
649
+ {
650
+ label: 'Banana',
651
+ value: 'banana',
652
+ },
653
+ {
654
+ label: 'Grapefruit',
655
+ value: 'grapefruit',
656
+ },
657
+ ];
658
+ return (
659
+ <React.Fragment>
660
+ <style>
661
+ {`
662
+ .example {
663
+ display: flex;
664
+ flex-wrap: wrap;
665
+ gap: 1rem 0.5rem;
666
+ }
667
+
668
+ .example--themed-form-fields {
669
+ --rui-FormField__label__color: var(--rui-color-text-secondary);
670
+ --rui-FormField--required__label__color: var(--rui-color-text-primary);
671
+ --rui-FormField--required__sign: '';
672
+ }
673
+ `}
674
+ </style>
675
+ <div class="example example--themed-form-fields">
676
+ <SelectField
677
+ label="This field is optional"
678
+ onChange={(e) => setFruit(e.target.value)}
679
+ options={options}
680
+ value={fruit}
681
+ />
682
+ <SelectField
683
+ label="This field is optional but looks like required"
684
+ onChange={(e) => setFruit(e.target.value)}
685
+ options={options}
686
+ value={fruit}
687
+ renderAsRequired
688
+ />
689
+ </div>
690
+ </React.Fragment>
691
+ );
692
+ });
693
+ ```
694
+
695
+ It renders the field as if it was required, but doesn't add the `required`
696
+ attribute to the actual input.
697
+
595
698
  ### Disabled State
596
699
 
597
700
  It's possible to disable just some options or the whole input.
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { useContext } from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { classNames } from '../../utils/classNames';
5
5
  import { transferProps } from '../../utils/transferProps';
6
6
  import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
@@ -21,6 +21,7 @@ export const SelectField = React.forwardRef((props, ref) => {
21
21
  label,
22
22
  layout,
23
23
  options,
24
+ renderAsRequired,
24
25
  required,
25
26
  size,
26
27
  validationState,
@@ -43,7 +44,7 @@ export const SelectField = React.forwardRef((props, ref) => {
43
44
  ? styles.isRootLayoutHorizontal
44
45
  : styles.isRootLayoutVertical,
45
46
  inputGroupContext && styles.isRootGrouped,
46
- required && styles.isRootRequired,
47
+ (renderAsRequired || required) && styles.isRootRequired,
47
48
  getRootSizeClassName(
48
49
  resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
49
50
  styles,
@@ -136,6 +137,7 @@ SelectField.defaultProps = {
136
137
  id: undefined,
137
138
  isLabelVisible: true,
138
139
  layout: 'vertical',
140
+ renderAsRequired: false,
139
141
  required: false,
140
142
  size: 'medium',
141
143
  validationState: null,
@@ -227,7 +229,11 @@ SelectField.propTypes = {
227
229
  })),
228
230
  ]).isRequired,
229
231
  /**
230
- * If `true`, the input will be required.
232
+ * If `true`, the input will be rendered as if it was required.
233
+ */
234
+ renderAsRequired: PropTypes.bool,
235
+ /**
236
+ * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
231
237
  */
232
238
  required: PropTypes.bool,
233
239
  /**
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { transferProps } from '../../utils/transferProps';
5
5
  import { TableHeaderCell } from './_components/TableHeaderCell';
6
6
  import { TableBodyCell } from './_components/TableBodyCell';
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { transferProps } from '../../utils/transferProps';
5
5
  import styles from './Tabs.module.scss';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { classNames } from '../../utils/classNames';
5
5
  import { transferProps } from '../../utils/transferProps';
6
6
  import styles from './TabsItem.module.scss';
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { classNames } from '../../utils/classNames';
5
5
  import { transferProps } from '../../utils/transferProps';
6
6
  import { isChildrenEmpty } from '../_helpers/isChildrenEmpty';
@@ -1,6 +1,6 @@
1
1
  import PropTypes from 'prop-types';
2
2
  import React, { useContext } from 'react';
3
- import { withGlobalProps } from '../../provider';
3
+ import { withGlobalProps } from '../../providers/globalProps';
4
4
  import { classNames } from '../../utils/classNames';
5
5
  import { transferProps } from '../../utils/transferProps';
6
6
  import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';