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 && (