@aws-amplify/ui-react 6.1.14 → 6.2.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.
- package/dist/{Field-1f747369.js → Field-d47a49dc.js} +12 -23
- package/dist/ThemeStyle-b2dce96a.js +91 -0
- package/dist/esm/components/ThemeProvider/ThemeProvider.mjs +3 -5
- package/dist/esm/components/ThemeProvider/ThemeStyle.mjs +69 -0
- package/dist/esm/helpers/constants.mjs +9 -0
- package/dist/esm/primitives/Checkbox/Checkbox.mjs +4 -4
- package/dist/esm/primitives/CheckboxField/CheckboxField.mjs +2 -2
- package/dist/esm/primitives/HighlightMatch/HighlightMatch.mjs +2 -2
- package/dist/esm/primitives/RadioGroupField/RadioGroupField.mjs +13 -5
- package/dist/esm/primitives/SelectField/SelectField.mjs +12 -3
- package/dist/esm/primitives/SliderField/SliderField.mjs +14 -6
- package/dist/esm/primitives/StepperField/StepperField.mjs +12 -3
- package/dist/esm/primitives/Tabs/TabsContainer.mjs +4 -1
- package/dist/esm/primitives/Tabs/TabsContext.mjs +1 -0
- package/dist/esm/primitives/Tabs/TabsItem.mjs +7 -2
- package/dist/esm/primitives/Tabs/TabsPanel.mjs +7 -2
- package/dist/esm/primitives/Tabs/constants.mjs +4 -0
- package/dist/esm/primitives/TextAreaField/TextAreaField.mjs +12 -3
- package/dist/esm/primitives/TextField/TextField.mjs +12 -3
- package/dist/esm/primitives/utils/createSpaceSeparatedIds.mjs +13 -0
- package/dist/esm/primitives/utils/getUniqueComponentId.mjs +3 -0
- package/dist/esm/server.mjs +2 -0
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +180 -114
- package/dist/internal.js +2 -1
- package/dist/primitiveWithForwardRef-7e929242.js +36 -0
- package/dist/server.js +32 -0
- package/dist/styles/breadcrumbs.css +2 -4
- package/dist/styles/breadcrumbs.layer.css +2 -4
- package/dist/styles/button.css +10 -19
- package/dist/styles/button.layer.css +10 -19
- package/dist/styles/input.css +2 -3
- package/dist/styles/input.layer.css +2 -3
- package/dist/styles/link.css +5 -10
- package/dist/styles/link.layer.css +5 -10
- package/dist/styles/reset.css +1 -3
- package/dist/styles/reset.layer.css +1 -3
- package/dist/styles/sliderField.css +2 -3
- package/dist/styles/sliderField.layer.css +2 -3
- package/dist/styles/textArea.css +2 -3
- package/dist/styles/textArea.layer.css +2 -3
- package/dist/styles.css +23 -42
- package/dist/styles.layer.css +23 -42
- package/dist/types/components/ThemeProvider/ThemeStyle.d.ts +18 -0
- package/dist/types/helpers/constants.d.ts +2 -0
- package/dist/types/primitives/Tabs/TabsContext.d.ts +1 -0
- package/dist/types/primitives/Tabs/constants.d.ts +1 -0
- package/dist/types/primitives/shared/responsive/utils.d.ts +1 -1
- package/dist/types/primitives/utils/createSpaceSeparatedIds.d.ts +8 -0
- package/dist/types/primitives/utils/getUniqueComponentId.d.ts +1 -0
- package/dist/types/server.d.ts +2 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +11 -5
- package/server/package.json +7 -0
- package/dist/esm/primitives/utils/getTestId.mjs +0 -3
- package/dist/types/primitives/utils/getTestId.d.ts +0 -1
|
@@ -5,6 +5,7 @@ var core = require('@aws-amplify/core');
|
|
|
5
5
|
var auth = require('aws-amplify/auth');
|
|
6
6
|
var uiReactCore = require('@aws-amplify/ui-react-core');
|
|
7
7
|
var ui = require('@aws-amplify/ui');
|
|
8
|
+
var primitiveWithForwardRef = require('./primitiveWithForwardRef-7e929242.js');
|
|
8
9
|
|
|
9
10
|
function _interopNamespace(e) {
|
|
10
11
|
if (e && e.__esModule) return e;
|
|
@@ -635,17 +636,6 @@ const useStyles = (props, style) => {
|
|
|
635
636
|
}), [propStyles, style, breakpoints, breakpoint, tokens]);
|
|
636
637
|
};
|
|
637
638
|
|
|
638
|
-
/**
|
|
639
|
-
* Updates the return type for primitives wrapped in `React.forwardRef` to
|
|
640
|
-
* `React.ReactElement`. In React 18 the return type of `React.ExoticComponent`
|
|
641
|
-
* was changed from `React.ReactElement` to `React.ReactNode`, which breaks
|
|
642
|
-
* clients using React 16 and 17.
|
|
643
|
-
*
|
|
644
|
-
* @param primitive UI Primitive to be wrapped with `React.forwardRef`
|
|
645
|
-
* @returns ForwaredRef wrapped UI Primitive
|
|
646
|
-
*/
|
|
647
|
-
const primitiveWithForwardRef = (primitive) => React__namespace.forwardRef(primitive);
|
|
648
|
-
|
|
649
639
|
const ViewPrimitive = ({ as = 'div', children, testId, ariaLabel, isDisabled, style, inert, ...rest }, ref) => {
|
|
650
640
|
const { propStyles, nonStyleProps } = useStyles(rest, style);
|
|
651
641
|
return React__namespace.createElement(as, {
|
|
@@ -661,7 +651,7 @@ const ViewPrimitive = ({ as = 'div', children, testId, ariaLabel, isDisabled, st
|
|
|
661
651
|
/**
|
|
662
652
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/view)
|
|
663
653
|
*/
|
|
664
|
-
const View = primitiveWithForwardRef(ViewPrimitive);
|
|
654
|
+
const View = primitiveWithForwardRef.primitiveWithForwardRef(ViewPrimitive);
|
|
665
655
|
View.displayName = 'View';
|
|
666
656
|
|
|
667
657
|
const defaultViewBox = { minX: 0, minY: 0, width: 24, height: 24 };
|
|
@@ -691,7 +681,7 @@ as = 'svg', fill = 'currentColor', pathData, viewBox = defaultViewBox, children,
|
|
|
691
681
|
/**
|
|
692
682
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/icon)
|
|
693
683
|
*/
|
|
694
|
-
const Icon = primitiveWithForwardRef(IconPrimitive);
|
|
684
|
+
const Icon = primitiveWithForwardRef.primitiveWithForwardRef(IconPrimitive);
|
|
695
685
|
Icon.displayName = 'Icon';
|
|
696
686
|
|
|
697
687
|
const IconsContext = React__namespace.createContext({});
|
|
@@ -994,7 +984,7 @@ function useDropZone({ onDropComplete, onDragEnter: _onDragEnter, onDragLeave: _
|
|
|
994
984
|
const FieldGroupIconPrimitive = ({ className, children, isVisible = true, excludeFromTabOrder = false, ...rest }, ref) => {
|
|
995
985
|
return isVisible ? (React__namespace.createElement(View, { className: ui.classNames(ui.ComponentClassName.FieldGroupIcon, className), ref: ref, tabIndex: excludeFromTabOrder ? -1 : undefined, ...rest }, children)) : null;
|
|
996
986
|
};
|
|
997
|
-
const FieldGroupIcon = primitiveWithForwardRef(FieldGroupIconPrimitive);
|
|
987
|
+
const FieldGroupIcon = primitiveWithForwardRef.primitiveWithForwardRef(FieldGroupIconPrimitive);
|
|
998
988
|
FieldGroupIcon.displayName = 'FieldGroupIcon';
|
|
999
989
|
|
|
1000
990
|
const FieldsetContext = React__namespace.createContext({
|
|
@@ -1011,7 +1001,7 @@ const FlexPrimitive = ({ className, children, ...rest }, ref) => (React__namespa
|
|
|
1011
1001
|
/**
|
|
1012
1002
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/flex)
|
|
1013
1003
|
*/
|
|
1014
|
-
const Flex = primitiveWithForwardRef(FlexPrimitive);
|
|
1004
|
+
const Flex = primitiveWithForwardRef.primitiveWithForwardRef(FlexPrimitive);
|
|
1015
1005
|
Flex.displayName = 'Flex';
|
|
1016
1006
|
|
|
1017
1007
|
const LINEAR_EMPTY = 'linear-empty';
|
|
@@ -1061,7 +1051,7 @@ const LoaderPrimitive = ({ className, filledColor, emptyColor, size, variation,
|
|
|
1061
1051
|
/**
|
|
1062
1052
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/loader)
|
|
1063
1053
|
*/
|
|
1064
|
-
const Loader = primitiveWithForwardRef(LoaderPrimitive);
|
|
1054
|
+
const Loader = primitiveWithForwardRef.primitiveWithForwardRef(LoaderPrimitive);
|
|
1065
1055
|
Loader.displayName = 'Loader';
|
|
1066
1056
|
|
|
1067
1057
|
// These variations support colorThemes. 'undefined' accounts for our
|
|
@@ -1085,11 +1075,11 @@ const ButtonPrimitive = ({ className, children, colorTheme, isFullWidth = false,
|
|
|
1085
1075
|
/**
|
|
1086
1076
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/button)
|
|
1087
1077
|
*/
|
|
1088
|
-
const Button = primitiveWithForwardRef(ButtonPrimitive);
|
|
1078
|
+
const Button = primitiveWithForwardRef.primitiveWithForwardRef(ButtonPrimitive);
|
|
1089
1079
|
Button.displayName = 'Button';
|
|
1090
1080
|
|
|
1091
1081
|
const FieldGroupIconButtonPrimitive = ({ children, className, ...rest }, ref) => (React__namespace.createElement(FieldGroupIcon, { as: Button, className: ui.classNames(ui.ComponentClassName.FieldGroupIconButton, className), ref: ref, ...rest }, children));
|
|
1092
|
-
const FieldGroupIconButton = primitiveWithForwardRef(FieldGroupIconButtonPrimitive);
|
|
1082
|
+
const FieldGroupIconButton = primitiveWithForwardRef.primitiveWithForwardRef(FieldGroupIconButtonPrimitive);
|
|
1093
1083
|
FieldGroupIconButton.displayName = 'FieldGroupIconButton';
|
|
1094
1084
|
|
|
1095
1085
|
const ariaLabelText = ComponentText.Fields.clearButtonLabel;
|
|
@@ -1097,7 +1087,7 @@ const FieldClearButtonPrimitive = ({ ariaLabel = ariaLabelText, size, ...rest },
|
|
|
1097
1087
|
const icons = useIcons('field');
|
|
1098
1088
|
return (React__namespace.createElement(FieldGroupIconButton, { ariaLabel: ariaLabel, size: size, ref: ref, ...rest }, icons?.clear ?? React__namespace.createElement(IconClose, null)));
|
|
1099
1089
|
};
|
|
1100
|
-
const FieldClearButton = primitiveWithForwardRef(FieldClearButtonPrimitive);
|
|
1090
|
+
const FieldClearButton = primitiveWithForwardRef.primitiveWithForwardRef(FieldClearButtonPrimitive);
|
|
1101
1091
|
FieldClearButton.displayName = 'FieldClearButton';
|
|
1102
1092
|
|
|
1103
1093
|
const TextPrimitive = ({ as = 'p', className, children, isTruncated, variation, ...rest }, ref) => {
|
|
@@ -1107,7 +1097,7 @@ const TextPrimitive = ({ as = 'p', className, children, isTruncated, variation,
|
|
|
1107
1097
|
/**
|
|
1108
1098
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/text)
|
|
1109
1099
|
*/
|
|
1110
|
-
const Text = primitiveWithForwardRef(TextPrimitive);
|
|
1100
|
+
const Text = primitiveWithForwardRef.primitiveWithForwardRef(TextPrimitive);
|
|
1111
1101
|
Text.displayName = 'Text';
|
|
1112
1102
|
|
|
1113
1103
|
const QA_FIELD_DESCRIPTION = 'qa-field-description';
|
|
@@ -1126,7 +1116,7 @@ const LabelPrimitive = ({ children, className, visuallyHidden, ...rest }, ref) =
|
|
|
1126
1116
|
[ui.ComponentClassName.VisuallyHidden]: visuallyHidden,
|
|
1127
1117
|
}), ref: ref, ...rest }, children));
|
|
1128
1118
|
};
|
|
1129
|
-
const Label = primitiveWithForwardRef(LabelPrimitive);
|
|
1119
|
+
const Label = primitiveWithForwardRef.primitiveWithForwardRef(LabelPrimitive);
|
|
1130
1120
|
Label.displayName = 'Label';
|
|
1131
1121
|
|
|
1132
1122
|
const FieldPrimitive = (props, ref) => {
|
|
@@ -1137,7 +1127,7 @@ const FieldPrimitive = (props, ref) => {
|
|
|
1137
1127
|
children,
|
|
1138
1128
|
errorMessage ? (React__namespace.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })) : null));
|
|
1139
1129
|
};
|
|
1140
|
-
const Field = primitiveWithForwardRef(FieldPrimitive);
|
|
1130
|
+
const Field = primitiveWithForwardRef.primitiveWithForwardRef(FieldPrimitive);
|
|
1141
1131
|
Field.displayName = 'Field';
|
|
1142
1132
|
|
|
1143
1133
|
exports.ARROW_DOWN = ARROW_DOWN;
|
|
@@ -1184,7 +1174,6 @@ exports.View = View;
|
|
|
1184
1174
|
exports.getConsecutiveIntArray = getConsecutiveIntArray;
|
|
1185
1175
|
exports.getStyleValue = getStyleValue;
|
|
1186
1176
|
exports.getValueAtCurrentBreakpoint = getValueAtCurrentBreakpoint;
|
|
1187
|
-
exports.primitiveWithForwardRef = primitiveWithForwardRef;
|
|
1188
1177
|
exports.strHasLength = strHasLength;
|
|
1189
1178
|
exports.useAuth = useAuth;
|
|
1190
1179
|
exports.useBreakpoint = useBreakpoint;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var primitiveWithForwardRef = require('./primitiveWithForwardRef-7e929242.js');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n["default"] = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
25
|
+
|
|
26
|
+
const ThemeStylePrimitive = ({ theme, nonce, ...rest }, ref) => {
|
|
27
|
+
if (!theme)
|
|
28
|
+
return null;
|
|
29
|
+
const { name, cssText } = theme;
|
|
30
|
+
/*
|
|
31
|
+
Only inject theme CSS variables if given a theme.
|
|
32
|
+
The CSS file users import already has the default theme variables in it.
|
|
33
|
+
This will allow users to use the provider and theme with CSS variables
|
|
34
|
+
without having to worry about specificity issues because this stylesheet
|
|
35
|
+
will likely come after a user's defined CSS.
|
|
36
|
+
|
|
37
|
+
Q: Why are we using dangerouslySetInnerHTML?
|
|
38
|
+
A: We need to directly inject the theme's CSS string into the <style> tag without typical HTML escaping.
|
|
39
|
+
For example, JSX would escape characters meaningful in CSS such as ', ", < and >, thus breaking the CSS.
|
|
40
|
+
Q: Why not use a sanitization library such as DOMPurify?
|
|
41
|
+
A: For our use case, we specifically want to purify CSS text, *not* HTML.
|
|
42
|
+
DOMPurify, as well as any other HTML sanitization library, would escape/encode meaningful CSS characters
|
|
43
|
+
and break our CSS in the same way that JSX would.
|
|
44
|
+
|
|
45
|
+
Q: Are there any security risks in this particular use case?
|
|
46
|
+
A: Anything set inside of a <style> tag is always interpreted as CSS text, *not* HTML.
|
|
47
|
+
Reference: “Restrictions on the content of raw text elements” https://html.spec.whatwg.org/dev/syntax.html#cdata-rcdata-restrictions
|
|
48
|
+
And in our case, we are using dangerouslySetInnerHTML to set CSS text inside of a <style> tag.
|
|
49
|
+
|
|
50
|
+
Thus, it really comes down to the question: Could a malicious user escape from the context of the <style> tag?
|
|
51
|
+
For example, when inserting HTML into the DOM, could someone prematurely close the </style> tag and add a <script> tag?
|
|
52
|
+
e.g., </style><script>alert('hello')</script>
|
|
53
|
+
The answer depends on whether the code is rendered on the client or server side.
|
|
54
|
+
|
|
55
|
+
Client side
|
|
56
|
+
- To prevent XSS inside of the <style> tag, we need to make sure it's not closed prematurely.
|
|
57
|
+
- This is prevented by React because React creates a style DOM node (e.g., React.createElement(‘style’, ...)), and directly sets innerHTML as a string.
|
|
58
|
+
- Even if the string contains a closing </style> tag, it will still be interpreted as CSS text by the browser.
|
|
59
|
+
- Therefore, there is not an XSS vulnerability on the client side.
|
|
60
|
+
|
|
61
|
+
Server side
|
|
62
|
+
- When React code is rendered on the server side (e.g., NextJS), the code is sent to the browser as HTML text.
|
|
63
|
+
- Therefore, it *IS* possible to insert a closing </style> tag and escape the CSS context, which opens an XSS vulnerability.
|
|
64
|
+
|
|
65
|
+
Q: How are we mitigating the potential attack vector?
|
|
66
|
+
A: To fix this potential attack vector on the server side, we need to filter out any closing </style> tags,
|
|
67
|
+
as this the only way to escape from the context of the browser interpreting the text as CSS.
|
|
68
|
+
We also need to catch cases where there is any kind of whitespace character </style[HERE]>, such as tabs, carriage returns, etc:
|
|
69
|
+
</style
|
|
70
|
+
|
|
71
|
+
>
|
|
72
|
+
Therefore, by only rendering CSS text which does not include a closing '</style>' tag,
|
|
73
|
+
we ensure that the browser will correctly interpret all the text as CSS.
|
|
74
|
+
*/
|
|
75
|
+
if (/<\/style/i.test(cssText)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return (React__namespace.createElement("style", { ...rest, ref: ref, id: `amplify-theme-${name}`,
|
|
80
|
+
// eslint-disable-next-line react/no-danger
|
|
81
|
+
dangerouslySetInnerHTML: { __html: cssText }, nonce: nonce }));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* @experimental
|
|
86
|
+
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
|
|
87
|
+
*/
|
|
88
|
+
const ThemeStyle = primitiveWithForwardRef.primitiveWithForwardRef(ThemeStylePrimitive);
|
|
89
|
+
ThemeStyle.displayName = 'ThemeStyle';
|
|
90
|
+
|
|
91
|
+
exports.ThemeStyle = ThemeStyle;
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import * as RadixDirection from '@radix-ui/react-direction';
|
|
3
3
|
import { sanitizeNamespaceImport, createTheme } from '@aws-amplify/ui';
|
|
4
4
|
import { ThemeContext } from './ThemeContext.mjs';
|
|
5
|
+
import { ThemeStyle } from './ThemeStyle.mjs';
|
|
5
6
|
|
|
6
7
|
// Radix packages don't support ESM in Node, in some scenarios(e.g. SSR)
|
|
7
8
|
// We have to use namespace import and sanitize it to ensure the interoperablity between ESM and CJS
|
|
@@ -11,13 +12,10 @@ const { DirectionProvider } = sanitizeNamespaceImport(RadixDirection);
|
|
|
11
12
|
*/
|
|
12
13
|
function ThemeProvider({ children, colorMode, direction = 'ltr', nonce, theme, }) {
|
|
13
14
|
const value = React.useMemo(() => ({ theme: createTheme(theme), colorMode }), [theme, colorMode]);
|
|
14
|
-
const { theme: { name, cssText }, } = value;
|
|
15
15
|
return (React.createElement(ThemeContext.Provider, { value: value },
|
|
16
16
|
React.createElement(DirectionProvider, { dir: direction },
|
|
17
|
-
React.createElement("div", { "data-amplify-theme": name, "data-amplify-color-mode": colorMode, dir: direction }, children),
|
|
18
|
-
|
|
19
|
-
// eslint-disable-next-line react/no-danger
|
|
20
|
-
dangerouslySetInnerHTML: { __html: cssText }, nonce: nonce })))));
|
|
17
|
+
React.createElement("div", { "data-amplify-theme": value.theme.name, "data-amplify-color-mode": colorMode, dir: direction }, children),
|
|
18
|
+
theme ? React.createElement(ThemeStyle, { theme: value.theme, nonce: nonce }) : null)));
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export { ThemeProvider };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { primitiveWithForwardRef } from '../../primitives/utils/primitiveWithForwardRef.mjs';
|
|
3
|
+
|
|
4
|
+
const ThemeStylePrimitive = ({ theme, nonce, ...rest }, ref) => {
|
|
5
|
+
if (!theme)
|
|
6
|
+
return null;
|
|
7
|
+
const { name, cssText } = theme;
|
|
8
|
+
/*
|
|
9
|
+
Only inject theme CSS variables if given a theme.
|
|
10
|
+
The CSS file users import already has the default theme variables in it.
|
|
11
|
+
This will allow users to use the provider and theme with CSS variables
|
|
12
|
+
without having to worry about specificity issues because this stylesheet
|
|
13
|
+
will likely come after a user's defined CSS.
|
|
14
|
+
|
|
15
|
+
Q: Why are we using dangerouslySetInnerHTML?
|
|
16
|
+
A: We need to directly inject the theme's CSS string into the <style> tag without typical HTML escaping.
|
|
17
|
+
For example, JSX would escape characters meaningful in CSS such as ', ", < and >, thus breaking the CSS.
|
|
18
|
+
Q: Why not use a sanitization library such as DOMPurify?
|
|
19
|
+
A: For our use case, we specifically want to purify CSS text, *not* HTML.
|
|
20
|
+
DOMPurify, as well as any other HTML sanitization library, would escape/encode meaningful CSS characters
|
|
21
|
+
and break our CSS in the same way that JSX would.
|
|
22
|
+
|
|
23
|
+
Q: Are there any security risks in this particular use case?
|
|
24
|
+
A: Anything set inside of a <style> tag is always interpreted as CSS text, *not* HTML.
|
|
25
|
+
Reference: “Restrictions on the content of raw text elements” https://html.spec.whatwg.org/dev/syntax.html#cdata-rcdata-restrictions
|
|
26
|
+
And in our case, we are using dangerouslySetInnerHTML to set CSS text inside of a <style> tag.
|
|
27
|
+
|
|
28
|
+
Thus, it really comes down to the question: Could a malicious user escape from the context of the <style> tag?
|
|
29
|
+
For example, when inserting HTML into the DOM, could someone prematurely close the </style> tag and add a <script> tag?
|
|
30
|
+
e.g., </style><script>alert('hello')</script>
|
|
31
|
+
The answer depends on whether the code is rendered on the client or server side.
|
|
32
|
+
|
|
33
|
+
Client side
|
|
34
|
+
- To prevent XSS inside of the <style> tag, we need to make sure it's not closed prematurely.
|
|
35
|
+
- This is prevented by React because React creates a style DOM node (e.g., React.createElement(‘style’, ...)), and directly sets innerHTML as a string.
|
|
36
|
+
- Even if the string contains a closing </style> tag, it will still be interpreted as CSS text by the browser.
|
|
37
|
+
- Therefore, there is not an XSS vulnerability on the client side.
|
|
38
|
+
|
|
39
|
+
Server side
|
|
40
|
+
- When React code is rendered on the server side (e.g., NextJS), the code is sent to the browser as HTML text.
|
|
41
|
+
- Therefore, it *IS* possible to insert a closing </style> tag and escape the CSS context, which opens an XSS vulnerability.
|
|
42
|
+
|
|
43
|
+
Q: How are we mitigating the potential attack vector?
|
|
44
|
+
A: To fix this potential attack vector on the server side, we need to filter out any closing </style> tags,
|
|
45
|
+
as this the only way to escape from the context of the browser interpreting the text as CSS.
|
|
46
|
+
We also need to catch cases where there is any kind of whitespace character </style[HERE]>, such as tabs, carriage returns, etc:
|
|
47
|
+
</style
|
|
48
|
+
|
|
49
|
+
>
|
|
50
|
+
Therefore, by only rendering CSS text which does not include a closing '</style>' tag,
|
|
51
|
+
we ensure that the browser will correctly interpret all the text as CSS.
|
|
52
|
+
*/
|
|
53
|
+
if (/<\/style/i.test(cssText)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
return (React.createElement("style", { ...rest, ref: ref, id: `amplify-theme-${name}`,
|
|
58
|
+
// eslint-disable-next-line react/no-danger
|
|
59
|
+
dangerouslySetInnerHTML: { __html: cssText }, nonce: nonce }));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* @experimental
|
|
64
|
+
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
|
|
65
|
+
*/
|
|
66
|
+
const ThemeStyle = primitiveWithForwardRef(ThemeStylePrimitive);
|
|
67
|
+
ThemeStyle.displayName = 'ThemeStyle';
|
|
68
|
+
|
|
69
|
+
export { ThemeStyle };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isFunction } from '@aws-amplify/ui';
|
|
2
|
+
|
|
3
|
+
(typeof Symbol !== 'undefined' && isFunction(Symbol.for)
|
|
4
|
+
? Symbol.for('amplify_default')
|
|
5
|
+
: '@@amplify_default');
|
|
6
|
+
const ERROR_SUFFIX = 'error';
|
|
7
|
+
const DESCRIPTION_SUFFIX = 'description';
|
|
8
|
+
|
|
9
|
+
export { DESCRIPTION_SUFFIX, ERROR_SUFFIX };
|
|
@@ -10,7 +10,7 @@ import { IconIndeterminate } from '../Icon/icons/IconIndeterminate.mjs';
|
|
|
10
10
|
import { Input } from '../Input/Input.mjs';
|
|
11
11
|
import { Text } from '../Text/Text.mjs';
|
|
12
12
|
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
14
14
|
import { useStableId } from '../utils/useStableId.mjs';
|
|
15
15
|
import { splitPrimitiveProps } from '../utils/splitPrimitiveProps.mjs';
|
|
16
16
|
import { useFieldset } from '../Fieldset/useFieldset.mjs';
|
|
@@ -57,9 +57,9 @@ const CheckboxPrimitive = ({ checked: controlledChecked, className, defaultCheck
|
|
|
57
57
|
isIndeterminate;
|
|
58
58
|
}
|
|
59
59
|
}, [dataId, isIndeterminate]);
|
|
60
|
-
const buttonTestId =
|
|
61
|
-
const iconTestId =
|
|
62
|
-
const labelTestId =
|
|
60
|
+
const buttonTestId = getUniqueComponentId(testId, ComponentClassName.CheckboxButton);
|
|
61
|
+
const iconTestId = getUniqueComponentId(testId, ComponentClassName.CheckboxIcon);
|
|
62
|
+
const labelTestId = getUniqueComponentId(testId, ComponentClassName.CheckboxLabel);
|
|
63
63
|
const flexClasses = classNames(ComponentClassName.CheckboxButton, classNameModifierByFlag(ComponentClassName.CheckboxButton, 'disabled', shouldBeDisabled), classNameModifierByFlag(ComponentClassName.CheckboxButton, 'error', hasError), classNameModifierByFlag(ComponentClassName.CheckboxButton, 'focused', focused));
|
|
64
64
|
const iconClasses = classNames(ComponentClassName.CheckboxIcon, classNameModifierByFlag(ComponentClassName.CheckboxIcon, 'checked', checked), classNameModifierByFlag(ComponentClassName.CheckboxIcon, 'disabled', shouldBeDisabled), classNameModifierByFlag(ComponentClassName.CheckboxIcon, 'indeterminate', isIndeterminate));
|
|
65
65
|
const iconProps = {
|
|
@@ -5,12 +5,12 @@ import '../Field/FieldClearButton.mjs';
|
|
|
5
5
|
import '../Field/FieldDescription.mjs';
|
|
6
6
|
import { FieldErrorMessage } from '../Field/FieldErrorMessage.mjs';
|
|
7
7
|
import '../Field/Field.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
9
9
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
10
10
|
import { Flex } from '../Flex/Flex.mjs';
|
|
11
11
|
|
|
12
12
|
const CheckboxFieldPrimitive = ({ className, errorMessage, hasError = false, labelHidden = false, labelPosition, testId, size, ...rest }, ref) => {
|
|
13
|
-
const checkboxTestId =
|
|
13
|
+
const checkboxTestId = getUniqueComponentId(testId, ComponentClassName.Checkbox);
|
|
14
14
|
return (React.createElement(Flex, { className: classNames(ComponentClassName.Field, ComponentClassName.CheckboxField, classNameModifier(ComponentClassName.Field, size), className), testId: testId },
|
|
15
15
|
React.createElement(Checkbox, { hasError: hasError, labelHidden: labelHidden, testId: checkboxTestId, labelPosition: labelPosition, ref: ref, ...rest }),
|
|
16
16
|
React.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })));
|
|
@@ -2,11 +2,11 @@ import { classNames, ComponentClassName } from '@aws-amplify/ui';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { View } from '../View/View.mjs';
|
|
4
4
|
import { strHasLength } from '../shared/utils.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
6
6
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
7
7
|
|
|
8
8
|
const HighlightMatchPrimitive = ({ children, className, query, testId, ...rest }, ref) => {
|
|
9
|
-
const matchTestId =
|
|
9
|
+
const matchTestId = getUniqueComponentId(testId, 'match');
|
|
10
10
|
const startIndex = children
|
|
11
11
|
?.toLocaleLowerCase()
|
|
12
12
|
.indexOf(query?.toLocaleLowerCase());
|
|
@@ -8,15 +8,23 @@ import { Fieldset } from '../Fieldset/Fieldset.mjs';
|
|
|
8
8
|
import '../Fieldset/useFieldset.mjs';
|
|
9
9
|
import { Flex } from '../Flex/Flex.mjs';
|
|
10
10
|
import { RadioGroupContext } from './context.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
12
12
|
import { useStableId } from '../utils/useStableId.mjs';
|
|
13
13
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
14
|
+
import { createSpaceSeparatedIds } from '../utils/createSpaceSeparatedIds.mjs';
|
|
15
|
+
import { DESCRIPTION_SUFFIX, ERROR_SUFFIX } from '../../helpers/constants.mjs';
|
|
14
16
|
|
|
15
17
|
const RadioGroupFieldPrimitive = ({ children, className, defaultValue, descriptiveText, errorMessage, hasError = false, id, isDisabled, isRequired, isReadOnly, legend, legendHidden = false, labelPosition, onChange, name, size, testId, value, variation, ...rest }, ref) => {
|
|
16
18
|
const fieldId = useStableId(id);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const stableId = useStableId();
|
|
20
|
+
const descriptionId = descriptiveText
|
|
21
|
+
? getUniqueComponentId(stableId, DESCRIPTION_SUFFIX)
|
|
22
|
+
: undefined;
|
|
23
|
+
const errorId = hasError
|
|
24
|
+
? getUniqueComponentId(stableId, ERROR_SUFFIX)
|
|
25
|
+
: undefined;
|
|
26
|
+
const ariaDescribedBy = createSpaceSeparatedIds([errorId, descriptionId]);
|
|
27
|
+
const radioGroupTestId = getUniqueComponentId(testId, ComponentClassName.RadioGroup);
|
|
20
28
|
const radioGroupContextValue = React.useMemo(() => ({
|
|
21
29
|
currentValue: value,
|
|
22
30
|
defaultValue,
|
|
@@ -44,7 +52,7 @@ const RadioGroupFieldPrimitive = ({ children, className, defaultValue, descripti
|
|
|
44
52
|
React.createElement(FieldDescription, { id: descriptionId, labelHidden: legendHidden, descriptiveText: descriptiveText }),
|
|
45
53
|
React.createElement(Flex, { "aria-describedby": ariaDescribedBy, className: ComponentClassName.RadioGroup, id: fieldId, testId: radioGroupTestId },
|
|
46
54
|
React.createElement(RadioGroupContext.Provider, { value: radioGroupContextValue }, children)),
|
|
47
|
-
React.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })));
|
|
55
|
+
React.createElement(FieldErrorMessage, { id: errorId, hasError: hasError, errorMessage: errorMessage })));
|
|
48
56
|
};
|
|
49
57
|
/**
|
|
50
58
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/radiogroupfield)
|
|
@@ -10,6 +10,9 @@ import { Select } from '../Select/Select.mjs';
|
|
|
10
10
|
import { splitPrimitiveProps } from '../utils/splitPrimitiveProps.mjs';
|
|
11
11
|
import { useStableId } from '../utils/useStableId.mjs';
|
|
12
12
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
13
|
+
import { createSpaceSeparatedIds } from '../utils/createSpaceSeparatedIds.mjs';
|
|
14
|
+
import { DESCRIPTION_SUFFIX, ERROR_SUFFIX } from '../../helpers/constants.mjs';
|
|
15
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
13
16
|
|
|
14
17
|
const selectFieldChildren = ({ children, options, }) => {
|
|
15
18
|
if (children) {
|
|
@@ -24,14 +27,20 @@ const selectFieldChildren = ({ children, options, }) => {
|
|
|
24
27
|
const SelectFieldPrimitive = (props, ref) => {
|
|
25
28
|
const { children, className, descriptiveText, errorMessage, hasError = false, id, label, labelHidden = false, options, size, testId, inputStyles, ..._rest } = props;
|
|
26
29
|
const fieldId = useStableId(id);
|
|
27
|
-
const
|
|
28
|
-
const
|
|
30
|
+
const stableId = useStableId();
|
|
31
|
+
const descriptionId = descriptiveText
|
|
32
|
+
? getUniqueComponentId(stableId, DESCRIPTION_SUFFIX)
|
|
33
|
+
: undefined;
|
|
34
|
+
const errorId = hasError
|
|
35
|
+
? getUniqueComponentId(stableId, ERROR_SUFFIX)
|
|
36
|
+
: undefined;
|
|
37
|
+
const ariaDescribedBy = createSpaceSeparatedIds([errorId, descriptionId]);
|
|
29
38
|
const { styleProps, rest } = splitPrimitiveProps(_rest);
|
|
30
39
|
return (React.createElement(Flex, { className: classNames(ComponentClassName.Field, classNameModifier(ComponentClassName.Field, size), ComponentClassName.SelectField, className), testId: testId, ...styleProps },
|
|
31
40
|
React.createElement(Label, { htmlFor: fieldId, visuallyHidden: labelHidden }, label),
|
|
32
41
|
React.createElement(FieldDescription, { id: descriptionId, labelHidden: labelHidden, descriptiveText: descriptiveText }),
|
|
33
42
|
React.createElement(Select, { "aria-describedby": ariaDescribedBy, hasError: hasError, id: fieldId, ref: ref, size: size, ...rest, ...inputStyles }, selectFieldChildren({ children, options })),
|
|
34
|
-
React.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })));
|
|
43
|
+
React.createElement(FieldErrorMessage, { id: errorId, hasError: hasError, errorMessage: errorMessage })));
|
|
35
44
|
};
|
|
36
45
|
/**
|
|
37
46
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/selectfield)
|
|
@@ -13,6 +13,9 @@ import { View } from '../View/View.mjs';
|
|
|
13
13
|
import { useStableId } from '../utils/useStableId.mjs';
|
|
14
14
|
import { useFieldset } from '../Fieldset/useFieldset.mjs';
|
|
15
15
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
16
|
+
import { createSpaceSeparatedIds } from '../utils/createSpaceSeparatedIds.mjs';
|
|
17
|
+
import { DESCRIPTION_SUFFIX, ERROR_SUFFIX } from '../../helpers/constants.mjs';
|
|
18
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
16
19
|
|
|
17
20
|
// Radix packages don't support ESM in Node, in some scenarios(e.g. SSR)
|
|
18
21
|
// We have to use namespace import and sanitize it to ensure the interoperablity between ESM and CJS
|
|
@@ -24,9 +27,14 @@ const SLIDER_RANGE_TEST_ID = 'slider-range';
|
|
|
24
27
|
const SliderFieldPrimitive = ({ ariaValuetext, className, defaultValue = 0, descriptiveText, emptyTrackColor, errorMessage, filledTrackColor, formatValue, hasError = false, id, isDisabled, isValueHidden = false, label, labelHidden = false, onChange, orientation = 'horizontal', outerEndComponent, outerStartComponent, testId, thumbColor, trackSize, value, size, ..._rest }, ref) => {
|
|
25
28
|
const { isFieldsetDisabled } = useFieldset();
|
|
26
29
|
const fieldId = useStableId(id);
|
|
27
|
-
const
|
|
28
|
-
const descriptionId =
|
|
29
|
-
|
|
30
|
+
const stableId = useStableId();
|
|
31
|
+
const descriptionId = descriptiveText
|
|
32
|
+
? getUniqueComponentId(stableId, DESCRIPTION_SUFFIX)
|
|
33
|
+
: undefined;
|
|
34
|
+
const errorId = hasError
|
|
35
|
+
? getUniqueComponentId(stableId, ERROR_SUFFIX)
|
|
36
|
+
: undefined;
|
|
37
|
+
const ariaDescribedBy = createSpaceSeparatedIds([errorId, descriptionId]);
|
|
30
38
|
const disabled = isFieldsetDisabled ? isFieldsetDisabled : isDisabled;
|
|
31
39
|
const { styleProps, rest } = splitPrimitiveProps(_rest);
|
|
32
40
|
const isControlled = value !== undefined;
|
|
@@ -54,7 +62,7 @@ const SliderFieldPrimitive = ({ ariaValuetext, className, defaultValue = 0, desc
|
|
|
54
62
|
, {
|
|
55
63
|
// Custom classnames will be added to Root below
|
|
56
64
|
className: classNames(ComponentClassName.Field, classNameModifier(ComponentClassName.Field, size), ComponentClassName.SliderField), testId: testId, ...styleProps },
|
|
57
|
-
React.createElement(Label, { className: ComponentClassName.SliderFieldLabel, id:
|
|
65
|
+
React.createElement(Label, { className: ComponentClassName.SliderFieldLabel, id: stableId, testId: SLIDER_LABEL_TEST_ID, visuallyHidden: labelHidden },
|
|
58
66
|
React.createElement(View, { as: "span" }, label),
|
|
59
67
|
!isValueHidden ? renderedValue : null),
|
|
60
68
|
React.createElement(FieldDescription, { id: descriptionId, labelHidden: labelHidden, descriptiveText: descriptiveText }),
|
|
@@ -65,8 +73,8 @@ const SliderFieldPrimitive = ({ ariaValuetext, className, defaultValue = 0, desc
|
|
|
65
73
|
[`${isVertical ? 'width' : 'height'}`]: trackSize,
|
|
66
74
|
} },
|
|
67
75
|
React.createElement(Range, { className: classNames(ComponentClassName.SliderFieldRange, classNameModifier(ComponentClassName.SliderFieldRange, orientation), classNameModifierByFlag(ComponentClassName.SliderFieldRange, 'disabled', disabled)), "data-testid": SLIDER_RANGE_TEST_ID, style: { backgroundColor: String(filledTrackColor) } })),
|
|
68
|
-
React.createElement(Thumb, { "aria-describedby": ariaDescribedBy, "aria-labelledby":
|
|
69
|
-
React.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })));
|
|
76
|
+
React.createElement(Thumb, { "aria-describedby": ariaDescribedBy, "aria-labelledby": stableId, "aria-valuetext": ariaValuetext, className: classNames(ComponentClassName.SliderFieldThumb, classNameModifier(ComponentClassName.SliderFieldThumb, size), classNameModifierByFlag(ComponentClassName.SliderFieldThumb, 'disabled', disabled)), style: { backgroundColor: String(thumbColor) } }))),
|
|
77
|
+
React.createElement(FieldErrorMessage, { id: errorId, hasError: hasError, errorMessage: errorMessage })));
|
|
70
78
|
};
|
|
71
79
|
/**
|
|
72
80
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/sliderfield)
|
|
@@ -21,6 +21,9 @@ import { ComponentText } from '../shared/constants.mjs';
|
|
|
21
21
|
import { splitPrimitiveProps } from '../utils/splitPrimitiveProps.mjs';
|
|
22
22
|
import { useStableId } from '../utils/useStableId.mjs';
|
|
23
23
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
24
|
+
import { createSpaceSeparatedIds } from '../utils/createSpaceSeparatedIds.mjs';
|
|
25
|
+
import { DESCRIPTION_SUFFIX, ERROR_SUFFIX } from '../../helpers/constants.mjs';
|
|
26
|
+
import { getUniqueComponentId } from '../utils/getUniqueComponentId.mjs';
|
|
24
27
|
|
|
25
28
|
const DECREASE_ICON = 'decrease-icon';
|
|
26
29
|
const INCREASE_ICON = 'increase-icon';
|
|
@@ -33,8 +36,14 @@ const StepperFieldPrimitive = (props, ref) => {
|
|
|
33
36
|
// this is only required in useStepper hook but deconstruct here to remove its existence in rest
|
|
34
37
|
value: controlledValue, variation, ..._rest } = props;
|
|
35
38
|
const fieldId = useStableId(id);
|
|
36
|
-
const
|
|
37
|
-
const
|
|
39
|
+
const stableId = useStableId();
|
|
40
|
+
const descriptionId = descriptiveText
|
|
41
|
+
? getUniqueComponentId(stableId, DESCRIPTION_SUFFIX)
|
|
42
|
+
: undefined;
|
|
43
|
+
const errorId = hasError
|
|
44
|
+
? getUniqueComponentId(stableId, ERROR_SUFFIX)
|
|
45
|
+
: undefined;
|
|
46
|
+
const ariaDescribedBy = createSpaceSeparatedIds([errorId, descriptionId]);
|
|
38
47
|
const { styleProps, rest } = splitPrimitiveProps(_rest);
|
|
39
48
|
const icons = useIcons('stepperField');
|
|
40
49
|
const { step, value, inputValue, handleDecrease, handleIncrease, handleOnBlur, handleOnChange, handleOnWheel, setInputValue, shouldDisableDecreaseButton, shouldDisableIncreaseButton, } = useStepper({ ...props, defaultValue, onStepChange });
|
|
@@ -49,7 +58,7 @@ const StepperFieldPrimitive = (props, ref) => {
|
|
|
49
58
|
React.createElement(FieldDescription, { id: descriptionId, labelHidden: labelHidden, descriptiveText: descriptiveText }),
|
|
50
59
|
React.createElement(FieldGroup, { outerStartComponent: React.createElement(FieldGroupIconButton, { "aria-controls": fieldId, ariaLabel: `${decreaseButtonLabel} ${value - step}`, className: classNames(ComponentClassName.StepperFieldButtonDecrease, classNameModifier(ComponentClassName.StepperFieldButtonDecrease, variation), classNameModifierByFlag(ComponentClassName.StepperFieldButtonDecrease, 'disabled', shouldDisableDecreaseButton)), "data-invalid": hasError, isDisabled: shouldDisableDecreaseButton, onClick: handleDecrease, size: size }, icons?.remove ?? React.createElement(IconRemove, { "data-testid": DECREASE_ICON })), outerEndComponent: React.createElement(FieldGroupIconButton, { "aria-controls": fieldId, ariaLabel: `${increaseButtonLabel} ${value + step}`, className: classNames(ComponentClassName.StepperFieldButtonIncrease, classNameModifier(ComponentClassName.StepperFieldButtonIncrease, variation), classNameModifierByFlag(ComponentClassName.StepperFieldButtonIncrease, 'disabled', shouldDisableIncreaseButton)), "data-invalid": hasError, isDisabled: shouldDisableIncreaseButton, onClick: handleIncrease, size: size }, icons?.add ?? React.createElement(IconAdd, { "data-testid": INCREASE_ICON })) },
|
|
51
60
|
React.createElement(Input, { "aria-describedby": ariaDescribedBy, className: ComponentClassName.StepperFieldInput, hasError: hasError, id: fieldId, isDisabled: isDisabled, isReadOnly: isReadOnly, isRequired: isRequired, onBlur: handleOnBlur, onChange: handleOnChange, onWheel: handleOnWheel, ref: ref, size: size, variation: variation, type: "number", value: inputValue, ...inputStyles, ...rest })),
|
|
52
|
-
React.createElement(FieldErrorMessage, { hasError: hasError, errorMessage: errorMessage })));
|
|
61
|
+
React.createElement(FieldErrorMessage, { id: errorId, hasError: hasError, errorMessage: errorMessage })));
|
|
53
62
|
};
|
|
54
63
|
/**
|
|
55
64
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/stepperfield)
|
|
@@ -3,8 +3,10 @@ import { isFunction, classNames, ComponentClassName } from '@aws-amplify/ui';
|
|
|
3
3
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
4
4
|
import { View } from '../View/View.mjs';
|
|
5
5
|
import { TabsContext } from './TabsContext.mjs';
|
|
6
|
+
import { useStableId } from '../utils/useStableId.mjs';
|
|
6
7
|
|
|
7
8
|
const TabsContainerPrimitive = ({ children, defaultValue, className, value: controlledValue, onValueChange, isLazy, ...rest }, ref) => {
|
|
9
|
+
const groupId = useStableId(); // groupId is used to ensure uniqueness between Tab Groups in IDs
|
|
8
10
|
const isControlled = controlledValue !== undefined;
|
|
9
11
|
const [localValue, setLocalValue] = React.useState(() => isControlled ? controlledValue : defaultValue);
|
|
10
12
|
const activeTab = isControlled ? controlledValue : localValue ?? '';
|
|
@@ -21,8 +23,9 @@ const TabsContainerPrimitive = ({ children, defaultValue, className, value: cont
|
|
|
21
23
|
activeTab,
|
|
22
24
|
isLazy,
|
|
23
25
|
setActiveTab,
|
|
26
|
+
groupId,
|
|
24
27
|
};
|
|
25
|
-
}, [activeTab, setActiveTab, isLazy]);
|
|
28
|
+
}, [activeTab, setActiveTab, isLazy, groupId]);
|
|
26
29
|
return (React.createElement(TabsContext.Provider, { value: _value },
|
|
27
30
|
React.createElement(View, { ...rest, ref: ref, className: classNames(className, ComponentClassName.Tabs) }, children)));
|
|
28
31
|
};
|
|
@@ -3,9 +3,14 @@ import { classNames, ComponentClassName, classNameModifierByFlag, isTypedFunctio
|
|
|
3
3
|
import { View } from '../View/View.mjs';
|
|
4
4
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
5
5
|
import { TabsContext } from './TabsContext.mjs';
|
|
6
|
+
import { WHITESPACE_VALUE } from './constants.mjs';
|
|
6
7
|
|
|
7
8
|
const TabsItemPrimitive = ({ className, value, children, onClick, as = 'button', role = 'tab', ...rest }, ref) => {
|
|
8
|
-
const { activeTab, setActiveTab } = React.useContext(TabsContext);
|
|
9
|
+
const { activeTab, setActiveTab, groupId } = React.useContext(TabsContext);
|
|
10
|
+
let idValue = value;
|
|
11
|
+
if (typeof idValue === 'string') {
|
|
12
|
+
idValue = idValue.replace(' ', WHITESPACE_VALUE);
|
|
13
|
+
}
|
|
9
14
|
const isActive = activeTab === value;
|
|
10
15
|
const handleOnClick = (e) => {
|
|
11
16
|
if (isTypedFunction(onClick)) {
|
|
@@ -13,7 +18,7 @@ const TabsItemPrimitive = ({ className, value, children, onClick, as = 'button',
|
|
|
13
18
|
}
|
|
14
19
|
setActiveTab(value);
|
|
15
20
|
};
|
|
16
|
-
return (React.createElement(View, { ...rest, role: role, as: as, id: `${
|
|
21
|
+
return (React.createElement(View, { ...rest, role: role, as: as, id: `${groupId}-tab-${idValue}`, "aria-selected": isActive, "aria-controls": `${groupId}-panel-${idValue}`, tabIndex: !isActive ? -1 : undefined, className: classNames(ComponentClassName.TabsItem, classNameModifierByFlag(ComponentClassName.TabsItem, 'active', activeTab === value), className), ref: ref, onClick: handleOnClick }, children));
|
|
17
22
|
};
|
|
18
23
|
/**
|
|
19
24
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/tabs)
|
|
@@ -3,12 +3,17 @@ import { classNames, ComponentClassName, classNameModifierByFlag } from '@aws-am
|
|
|
3
3
|
import { View } from '../View/View.mjs';
|
|
4
4
|
import { primitiveWithForwardRef } from '../utils/primitiveWithForwardRef.mjs';
|
|
5
5
|
import { TabsContext } from './TabsContext.mjs';
|
|
6
|
+
import { WHITESPACE_VALUE } from './constants.mjs';
|
|
6
7
|
|
|
7
8
|
const TabPanelPrimitive = ({ className, value, children, role = 'tabpanel', ...rest }, ref) => {
|
|
8
|
-
const { activeTab, isLazy } = React.useContext(TabsContext);
|
|
9
|
+
const { activeTab, isLazy, groupId } = React.useContext(TabsContext);
|
|
9
10
|
if (isLazy && activeTab !== value)
|
|
10
11
|
return null;
|
|
11
|
-
|
|
12
|
+
let idValue = value;
|
|
13
|
+
if (typeof idValue === 'string') {
|
|
14
|
+
idValue = idValue.replace(' ', WHITESPACE_VALUE);
|
|
15
|
+
}
|
|
16
|
+
return (React.createElement(View, { ...rest, role: role, id: `${groupId}-panel-${idValue}`, "aria-labelledby": `${groupId}-tab-${idValue}`, className: classNames(ComponentClassName.TabsPanel, classNameModifierByFlag(ComponentClassName.TabsPanel, 'active', activeTab === value), className), ref: ref }, children));
|
|
12
17
|
};
|
|
13
18
|
/**
|
|
14
19
|
* [📖 Docs](https://ui.docs.amplify.aws/react/components/tabs)
|