Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Kbd } from "@mittwald/flow-react-components";

<Kbd>
<Kbd keys={["mod"]} /> + <Kbd keys={["k"]} />,{" "}
<Kbd keys={["mod"]} /> + <Kbd keys={["c"]} />
</Kbd>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
Button,
ContextMenu,
ContextMenuTrigger,
MenuItem,
Kbd,
} from "@mittwald/flow-react-components";

<ContextMenuTrigger>
<Button>Menü öffnen</Button>
<ContextMenu>
<MenuItem>
Speichern <Kbd keys={["mod", "s"]} />
</MenuItem>
<MenuItem>
Kopieren <Kbd keys={["mod", "c"]} />
</MenuItem>
<MenuItem>
Einfügen <Kbd keys={["mod", "v"]} />
</MenuItem>
</ContextMenu>
</ContextMenuTrigger>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Kbd } from "@mittwald/flow-react-components";

<Kbd keys={["mod", "k"]} />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {
SearchField,
Kbd,
} from "@mittwald/flow-react-components";

<SearchField aria-label="Suche">
<Kbd keys={["mod", "k"]} />
</SearchField>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Text, Kbd } from "@mittwald/flow-react-components";

<Text>
Verwende <Kbd keys={["mod", "k"]} /> um die Suche zu
öffnen.
</Text>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Kbd } from "@mittwald/flow-react-components";

<Column>
<Kbd variant="plain" keys={["mod", "k"]} />
<Kbd variant="soft" keys={["mod", "k"]} />
</Column>;
17 changes: 17 additions & 0 deletions apps/docs/src/content/04-components/content/kbd/guidelines.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Grundlagen

## Best practices

Achte bei Verwendung eines Kbds darauf, dass ...

- nur tatsächlich verfügbare und funktionierende Keyboard-Shortcuts angezeigt
werden.
- der Kbd in direktem Bezug zu dem Inhalt oder der Aktion platziert wird, die
durch den Shortcut ausgeführt werden kann.

---

## Verwendung

Verwende einen Kbd, um Keyboard-Shortcuts visuell klar und konsistent
darzustellen.
9 changes: 9 additions & 0 deletions apps/docs/src/content/04-components/content/kbd/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
component: Kbd
description:
Kbd ist eine dedizierte Komponente zur einheitlichen Darstellung von
Tastaturkürzeln. Er stellt Tastaturkürzel visuelle dar und passt sich
automatisch an das Betriebssystem des Users an.
---

<LiveCodeEditor editorDisabled />
61 changes: 61 additions & 0 deletions apps/docs/src/content/04-components/content/kbd/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Playground

Die Komponente unterstützt ein Array von `keys`. Die einzelnen keys werden als
Strings übergeben. Wenn dabei die Werte "mod", "alt" oder "shift" verwendet
werden, wird automatisch das passende Tastensymbol angezeigt – abhängig davon,
ob der Nutzer macOS oder Windows verwendet. Dadurch werden die jeweiligen
Tastenbezeichnungen plattformspezifisch korrekt dargestellt, ohne dass
zusätzliche Logik implementiert werden muss.

<LiveCodeEditor />

# Children

Um flexiblere Shortcuts darzustellen, unterstützt die Komponente auch
`children`. Hierbei ist darauf zu achten, dass `Kbd` Komponenten für die
einzelnen Keys gesetzt werden müssen, um semantisch korrekte
[kbd-HTML-Elemente](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/kbd)
zu erzeugen.

<LiveCodeEditor editorCollapsed example="children" />

# Varianten

Die Kbd-Komponente wird in den Varianten `plain` und `soft` angeboten. Für die
Darstellung von Shortcuts direkt an Bedienelementen sollte die Variante plain
verwendet werden, da sie sich dezent in die Oberfläche einfügt und nicht zu
aufdringlich wirkt. Innerhalb von erklärenden Texten oder Beschreibungen sollte
hingegen die Variante soft eingesetzt werden, um hervorzuheben, dass es sich um
einen Tastatur-Shortcut handelt.

<LiveCodeEditor editorCollapsed example="variants" />

---

# Kombiniere mit …

## SearchField

Wenn eine Suche durch einen Keyboard-Shortcut fokussiert werden kann, kann die
Kbd Komponente mit dem
[SearchField](/04-components/form-controls/search-field/overview) kombiniert
werden, um dem User das verfügbare Tastaturkürzel sichtbar zu machen.

<LiveCodeEditor editorCollapsed example="search-field" />

## ContextMenu

In [ContextMenus](/04-components/actions/context-menu/overview) werden häufig
Aktionen angezeigt, die zusätzlich per Keyboard-Shortcut ausgelöst werden
können. In diesem Fall kann Kbd mit dem jeweiligen MenuItem kombiniert werden.

<LiveCodeEditor editorCollapsed example="context-menu" />

## Text

Wird die Kbd Komponente innerhalb eines
[Texts](/04-components/content/text/overview) verwendet, wird sie automatisch in
der `soft` Variante dargestellt. Dadurch hebt sie sich visuell vom Fließtext ab
und macht deutlich, dass es sich um einen Tastatur-Shortcut handelt.

<LiveCodeEditor editorCollapsed example="text" />
40 changes: 40 additions & 0 deletions packages/components/src/components/Kbd/Kbd.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
:not(.kbd) > .kbd {
font-size: var(--font-size-text--s);

&.soft {
display: inline-block;
line-height: var(--font-size-text--m);
color: var(--kbd--content-color--default);
background-color: var(--kbd--background-color--default);
padding: var(--kbd--padding);
padding-top: var(--kbd--padding-y-start);
border-radius: var(--kbd--corner-radius);
border-left: var(--kbd--inner-shadow-size-x) solid
var(--kbd--inner-shadow-color--default);
border-right: var(--kbd--inner-shadow-size-x) solid
var(--kbd--inner-shadow-color--default);
box-shadow: inset 0 calc(var(--kbd--inner-shadow-size-y-end) * -1) 0
var(--kbd--inner-shadow-color--default);
width: fit-content;
height: fit-content;
}

&.plain {
color: var(--kbd--content-color-plain--default);
}

&.disabled {
&.soft {
color: var(--kbd--content-color--disabled);
background-color: var(--kbd--background-color--disabled);
border-left-color: var(--kbd--inner-shadow-color--disabled);
border-right-color: var(--kbd--inner-shadow-color--disabled);
box-shadow: inset 0 calc(var(--kbd--inner-shadow-size-y-end) * -1) 0
var(--kbd--inner-shadow-color--disabled);
}

&.plain {
color: var(--kbd--content-color-plain--disabled);
}
}
}
73 changes: 73 additions & 0 deletions packages/components/src/components/Kbd/Kbd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useLocalizedStringFormatter } from "react-aria";
import locales from "./locales/*.locale.json";
import type { PropsWithClassName } from "@/lib/types/props";
import clsx from "clsx";
import styles from "./Kbd.module.scss";
import {
flowComponent,
type FlowComponentProps,
} from "@/lib/componentFactory/flowComponent";
import { Fragment, type PropsWithChildren } from "react";
import { isAppleDevice } from "@react-aria/utils";

export interface KbdProps
extends PropsWithClassName, FlowComponentProps, PropsWithChildren {
/** Array of keys to be joined */
keys?: (string | "mod" | "alt" | "shift")[];
/** Whether the component is displayed as disabled */
isDisabled?: boolean;
/** The visual variant @default "plain" */
variant?: "plain" | "soft";
}

/** @flr-generate all */
export const Kbd = flowComponent("Kbd", (props) => {
const {
keys,
className,
isDisabled,
children,
variant = "plain",
...rest
} = props;

const rootClassName = clsx(
styles.kbd,
isDisabled && styles.disabled,
className,
styles[variant],
);

const stringFormatter = useLocalizedStringFormatter(locales);

const joinedKeys = keys?.map((key, index) => {
let formattedKey = key;

if (key === "mod") {
formattedKey = isAppleDevice() ? "⌘" : stringFormatter.format("kbd.mod");
}
if (key === "alt") {
formattedKey = isAppleDevice() ? "⌥" : stringFormatter.format("kbd.alt");
}
if (key === "shift") {
formattedKey = "⇧";
}

if (keys?.length === 1) {
return <Fragment key={index}>{formattedKey}</Fragment>;
}

return (
<Fragment key={index}>
<kbd>{formattedKey}</kbd>
{index < keys.length - 1 && " + "}
</Fragment>
);
});

return (
<kbd className={rootClassName} {...rest}>
{joinedKeys ?? children}
</kbd>
);
});
1 change: 1 addition & 0 deletions packages/components/src/components/Kbd/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Kbd, type KbdProps } from "./Kbd";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"kbd.alt": "Alt",
"kbd.mod": "Strg"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"kbd.alt": "Alt",
"kbd.mod": "Ctrl"
}
48 changes: 48 additions & 0 deletions packages/components/src/components/Kbd/stories/Default.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Kbd } from "@/components/Kbd";

const meta: Meta<typeof Kbd> = {
title: "Content/Kbd",
component: Kbd,
render: (props) => (
<Kbd {...props} keys={["mod", "k"]}>
mod + k
</Kbd>
),
args: {
isDisabled: false,
variant: "plain",
},
argTypes: {
isDisabled: {
control: "boolean",
},
variant: { control: "inline-radio", options: ["plain", "soft"] },
},
parameters: {
controls: { exclude: ["keys", "children"] },
},
};
export default meta;

type Story = StoryObj<typeof Kbd>;

export const Default: Story = {};

export const WithChildren: Story = {
args: {
keys: undefined,
children: (
<Kbd>
<Kbd>Ctrl</Kbd> + <Kbd>K</Kbd>, <Kbd>Ctrl</Kbd> + <Kbd>C</Kbd>
</Kbd>
),
},
render: (props) => (
<Kbd {...props}>
<Kbd keys={["mod"]} /> + <Kbd keys={["k"]} />, <Kbd keys={["mod"]} /> +{" "}
<Kbd keys={["c"]} />
</Kbd>
),
};
10 changes: 10 additions & 0 deletions packages/components/src/components/Kbd/view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* prettier-ignore */
/* This file is auto-generated with the remote-components-generator */
import type { Kbd } from "./Kbd";
import type { ViewComponent } from "@/lib/viewComponentContext";

declare global {
interface FlowViewComponents {
Kbd: ViewComponent<typeof Kbd>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@
justify-content: center;
align-items: center;
}

.kbd {
margin-inline-start: auto;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const MenuItemContent: FC<Props> = (props) => {
Avatar: {
className: styles.avatar,
},
Kbd: {
className: styles.kbd,
},
};

const controlIconPropsContext: PropsContext = {
Expand Down
Loading
Loading