diff --git a/pages/popover/style-api-v2.page.tsx b/pages/popover/style-api-v2.page.tsx
new file mode 100644
index 0000000000..ee43cf2e0c
--- /dev/null
+++ b/pages/popover/style-api-v2.page.tsx
@@ -0,0 +1,64 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React, { useContext } from 'react';
+
+import { Checkbox, KeyValuePairs, Popover } from '~components';
+
+import AppContext, { AppContextType } from '../app/app-context';
+import { SimplePage } from '../app/templates';
+
+import styles from './style-api-v2.scss';
+
+const brand = { popover: styles['popover-brand'], dismissButton: styles['popover-brand-dismiss'] };
+
+type PageContext = React.Context<
+ AppContextType<{
+ renderWithPortal: boolean;
+ }>
+>;
+
+export default function () {
+ const {
+ urlParams: { renderWithPortal = true },
+ setUrlParams,
+ } = useContext(AppContext as PageContext);
+ const shared = { header: 'Popover header', content: 'Popover content', renderWithPortal };
+ return (
+ setUrlParams({ renderWithPortal: detail.checked })}
+ >
+ renderWithPortal
+
+ }
+ >
+
+ Open popover
+
+ ),
+ },
+ {
+ type: 'pair',
+ label: 'Borderless',
+ value: (
+
+ Open popover
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
diff --git a/pages/popover/style-api-v2.scss b/pages/popover/style-api-v2.scss
new file mode 100644
index 0000000000..50293a238a
--- /dev/null
+++ b/pages/popover/style-api-v2.scss
@@ -0,0 +1,29 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../style-api/palette' as palette;
+
+.popover-brand {
+ --awsui-style-color: #{palette.$genai-text};
+ --awsui-style-background: #{palette.$genai-surface};
+ --awsui-style-border-color: #{palette.$genai};
+ --awsui-style-border-width: 3px;
+ --awsui-style-border-radius: 8px;
+}
+
+.popover-borderless {
+ --awsui-style-background: #{palette.$subtle};
+ --awsui-style-border-width: 0px;
+ --awsui-style-border-radius: 4px;
+}
+
+.popover-brand-dismiss {
+ --awsui-style-color: #{palette.$genai};
+ --awsui-style-focus-ring-color: #{palette.$genai};
+
+ &:hover {
+ --awsui-style-color: #{palette.$genai-hover};
+ }
+}
diff --git a/pages/tooltip/style-api-v2.page.tsx b/pages/tooltip/style-api-v2.page.tsx
new file mode 100644
index 0000000000..f1823835bd
--- /dev/null
+++ b/pages/tooltip/style-api-v2.page.tsx
@@ -0,0 +1,36 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import React, { useRef, useState } from 'react';
+
+import { Button, Tooltip } from '~components';
+
+import { SimplePage } from '../app/templates';
+
+import styles from './style-api-v2.scss';
+
+export default function () {
+ const triggerRef = useRef(null);
+ const [open, setOpen] = useState(false);
+ return (
+
+
+ setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
+ onFocus={() => setOpen(true)}
+ onBlur={() => setOpen(false)}
+ >
+
+ {open && (
+ triggerRef.current}
+ onEscape={() => setOpen(false)}
+ {...{ styleClassNames: { tooltip: styles['tooltip-inverted'] } }}
+ />
+ )}
+
+
+ );
+}
diff --git a/pages/tooltip/style-api-v2.scss b/pages/tooltip/style-api-v2.scss
new file mode 100644
index 0000000000..6abb52a76b
--- /dev/null
+++ b/pages/tooltip/style-api-v2.scss
@@ -0,0 +1,13 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../style-api/palette' as palette;
+
+.tooltip-inverted {
+ --awsui-style-color: #{palette.$text-on-accent};
+ --awsui-style-background: #{palette.$accent};
+ --awsui-style-border-color: #{palette.$accent};
+ --awsui-style-border-radius: 2px;
+}
diff --git a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
index 0ec1499e58..54e5e0a134 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/style-docs.test.ts.snap
@@ -174,6 +174,36 @@ exports[`Style API docs definition for link matches the snapshot: link 1`] = `
}
`;
+exports[`Style API docs definition for popover matches the snapshot: popover 1`] = `
+{
+ "slots": [
+ {
+ "name": "popover",
+ "properties": [],
+ "propertyDescriptions": {},
+ "tokenDescriptions": {
+ "background": "Background of the surface and arrow.",
+ "border-color": "Border color of the surface and arrow.",
+ "border-width": "Border width of the surface and arrow.",
+ },
+ "tokens": [
+ "background",
+ "border-color",
+ "border-width",
+ "border-radius",
+ ],
+ },
+ {
+ "forwardsTo": {
+ "component": "button",
+ "slot": "root",
+ },
+ "name": "dismissButton",
+ },
+ ],
+}
+`;
+
exports[`Style API docs definition for radio-button matches the snapshot: radio-button 1`] = `
{
"slots": [
@@ -237,6 +267,29 @@ exports[`Style API docs definition for toggle matches the snapshot: toggle 1`] =
}
`;
+exports[`Style API docs definition for tooltip matches the snapshot: tooltip 1`] = `
+{
+ "slots": [
+ {
+ "name": "tooltip",
+ "properties": [],
+ "propertyDescriptions": {},
+ "tokenDescriptions": {
+ "background": "Background of the surface and arrow.",
+ "border-color": "Border color of the surface and arrow.",
+ "border-width": "Border width of the surface and arrow.",
+ },
+ "tokens": [
+ "background",
+ "border-color",
+ "border-width",
+ "border-radius",
+ ],
+ },
+ ],
+}
+`;
+
exports[`Style API docs list of components with a Style API doc matches the snapshot 1`] = `
[
"alert",
@@ -245,7 +298,9 @@ exports[`Style API docs list of components with a Style API doc matches the snap
"checkbox",
"dropdown",
"link",
+ "popover",
"radio-button",
"toggle",
+ "tooltip",
]
`;
diff --git a/src/internal/styles/style-api.scss b/src/internal/styles/style-api.scss
index a245639edf..8e3a8eddfd 100644
--- a/src/internal/styles/style-api.scss
+++ b/src/internal/styles/style-api.scss
@@ -51,9 +51,9 @@ $_focus-ring-carriers: toolkit.resolve($focus-ring, carrier);
}
}
-@mixin docs($name, $map) {
+@mixin docs($name, $tokens: null, $properties: null, $descriptions: null) {
@if enabled() {
- @include toolkit.docs($name, $map);
+ @include toolkit.docs($name, $tokens, $properties, $descriptions);
}
}
diff --git a/src/popover/__tests__/style-api.test.tsx b/src/popover/__tests__/style-api.test.tsx
new file mode 100644
index 0000000000..f324417ae9
--- /dev/null
+++ b/src/popover/__tests__/style-api.test.tsx
@@ -0,0 +1,40 @@
+// 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 Popover from '../../../lib/components/popover';
+import createWrapper from '../../../lib/components/test-utils/dom';
+
+import styles from '../../../lib/components/popover/styles.css.js';
+
+function renderOpenPopover(props: Record) {
+ const { container } = render();
+ const wrapper = createWrapper(container).findPopover()!;
+ wrapper.findTrigger().click();
+ return wrapper;
+}
+
+describe('Popover Style API v2', () => {
+ test('applies styleClassNames to the popover container', () => {
+ const wrapper = renderOpenPopover({ styleClassNames: { popover: 'a', unknown: 'b' } });
+ expect(wrapper.findByClassName(styles.container)!.getElement()).toHaveClass('a');
+ expect(wrapper.find('.b')).toBeNull();
+ });
+
+ test('forwards the dismissButton slot to the dismiss button', () => {
+ const wrapper = renderOpenPopover({ styleClassNames: { dismissButton: 'a' } });
+ expect(wrapper.findDismissButton()!.getElement()).toHaveClass('a');
+ });
+
+ test('applies styleClassNames to the popover container when rendered in a portal', () => {
+ const wrapper = renderOpenPopover({ renderWithPortal: true, styleClassNames: { popover: 'a' } });
+ expect(wrapper.findContent({ renderWithPortal: true })).not.toBeNull();
+ expect(document.body.querySelector(`.${styles.container}`)).toHaveClass('a');
+ });
+
+ test('does not leak the styleClassNames prop to the DOM', () => {
+ renderOpenPopover({ renderWithPortal: true, styleClassNames: { popover: 'a' } });
+ expect(document.body.querySelector('[styleClassNames]')).toBeNull();
+ });
+});
diff --git a/src/popover/arrow.scss b/src/popover/arrow.scss
index 15b3f0a42c..4038762cb8 100644
--- a/src/popover/arrow.scss
+++ b/src/popover/arrow.scss
@@ -3,6 +3,8 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use './style-api' as popover-style-api;
.arrow {
// Arrow design:
@@ -64,19 +66,22 @@
&-outer {
&::after {
- background-color: awsui.$color-border-popover;
+ background-color: style-api.value(popover-style-api.$tokens, border-color, awsui.$color-border-popover);
}
}
&-inner {
- inset-block-start: calc(awsui.$border-width-popover + 1px);
+ // The inner triangle is shifted by the border width plus up to 1px so that the uncovered part of the
+ // outer triangle renders as the border; min() collapses the shift to 0 for a borderless popover.
+ $border-width: style-api.value(popover-style-api.$tokens, border-width, awsui.$border-width-popover);
+ inset-block-start: calc(#{$border-width} + min(#{$border-width}, 1px));
&::after {
border-start-start-radius: 1px;
border-start-end-radius: 0;
border-end-start-radius: 0;
border-end-end-radius: 0;
- background-color: awsui.$color-background-popover;
+ background: style-api.value(popover-style-api.$tokens, background, awsui.$color-background-popover);
}
}
diff --git a/src/popover/body.scss b/src/popover/body.scss
index f9633f4cf0..6e44477919 100644
--- a/src/popover/body.scss
+++ b/src/popover/body.scss
@@ -9,6 +9,7 @@ $header-row-margin-block-end: awsui.$space-xs;
.body {
@include styles.styles-reset;
@include styles.text-flex-wrapping;
+ color: awsui.$color-text-body-default;
padding-block: awsui.$space-s;
padding-inline: awsui.$space-m;
diff --git a/src/popover/body.tsx b/src/popover/body.tsx
index 8bca2d3cf1..69e1c35582 100644
--- a/src/popover/body.tsx
+++ b/src/popover/body.tsx
@@ -28,6 +28,7 @@ export interface PopoverBodyProps {
overflowVisible?: 'content' | 'both';
className?: string;
+ dismissButtonClassName?: string;
ariaLabelledby?: string;
closeAnalyticsAction?: string;
@@ -46,6 +47,7 @@ const PopoverBody = React.forwardRef(
variant,
overflowVisible,
className,
+ dismissButtonClassName,
ariaLabelledby,
closeAnalyticsAction,
}: PopoverBodyProps,
@@ -90,6 +92,7 @@ const PopoverBody = React.forwardRef(
ariaLabel={i18n('dismissAriaLabel', dismissAriaLabel)}
onClick={() => onDismiss?.('close-button')}
ref={dismissButtonRef}
+ styleClassNames={{ root: dismissButtonClassName }}
/>
);
diff --git a/src/popover/container.scss b/src/popover/container.scss
index 6f91714b2e..74a242fbd9 100644
--- a/src/popover/container.scss
+++ b/src/popover/container.scss
@@ -3,6 +3,8 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use './style-api' as popover-style-api;
$arrow-height: 10px;
@@ -12,6 +14,7 @@ $arrow-height: 10px;
inset-block-start: -9999px;
inset-inline-start: -9999px;
z-index: 2000;
+ @include style-api.carriers(popover-style-api.$tokens);
// A little pseudoelement to extend the container to the space between the
// popover and the trigger to avoid hover-based popovers/tooltips from
@@ -61,15 +64,18 @@ $arrow-height: 10px;
}
.container-body {
+ $tokens: popover-style-api.$tokens;
@include styles.styles-reset;
- border-start-start-radius: awsui.$border-radius-popover;
- border-start-end-radius: awsui.$border-radius-popover;
- border-end-start-radius: awsui.$border-radius-popover;
- border-end-end-radius: awsui.$border-radius-popover;
- background-color: awsui.$color-background-popover;
+ border-start-start-radius: style-api.value($tokens, border-radius, awsui.$border-radius-popover);
+ border-start-end-radius: style-api.value($tokens, border-radius, awsui.$border-radius-popover);
+ border-end-start-radius: style-api.value($tokens, border-radius, awsui.$border-radius-popover);
+ border-end-end-radius: style-api.value($tokens, border-radius, awsui.$border-radius-popover);
+ background: style-api.value($tokens, background, awsui.$color-background-popover);
box-shadow: awsui.$shadow-popover;
- border-block: awsui.$border-width-popover solid awsui.$color-border-popover;
- border-inline: awsui.$border-width-popover solid awsui.$color-border-popover;
+ $border: style-api.value($tokens, border-width, awsui.$border-width-popover) solid
+ style-api.value($tokens, border-color, awsui.$color-border-popover);
+ border-block: $border;
+ border-inline: $border;
}
.container-body-variant-annotation {
diff --git a/src/popover/internal.tsx b/src/popover/internal.tsx
index f473552b63..7b5bf894c8 100644
--- a/src/popover/internal.tsx
+++ b/src/popover/internal.tsx
@@ -33,6 +33,13 @@ export interface InternalPopoverProps extends Omit;
+ styleClassNames?: StyleClassNames;
+}
+
+// Style API v2
+interface StyleClassNames {
+ popover?: string;
+ dismissButton?: string;
}
export default React.forwardRef(InternalPopover);
@@ -59,6 +66,7 @@ function InternalPopover(
__closeAnalyticsAction,
__visible: controlledVisible,
__onVisibleChange: onVisibleChange,
+ styleClassNames,
...restProps
}: InternalPopoverProps,
@@ -180,6 +188,7 @@ function InternalPopover(
arrow={position => }
renderWithPortal={renderWithPortal}
zIndex={renderWithPortal ? 7000 : undefined}
+ className={styleClassNames?.popover}
>
diff --git a/src/popover/style-api.scss b/src/popover/style-api.scss
new file mode 100644
index 0000000000..9ab0e31415
--- /dev/null
+++ b/src/popover/style-api.scss
@@ -0,0 +1,13 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+@use '../internal/styles/style-api' as style-api;
+
+// Style API v2 tokens of the popover surface, shared by Popover and Tooltip.
+$tokens: style-api.resolve((background, border-color, border-width, border-radius), carrier);
+
+$token-descriptions: (
+ background: 'Background of the surface and arrow.',
+ border-color: 'Border color of the surface and arrow.',
+ border-width: 'Border width of the surface and arrow.',
+);
diff --git a/src/popover/styles.scss b/src/popover/styles.scss
index 89a179b17c..68ad682313 100644
--- a/src/popover/styles.scss
+++ b/src/popover/styles.scss
@@ -7,11 +7,22 @@
@use '../internal/styles/tokens' as awsui;
@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+@use '../internal/styles/style-api' as style-api;
+@use './style-api' as popover-style-api;
+
@use './arrow';
@use './body';
@use './container';
@use './motion';
+@include style-api.register(popover-style-api.$tokens);
+@include style-api.docs(
+ 'popover',
+ popover-style-api.$tokens,
+ $descriptions: (tokens: popover-style-api.$token-descriptions)
+);
+@include style-api.docs-forward('dismissButton', 'button', 'root');
+
$trigger-underline-offset: 0.25em;
.root {
diff --git a/src/tooltip/__tests__/style-api.test.tsx b/src/tooltip/__tests__/style-api.test.tsx
new file mode 100644
index 0000000000..2edf29bbee
--- /dev/null
+++ b/src/tooltip/__tests__/style-api.test.tsx
@@ -0,0 +1,27 @@
+// 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 Tooltip from '../../../lib/components/tooltip';
+
+import popoverStyles from '../../../lib/components/popover/styles.css.js';
+
+function renderTooltip(props: Record) {
+ const trigger = document.createElement('button');
+ document.body.appendChild(trigger);
+ return render( trigger} {...props} />);
+}
+
+describe('Tooltip Style API v2', () => {
+ test('applies styleClassNames to the tooltip container', () => {
+ renderTooltip({ styleClassNames: { tooltip: 'a', unknown: 'b' } });
+ expect(document.body.querySelector(`.${popoverStyles.container}`)).toHaveClass('a');
+ expect(document.body.querySelector('.b')).toBeNull();
+ });
+
+ test('does not leak the styleClassNames prop to the DOM', () => {
+ renderTooltip({ styleClassNames: { tooltip: 'a' } });
+ expect(document.body.querySelector('[styleClassNames]')).toBeNull();
+ });
+});
diff --git a/src/tooltip/internal.tsx b/src/tooltip/internal.tsx
index 7556612d1d..d2ae9e30df 100644
--- a/src/tooltip/internal.tsx
+++ b/src/tooltip/internal.tsx
@@ -22,8 +22,14 @@ type InternalTooltipComponentProps = InternalTooltipProps &
// ID of the associated trigger. When the trigger and the tooltip are connected via referrer ID, the
// nodeBelongs(triggerNode, tooltipNode) returns true.
referrerId?: string;
+ styleClassNames?: StyleClassNames;
};
+// Style API v2
+interface StyleClassNames {
+ tooltip?: string;
+}
+
export default function InternalTooltip({
content,
getTrack,
@@ -31,6 +37,7 @@ export default function InternalTooltip({
position = 'top',
onEscape,
referrerId,
+ styleClassNames,
__internalRootRef,
...restProps
}: InternalTooltipComponentProps) {
@@ -86,7 +93,7 @@ export default function InternalTooltip({
zIndex={7000}
arrow={position => }
hideOnOverscroll={true}
- className={className}
+ className={clsx(className, styleClassNames?.tooltip)}
>
{content}
diff --git a/src/tooltip/styles.scss b/src/tooltip/styles.scss
index 65b7808010..f0e501b820 100644
--- a/src/tooltip/styles.scss
+++ b/src/tooltip/styles.scss
@@ -5,6 +5,14 @@
@use '../internal/styles' as styles;
@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles/style-api' as style-api;
+@use '../popover/style-api' as popover-style-api;
+
+@include style-api.docs(
+ 'tooltip',
+ popover-style-api.$tokens,
+ $descriptions: (tokens: popover-style-api.$token-descriptions)
+);
.root {
@include styles.with-motion {