diff --git a/src/annotation-context/__tests__/annotation-context.test.tsx b/src/annotation-context/__tests__/annotation-context.test.tsx
index 7c6a1e0ac9..b0c003a3a4 100644
--- a/src/annotation-context/__tests__/annotation-context.test.tsx
+++ b/src/annotation-context/__tests__/annotation-context.test.tsx
@@ -261,6 +261,25 @@ test('trigger should have aria-expanded depending on open state', () => {
expect(hotspot.findTrigger().getElement().getAttribute('aria-expanded')).toBe('false');
});
+test('propagates one-theme class to the portaled annotation when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+
+ try {
+ const { wrapper } = renderAnnotationContext();
+ expect(wrapper.findAnnotation()!.findContent().getElement().closest('.awsui-one-theme')).not.toBeNull();
+ } finally {
+ themeRoot.remove();
+ }
+});
+
+test('does not stamp one-theme class on the portaled annotation when one theme is inactive', () => {
+ const { wrapper } = renderAnnotationContext();
+
+ expect(wrapper.findAnnotation()!.findContent().getElement().closest('.awsui-one-theme')).toBeNull();
+});
+
test('annotation should have be labeled by header and step counter', () => {
const { wrapper } = renderAnnotationContext(
<>
diff --git a/src/annotation-context/annotation/open-annotation.tsx b/src/annotation-context/annotation/open-annotation.tsx
index ab91c711c6..3a5bcf11dd 100644
--- a/src/annotation-context/annotation/open-annotation.tsx
+++ b/src/annotation-context/annotation/open-annotation.tsx
@@ -5,6 +5,7 @@ import React, { useRef } from 'react';
import { Portal } from '@cloudscape-design/component-toolkit/internal';
import { HotspotProps } from '../../hotspot/interfaces';
+import { useOneThemePortalClass } from '../../internal/hooks/use-portal-mode-classes';
import { AnnotationContextProps } from '../interfaces';
import { AnnotationPopover } from './annotation-popover';
import AnnotationTrigger from './annotation-trigger';
@@ -61,6 +62,7 @@ export function OpenAnnotation({
i18nStrings,
}: AnnotationProps) {
const trackRef = useRef(null);
+ const portalClasses = useOneThemePortalClass();
return (
<>
@@ -73,24 +75,26 @@ export function OpenAnnotation({
taskLocalStepIndex={taskLocalStepIndex}
/>
-
+
+
+
>
);
diff --git a/src/drawer/__tests__/drawer-position-and-placement.test.tsx b/src/drawer/__tests__/drawer-position-and-placement.test.tsx
index ee97fa0d8e..5104eb4213 100644
--- a/src/drawer/__tests__/drawer-position-and-placement.test.tsx
+++ b/src/drawer/__tests__/drawer-position-and-placement.test.tsx
@@ -100,6 +100,25 @@ describe('position=fixed', () => {
);
expect(el).toHaveStyle({ insetInlineStart: '8px', insetBlockStart: '4px', insetBlockEnd: '12px' });
});
+
+ test('propagates one-theme class to the fixed drawer when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+
+ try {
+ const el = getDrawerElement();
+ expect(el).toHaveClass('awsui-one-theme');
+ } finally {
+ themeRoot.remove();
+ }
+ });
+
+ test('does not stamp one-theme class on the fixed drawer when one theme is inactive', () => {
+ const el = getDrawerElement();
+
+ expect(el).not.toHaveClass('awsui-one-theme');
+ });
});
describe('position=sticky', () => {
diff --git a/src/drawer/implementation.tsx b/src/drawer/implementation.tsx
index 27a8e49230..f36f1dae4d 100644
--- a/src/drawer/implementation.tsx
+++ b/src/drawer/implementation.tsx
@@ -17,6 +17,7 @@ import { getAllFocusables, isFocusable } from '../internal/components/focus-lock
import { fireNonCancelableEvent } from '../internal/events';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update';
+import { useOneThemePortalClass } from '../internal/hooks/use-portal-mode-classes';
import { createWidgetizedComponent } from '../internal/widgets';
import InternalLiveRegion from '../live-region/internal';
import InternalStatusIndicator from '../status-indicator/internal';
@@ -63,6 +64,7 @@ export function DrawerImplementation({
const returnFocusTargetRef = useRef(null);
const baseProps = getBaseProps(restProps);
+ const portalClasses = useOneThemePortalClass();
const isToolbar = useAppLayoutToolbarDesignEnabled();
const i18n = useInternalI18n('drawer');
const positionStyles = getPositionStyles({ position, placement, offset, stickyOffset, zIndex });
@@ -84,7 +86,8 @@ export function DrawerImplementation({
isToolbar && styles['with-toolbar'],
!!footer && styles['with-footer'],
closeAction && !hideCloseAction && styles['has-close-action'],
- positionStyles.className
+ positionStyles.className,
+ position === 'fixed' && portalClasses
),
};
diff --git a/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx b/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
index 5b28eb99d1..d58cd95dae 100644
--- a/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
+++ b/src/internal/components/drag-handle-wrapper/__tests__/portal-overlay.test.tsx
@@ -109,3 +109,40 @@ test('resumes position updates when enabled after being disabled', async () => {
expect(portalOverlay.style.height).toBe('20px');
});
});
+
+test('propagates one-theme class to the portal overlay when one theme is active', async () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+ const mockRef = createMockRef();
+
+ try {
+ render(
+
+ Overlay
+
+ );
+
+ const portalOverlay = document.querySelector(`.${styles['portal-overlay']}`)!;
+ await waitFor(() => {
+ expect(portalOverlay).toHaveClass('awsui-one-theme');
+ });
+ } finally {
+ themeRoot.remove();
+ }
+});
+
+test('does not stamp one-theme class on the portal overlay when one theme is inactive', async () => {
+ const mockRef = createMockRef();
+
+ render(
+
+ Overlay
+
+ );
+
+ const portalOverlay = document.querySelector(`.${styles['portal-overlay']}`)!;
+ await waitFor(() => {
+ expect(portalOverlay).not.toHaveClass('awsui-one-theme');
+ });
+});
diff --git a/src/internal/components/drag-handle-wrapper/portal-overlay.tsx b/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
index ea6da98c4d..a0c774b716 100644
--- a/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
+++ b/src/internal/components/drag-handle-wrapper/portal-overlay.tsx
@@ -10,6 +10,8 @@ import {
Portal,
} from '@cloudscape-design/component-toolkit/internal';
+import { useOneThemePortalClass } from '../../hooks/use-portal-mode-classes';
+
import styles from './styles.css.js';
export default function PortalOverlay({
@@ -25,6 +27,7 @@ export default function PortalOverlay({
}) {
const ref = useRef(null);
const [container, setContainer] = useState(null);
+ const portalClasses = useOneThemePortalClass();
useLayoutEffect(() => {
if (track.current) {
@@ -90,7 +93,7 @@ export default function PortalOverlay({
{children}
diff --git a/src/internal/components/sortable-area/__tests__/portal-mode.test.tsx b/src/internal/components/sortable-area/__tests__/portal-mode.test.tsx
new file mode 100644
index 0000000000..edf9d6d505
--- /dev/null
+++ b/src/internal/components/sortable-area/__tests__/portal-mode.test.tsx
@@ -0,0 +1,104 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import SortableArea, { SortableAreaProps } from '../../../../../lib/components/internal/components/sortable-area';
+import useDragAndDropReorder from '../../../../../lib/components/internal/components/sortable-area/use-drag-and-drop-reorder';
+
+import styles from '../../../../../lib/components/internal/components/sortable-area/styles.css.js';
+
+jest.mock('@dnd-kit/core', () => ({
+ ...jest.requireActual('@dnd-kit/core'),
+ DndContext: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ DragOverlay: ({ children, className }: { children: React.ReactNode; className?: string }) => (
+ {children}
+ ),
+}));
+
+jest.mock('@dnd-kit/sortable', () => ({
+ ...jest.requireActual('@dnd-kit/sortable'),
+ SortableContext: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ useSortable: () => ({
+ isDragging: false,
+ isSorting: false,
+ listeners: {},
+ setNodeRef: jest.fn(),
+ transform: null,
+ attributes: {
+ 'aria-describedby': 'drag-handle',
+ 'aria-disabled': false,
+ },
+ }),
+}));
+
+jest.mock('../../../../../lib/components/internal/components/sortable-area/use-drag-and-drop-reorder');
+
+interface Item {
+ id: string;
+ label: string;
+}
+
+const items: readonly Item[] = [
+ { id: '1', label: 'First' },
+ { id: '2', label: 'Second' },
+];
+const itemDefinition: SortableAreaProps.ItemDefinition- = { id: item => item.id, label: item => item.label };
+const mockedUseDragAndDropReorder = useDragAndDropReorder as jest.MockedFunction;
+
+beforeEach(() => {
+ mockedUseDragAndDropReorder.mockReturnValue({
+ activeItemId: '1',
+ setActiveItemId: jest.fn(),
+ collisionDetection: jest.fn(),
+ coordinateGetter: jest.fn(),
+ handleKeyDown: jest.fn(),
+ sensors: [],
+ isKeyboard: { current: false },
+ });
+});
+
+test('propagates one-theme class to the drag overlay when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+
+ try {
+ render(
+ {}}
+ renderItem={({ item, className, ref }) => (
+
+ {item.label}
+
+ )}
+ i18nStrings={{}}
+ />
+ );
+
+ expect(document.querySelector(`.${styles['drag-overlay']}`)).toHaveClass('awsui-one-theme');
+ } finally {
+ themeRoot.remove();
+ }
+});
+
+test('does not stamp one-theme class on the drag overlay when one theme is inactive', () => {
+ render(
+ {}}
+ renderItem={({ item, className, ref }) => (
+
+ {item.label}
+
+ )}
+ i18nStrings={{}}
+ />
+ );
+
+ expect(document.querySelector(`.${styles['drag-overlay']}`)).not.toHaveClass('awsui-one-theme');
+});
diff --git a/src/internal/components/sortable-area/index.tsx b/src/internal/components/sortable-area/index.tsx
index 77dc592d4a..4ee78b04a1 100644
--- a/src/internal/components/sortable-area/index.tsx
+++ b/src/internal/components/sortable-area/index.tsx
@@ -10,6 +10,7 @@ import clsx from 'clsx';
import { Portal } from '@cloudscape-design/component-toolkit/internal';
import { fireNonCancelableEvent } from '../../events';
+import { useOneThemePortalClass } from '../../hooks/use-portal-mode-classes';
import { joinStrings } from '../../utils/strings';
import { SortableAreaProps } from './interfaces';
import { EventName } from './keyboard-sensor/utilities/events';
@@ -37,6 +38,7 @@ export default function SortableArea- ({
const isDragging = activeItemId !== null;
const announcements = useLiveAnnouncements({ items, itemDefinition, isDragging, ...i18nStrings });
const portalContainer = usePortalContainer();
+ const portalClasses = useOneThemePortalClass();
return (
({
{/* Make sure that the drag overlay is above the modal by assigning the z-index as inline style
so that it prevails over dnd-kit's inline z-index of 999 */}
{
expect(keydownEvent.stopPropagation).toHaveBeenCalled();
expect(onDismiss).toHaveBeenCalled();
});
+
+ it('propagates one-theme class to the portaled tooltip when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+ const trackRef = React.createRef();
+
+ try {
+ render(
+ <>
+
+ {}} />
+ >
+ );
+
+ expect(createWrapper().findByClassName(tooltipStyles.root)!.getElement()).toHaveClass('awsui-one-theme');
+ } finally {
+ themeRoot.remove();
+ }
+ });
+
+ it('does not stamp one-theme class on the portaled tooltip when one theme is inactive', () => {
+ const trackRef = React.createRef();
+
+ render(
+ <>
+
+ {}} />
+ >
+ );
+
+ expect(createWrapper().findByClassName(tooltipStyles.root)!.getElement()).not.toHaveClass('awsui-one-theme');
+ });
});
diff --git a/src/internal/components/tooltip/index.tsx b/src/internal/components/tooltip/index.tsx
index 048d832cd1..0dd3bc0b64 100644
--- a/src/internal/components/tooltip/index.tsx
+++ b/src/internal/components/tooltip/index.tsx
@@ -1,6 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect } from 'react';
+import clsx from 'clsx';
import { Portal } from '@cloudscape-design/component-toolkit/internal';
@@ -8,6 +9,7 @@ import PopoverArrow from '../../../popover/arrow';
import PopoverBody from '../../../popover/body';
import PopoverContainer from '../../../popover/container';
import { PopoverProps } from '../../../popover/interfaces';
+import { useOneThemePortalClass } from '../../hooks/use-portal-mode-classes';
import { Transition } from '../transition';
import testUtilsStyles from '../../../tooltip/test-classes/styles.css.js';
@@ -41,6 +43,8 @@ export default function Tooltip({
hideOnOverscroll,
onDismiss,
}: TooltipProps) {
+ const portalClasses = useOneThemePortalClass();
+
if (!trackKey && (typeof value === 'string' || typeof value === 'number')) {
trackKey = value;
}
@@ -71,7 +75,11 @@ export default function Tooltip({
return (
-
+
{() => (
{
expect(onDismissMock).toHaveBeenCalledWith(expect.objectContaining({ detail: { method: 'click-outside' } }));
expect(wrapper.findContent()).toBeFalsy();
});
+
+ test('should propagate one-theme class to the portaled feature prompt when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+
+ try {
+ const { getByTestId, wrapper } = renderComponent();
+ getByTestId('trigger-button').click();
+
+ expect(wrapper.findContent()!.getElement().closest('.awsui-one-theme')).not.toBeNull();
+ } finally {
+ themeRoot.remove();
+ }
+ });
+
+ test('should not stamp one-theme class on the portaled feature prompt when one theme is inactive', () => {
+ const { getByTestId, wrapper } = renderComponent();
+
+ getByTestId('trigger-button').click();
+
+ expect(wrapper.findContent()!.getElement().closest('.awsui-one-theme')).toBeNull();
+ });
});
diff --git a/src/internal/do-not-use/feature-prompt/internal.tsx b/src/internal/do-not-use/feature-prompt/internal.tsx
index 28f532369e..5a6356e173 100644
--- a/src/internal/do-not-use/feature-prompt/internal.tsx
+++ b/src/internal/do-not-use/feature-prompt/internal.tsx
@@ -13,6 +13,7 @@ import { getBaseProps } from '../../base-component';
import ResetContextsForModal from '../../context/reset-contexts-for-modal';
import { fireNonCancelableEvent } from '../../events';
import { InternalBaseComponentProps } from '../../hooks/use-base-component';
+import { useOneThemePortalClass } from '../../hooks/use-portal-mode-classes';
import { nodeBelongs } from '../../utils/node-belongs';
import { FeaturePromptProps } from './interfaces';
@@ -42,6 +43,7 @@ function InternalFeaturePrompt(
const [show, setShow] = useState(false);
const popoverBodyRef = useRef(null);
+ const portalClasses = useOneThemePortalClass();
useImperativeHandle(ref, () => ({
dismiss: () => {
@@ -89,40 +91,42 @@ function InternalFeaturePrompt(
{show && (
-
- }
- zIndex={7000}
- renderWithPortal={true}
- >
- {
- setShow(false);
- fireNonCancelableEvent(onDismiss, { method });
- }}
- onBlur={event => {
- if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) {
- setShow(false);
- fireNonCancelableEvent(onDismiss, { method: 'blur' });
- }
- }}
- variant="feature-prompt"
- overflowVisible="content"
+
+
+ }
+ zIndex={7000}
+ renderWithPortal={true}
>
- {content}
-
-
-
+ {
+ setShow(false);
+ fireNonCancelableEvent(onDismiss, { method });
+ }}
+ onBlur={event => {
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) {
+ setShow(false);
+ fireNonCancelableEvent(onDismiss, { method: 'blur' });
+ }
+ }}
+ variant="feature-prompt"
+ overflowVisible="content"
+ >
+ {content}
+
+
+
+
)}
diff --git a/src/internal/hooks/use-portal-mode-classes/index.ts b/src/internal/hooks/use-portal-mode-classes/index.ts
index 682aa6b8a3..4cca0eea2a 100644
--- a/src/internal/hooks/use-portal-mode-classes/index.ts
+++ b/src/internal/hooks/use-portal-mode-classes/index.ts
@@ -24,3 +24,7 @@ export function usePortalModeClasses(ref: React.RefObject, options?
[`awsui-context-${context}`]: context && !options?.resetVisualContext,
});
}
+
+export function useOneThemePortalClass() {
+ return useOneTheme() ? 'awsui-one-theme' : '';
+}
diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx
index 603c522502..6f7df8d198 100644
--- a/src/modal/__tests__/modal.test.tsx
+++ b/src/modal/__tests__/modal.test.tsx
@@ -96,6 +96,34 @@ describe('Modal component', () => {
wrapper.findDismissButton().click();
expect(onDismissSpy).toHaveBeenCalled();
});
+
+ it('propagates one-theme class to the portaled root when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+ const modalRoot = document.createElement('div');
+ document.body.appendChild(modalRoot);
+
+ try {
+ render();
+ expect(createWrapper(modalRoot).findModal()!.getElement()).toHaveClass('awsui-one-theme');
+ } finally {
+ modalRoot.remove();
+ themeRoot.remove();
+ }
+ });
+
+ it('does not stamp one-theme class on the portaled root when one theme is inactive', () => {
+ const modalRoot = document.createElement('div');
+ document.body.appendChild(modalRoot);
+
+ try {
+ render();
+ expect(createWrapper(modalRoot).findModal()!.getElement()).not.toHaveClass('awsui-one-theme');
+ } finally {
+ modalRoot.remove();
+ }
+ });
});
describe('no paddings', () => {
diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx
index 16761199a1..b485a7f552 100644
--- a/src/modal/internal.tsx
+++ b/src/modal/internal.tsx
@@ -27,6 +27,7 @@ import { fireNonCancelableEvent } from '../internal/events';
import { useContainerBreakpoints } from '../internal/hooks/container-queries';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { useIntersectionObserver } from '../internal/hooks/use-intersection-observer';
+import { useOneThemePortalClass } from '../internal/hooks/use-portal-mode-classes';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { KeyCode } from '../internal/keycode';
import { SomeRequired } from '../internal/types';
@@ -78,14 +79,18 @@ type InternalModalProps = SomeRequired &
};
export default function InternalModal({ modalRoot, getModalRoot, removeModalRoot, ...rest }: InternalModalProps) {
+ const portalClasses = useOneThemePortalClass();
+
return (
-
+
);
}
-type PortaledModalProps = Omit;
+type PortaledModalProps = Omit & {
+ portalClasses: string;
+};
// Separate component to prevent the Portal from getting in the way of refs, as it needs extra cycles to render the inner components.
// useContainerQuery needs its targeted element to exist on the first render in order to work properly.
@@ -108,6 +113,7 @@ function PortaledModal({
__subStepRef,
__subStepFunnelProps,
referrerId,
+ portalClasses,
...rest
}: PortaledModalProps) {
const instanceUniqueId = useUniqueId();
@@ -243,6 +249,7 @@ function PortaledModal({
{...__funnelProps}
{...__funnelStepProps}
className={clsx(
+ portalClasses,
styles.root,
{ [styles.hidden]: !visible },
baseProps.className,
diff --git a/src/tooltip/__tests__/tooltip.test.tsx b/src/tooltip/__tests__/tooltip.test.tsx
index aef2dcd92b..d854a64bb3 100644
--- a/src/tooltip/__tests__/tooltip.test.tsx
+++ b/src/tooltip/__tests__/tooltip.test.tsx
@@ -110,6 +110,39 @@ describe('Tooltip', () => {
expect(wrapper).not.toBeNull();
});
+ it('propagates one-theme class to the portaled tooltip when one theme is active', () => {
+ const themeRoot = document.createElement('div');
+ themeRoot.className = 'awsui-one-theme';
+ document.body.appendChild(themeRoot);
+ const trackRef = React.createRef();
+
+ try {
+ render(
+ <>
+
+ trackRef.current} />
+ >
+ );
+
+ expect(createWrapper().findTooltip()!.getElement()).toHaveClass('awsui-one-theme');
+ } finally {
+ themeRoot.remove();
+ }
+ });
+
+ it('does not stamp one-theme class on the portaled tooltip when one theme is inactive', () => {
+ const trackRef = React.createRef();
+
+ render(
+ <>
+
+ trackRef.current} />
+ >
+ );
+
+ expect(createWrapper().findTooltip()!.getElement()).not.toHaveClass('awsui-one-theme');
+ });
+
it('updates tracked element when getTrack changes', () => {
const element1 = document.createElement('div');
const element2 = document.createElement('div');
diff --git a/src/tooltip/internal.tsx b/src/tooltip/internal.tsx
index 7556612d1d..cdee3bdf0a 100644
--- a/src/tooltip/internal.tsx
+++ b/src/tooltip/internal.tsx
@@ -9,6 +9,7 @@ import { getBaseProps } from '../internal/base-component';
import { Transition } from '../internal/components/transition';
import { fireNonCancelableEvent } from '../internal/events';
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
+import { useOneThemePortalClass } from '../internal/hooks/use-portal-mode-classes';
import PopoverArrow from '../popover/arrow';
import PopoverBody from '../popover/body';
import PopoverContainer from '../popover/container';
@@ -36,6 +37,7 @@ export default function InternalTooltip({
}: InternalTooltipComponentProps) {
const baseProps = getBaseProps(restProps);
const trackRef = React.useRef(null);
+ const portalClasses = useOneThemePortalClass();
// Update the ref with the current tracked element
React.useEffect(() => {
@@ -71,7 +73,7 @@ export default function InternalTooltip({