@react-ui-org/react-ui 0.56.0 → 0.58.0

Sign up to get free protection for your applications and to get access to all the features.
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';