diff --git a/apps/docs/src/content/components/status/badge/examples/contextualHelp.tsx b/apps/docs/src/content/components/status/badge/examples/contextualHelp.tsx
new file mode 100644
index 0000000000..0fc2a9be86
--- /dev/null
+++ b/apps/docs/src/content/components/status/badge/examples/contextualHelp.tsx
@@ -0,0 +1,16 @@
+import {
+ Badge,
+ ContextualHelp,
+ Label,
+ Text,
+} from "@mittwald/flow-react-components";
+
+
+
+ Hoch
+
+
+ Tickets mit hoher Priorität werden zuerst bearbeitet.
+
+
+;
diff --git a/apps/docs/src/content/components/status/badge/index.mdx b/apps/docs/src/content/components/status/badge/index.mdx
index 5eaa8eb7a1..90876882df 100644
--- a/apps/docs/src/content/components/status/badge/index.mdx
+++ b/apps/docs/src/content/components/status/badge/index.mdx
@@ -80,9 +80,19 @@ Informationen zu öffnen. Es sollte dabei nicht als Ersatz für einen
---
-# Badge entfernen
+# Mit ContextualHelp
-Über `onClose` lässt sich ein Badge vom User aus der Oberfläche entfernen.
+Eine [ContextualHelp](/components/overlays/contextual-help) im Badge wird am
+Ende als Button dargestellt. Den Button stellt das Badge selbst.
+
+
+
+---
+
+# Mit onClose
+
+Über `onClose` platzierst du einen X-Button im Badge. Nutze ihn z. B., um das
+Badge entfernbar zu machen.
diff --git a/packages/components/src/components/Badge/Badge.browser.test.tsx b/packages/components/src/components/Badge/Badge.browser.test.tsx
new file mode 100644
index 0000000000..bd18d77e62
--- /dev/null
+++ b/packages/components/src/components/Badge/Badge.browser.test.tsx
@@ -0,0 +1,42 @@
+import { expect, test } from "vitest";
+import { render } from "vitest-browser-react";
+import { page } from "vitest/browser";
+import { Badge } from "@/components/Badge";
+import { ContextualHelp } from "@/components/ContextualHelp";
+import { Text } from "@/components/Text";
+
+test("A contextual help in the content opens from a button of its own", async () => {
+ await render(
+
+ Value
+
+ Every value has a story to tell.
+
+ ,
+ );
+
+ await expect
+ .element(page.getByText("Every value has a story to tell."))
+ .not.toBeInTheDocument();
+
+ await page.getByRole("button", { name: "More information" }).click();
+
+ await expect
+ .element(page.getByText("Every value has a story to tell."))
+ .toBeInTheDocument();
+});
+
+test("A disabled badge disables the contextual help button", async () => {
+ const screen = await render(
+
+ Value
+
+ Every value has a story to tell.
+
+ ,
+ );
+
+ await expect
+ .element(screen.getByRole("button", { name: "More information" }))
+ .toBeDisabled();
+});
diff --git a/packages/components/src/components/Badge/Badge.module.d.scss.ts b/packages/components/src/components/Badge/Badge.module.d.scss.ts
index 8e8df580fe..c7a79793d6 100644
--- a/packages/components/src/components/Badge/Badge.module.d.scss.ts
+++ b/packages/components/src/components/Badge/Badge.module.d.scss.ts
@@ -2,6 +2,7 @@ declare const classNames: {
readonly badge: "badge";
readonly content: "content";
readonly button: "button";
+ readonly action: "action";
readonly close: "close";
readonly scope: "scope";
readonly value: "value";
diff --git a/packages/components/src/components/Badge/Badge.module.scss b/packages/components/src/components/Badge/Badge.module.scss
index 51ffb70ab9..7df639fe01 100644
--- a/packages/components/src/components/Badge/Badge.module.scss
+++ b/packages/components/src/components/Badge/Badge.module.scss
@@ -30,19 +30,28 @@
}
.button,
+ .action,
.close {
@include focus.focus;
}
@extend .neutral;
- &:has(.close) {
+ &:has(.close),
+ &:has(.action) {
.button,
.content {
padding-inline-end: calc(var(--content-size) + var(--badge--spacing));
}
}
+ &:has(.action):has(.close) {
+ .button,
+ .content {
+ padding-inline-end: calc(var(--content-size) * 2 + var(--badge--spacing));
+ }
+ }
+
&:has(.scope) {
.content,
.button {
@@ -59,7 +68,8 @@
flex-shrink: 0;
}
- &:has(.close) {
+ &:has(.close),
+ &:has(.action) {
.button,
.content {
padding-inline-end: var(--content-size);
@@ -69,20 +79,45 @@
}
}
}
+
+ &:has(.action):has(.close) {
+ .button,
+ .content {
+ padding-inline-end: calc(var(--content-size) * 2);
+ }
+ }
}
+ .action,
.close {
height: var(--content-size);
padding: calc(var(--badge--padding-y) - var(--badge--border-width));
border-radius: 0;
- border-end-end-radius: var(--badge-size);
- border-start-end-radius: var(--badge-size);
margin-inline-start: calc(
- (var(--badge-size) - var(--badge--border-width)) * -1
+ (var(--content-size) + var(--badge--border-width)) * -1
);
margin-top: var(--badge--border-width);
}
+ /* The trailing button rounds off the badge */
+ .close,
+ &:not(:has(.close)) .action {
+ border-end-end-radius: var(--badge-size);
+ border-start-end-radius: var(--badge-size);
+ }
+
+ &:has(.action):has(.close) {
+ .action {
+ margin-inline-start: calc(
+ (var(--content-size) * 2 + var(--badge--border-width)) * -1
+ );
+ }
+
+ .close {
+ margin-inline-start: 0;
+ }
+ }
+
/* Colors */
@mixin color($color) {
@@ -135,6 +170,7 @@
color: var(--badge--disabled-content-color);
}
+ .action,
.close {
color: var(--badge--disabled-content-color);
}
@@ -156,6 +192,7 @@
color: var(--badge--disabled-#{$color}-content-color);
}
+ .action,
.close {
color: var(--badge--disabled-#{$color}-content-color);
}
diff --git a/packages/components/src/components/Badge/Badge.tsx b/packages/components/src/components/Badge/Badge.tsx
index d0b7a08ba0..dbf3d333aa 100644
--- a/packages/components/src/components/Badge/Badge.tsx
+++ b/packages/components/src/components/Badge/Badge.tsx
@@ -13,6 +13,8 @@ import type { PressEvent } from "@react-types/shared";
import { Button } from "@/components/Button";
import { IconClose } from "@/components/Icon/components/icons";
import { useLocalizedStringFormatter } from "@/components/TranslationProvider/useLocalizedStringFormatter";
+import { UiComponentTunnelExit } from "@/components/UiComponentTunnel/UiComponentTunnelExit";
+import { ContextualHelpButton } from "@/components/Badge/components/ContextualHelpButton";
import locales from "./locales/*.locale.json";
export const badgeColors = [
@@ -41,6 +43,11 @@ export interface BadgeProps
isDisabled?: boolean;
}
+const contextualHelpTunnel = {
+ id: "contextualHelp",
+ component: "Badge",
+} as const;
+
/** @flr-generate all */
export const Badge = flowComponent("Badge", (props) => {
const {
@@ -63,6 +70,8 @@ export const Badge = flowComponent("Badge", (props) => {
className,
);
+ const buttonColor = isAlphaColor(color) ? color : "dark";
+
const propsContext: PropsContext = {
Label: {
elementType: "span",
@@ -84,6 +93,16 @@ export const Badge = flowComponent("Badge", (props) => {
className: styles.value,
},
},
+ ContextualHelp: {
+ tunnel: contextualHelpTunnel,
+ wrapWith: (
+
+ ),
+ },
};
return (
@@ -101,11 +120,13 @@ export const Badge = flowComponent("Badge", (props) => {
)}
+
+
{onClose && (