Skip to content
Closed
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
33 changes: 33 additions & 0 deletions etc/lime-elements.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,11 @@ export namespace Components {
"length"?: number;
"maxLength"?: number;
}
// @internal
export interface LimelHotkey {
"disabled": boolean;
"value": string;
}
export interface LimelIcon {
"badge": boolean;
"name": string;
Expand Down Expand Up @@ -638,6 +643,8 @@ export namespace Components {
export interface LimelMenuItemMeta {
"badge"?: string | number;
"commandText"?: string;
"disabled": boolean;
"hotkey"?: string;
"showChevron": boolean;
}
// @internal (undocumented)
Expand Down Expand Up @@ -1391,6 +1398,10 @@ export namespace JSX {
//
// (undocumented)
"limel-helper-line": Omit<LimelHelperLine, keyof LimelHelperLineAttributes> & { [K in keyof LimelHelperLine & keyof LimelHelperLineAttributes]?: LimelHelperLine[K] } & { [K in keyof LimelHelperLine & keyof LimelHelperLineAttributes as `attr:${K}`]?: LimelHelperLineAttributes[K] } & { [K in keyof LimelHelperLine & keyof LimelHelperLineAttributes as `prop:${K}`]?: LimelHelperLine[K] };
// Warning: (ae-incompatible-release-tags) The symbol ""limel-hotkey"" is marked as @public, but its signature references "JSX" which is marked as @internal
//
// (undocumented)
"limel-hotkey": Omit<LimelHotkey, keyof LimelHotkeyAttributes> & { [K in keyof LimelHotkey & keyof LimelHotkeyAttributes]?: LimelHotkey[K] } & { [K in keyof LimelHotkey & keyof LimelHotkeyAttributes as `attr:${K}`]?: LimelHotkeyAttributes[K] } & { [K in keyof LimelHotkey & keyof LimelHotkeyAttributes as `prop:${K}`]?: LimelHotkey[K] };
// (undocumented)
"limel-icon": Omit<LimelIcon, keyof LimelIconAttributes> & { [K in keyof LimelIcon & keyof LimelIconAttributes]?: LimelIcon[K] } & { [K in keyof LimelIcon & keyof LimelIconAttributes as `attr:${K}`]?: LimelIconAttributes[K] } & { [K in keyof LimelIcon & keyof LimelIconAttributes as `prop:${K}`]?: LimelIcon[K] };
// (undocumented)
Expand Down Expand Up @@ -2534,6 +2545,20 @@ export namespace JSX {
"maxLength": number;
}

// @internal
export interface LimelHotkey {
"disabled"?: boolean;
"value"?: string;
}

// (undocumented)
export interface LimelHotkeyAttributes {
// (undocumented)
"disabled": boolean;
// (undocumented)
"value": string;
}

export interface LimelIcon {
"badge"?: boolean;
"name"?: string;
Expand Down Expand Up @@ -2827,6 +2852,8 @@ export namespace JSX {
export interface LimelMenuItemMeta {
"badge"?: string | number;
"commandText"?: string;
"disabled"?: boolean;
"hotkey"?: string;
"showChevron"?: boolean;
}

Expand All @@ -2837,6 +2864,10 @@ export namespace JSX {
// (undocumented)
"commandText": string;
// (undocumented)
"disabled": boolean;
// (undocumented)
"hotkey": string;
// (undocumented)
"showChevron": boolean;
}

Expand Down Expand Up @@ -4153,6 +4184,7 @@ interface MenuItem<T = any> {
badge?: number | string;
commandText?: string;
disabled?: boolean;
hotkey?: string;
icon?: string | Icon;
// @deprecated
iconColor?: Color;
Expand Down Expand Up @@ -4192,6 +4224,7 @@ export type OpenDirection = 'left-start' | 'left' | 'left-end' | 'right-start' |
// @public
interface Option_2<T extends string = string> {
disabled?: boolean;
hotkey?: string;
icon?: string | Icon;
// @deprecated
iconColor?: Color;
Expand Down
9 changes: 9 additions & 0 deletions src/components/hotkey/examples/hotkey-basic.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:host {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 2rem;
}

limel-example-value {
grid-column: 1/-1;
}
59 changes: 59 additions & 0 deletions src/components/hotkey/examples/hotkey-basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component, h, Host } from '@stencil/core';

/**
* Basic example
*
* The value is passed as a string, indicating which hotkey to display.
*
* The component will automatically detect the operating system, and
* render the hotkey accordingly, using standard glyphs to save space.
*
* For example, the "meta" key will be rendered as <kbd>⌘</kbd> on macOS,
* and as <kbd>⊞ Win</kbd> on Windows/Linux. Or the "alt" key will be rendered
* as <kbd>⌥</kbd> on macOS, and as <kbd>Alt</kbd> on Windows.
*
* :::note
* `meta` always means the actual Meta key.
*
* This component will render `meta` using platform conventions:
* - macOS/iOS/iPadOS: <kbd>⌘</kbd>
* - Windows/Linux: <kbd>⊞ Win</kbd>
*
* If you want a hotkey that differs between operating systems (for example
* ⌘+C on macOS and Ctrl+C on Windows/Linux), detect the OS in your application
* and pass the appropriate hotkey string.
*
* - `ctrl` means "Control specifically" on all platforms.
* - `cmd` or `command` always render as <kbd>⌘</kbd> (even on Windows/Linux).
* :::
*
* :::important
* This component is **display-only**. It does not listen for or handle
* any keyboard events. Keyboard event handling is the responsibility
* of the parent component (e.g. `limel-menu` or `limel-select`).
* :::
*/
@Component({
tag: 'limel-example-hotkey-basic',
shadow: true,
styleUrl: 'hotkey-basic.scss',
})
export class HotkeyBasicExample {
public render() {
return (
<Host>
<limel-hotkey value="s" />
<limel-hotkey value="alt+s" />
<limel-hotkey value="meta+c" />
<limel-hotkey value="meta+alt+s" />
<limel-hotkey value="meta+enter" />
<limel-hotkey value="cmd+enter" />
<limel-hotkey value="ctrl+shift+c" />
<limel-hotkey value="f1" />
<limel-hotkey value="tab" />
<limel-hotkey value="+" />
<limel-hotkey value="-" />
</Host>
);
}
}
24 changes: 24 additions & 0 deletions src/components/hotkey/examples/hotkey-disabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Component, h, Host } from '@stencil/core';

/**
* The `disabled` prop
*
* When set to `true`, the hotkey is rendered in a visually disabled state.
* This is useful when the action associated with the hotkey is temporarily
* unavailable (e.g. a disabled menu item).
*/
@Component({
tag: 'limel-example-hotkey-disabled',
shadow: true,
styleUrl: 'hotkey-basic.scss',
})
export class HotkeyDisabledExample {
public render() {
return (
<Host>
<limel-hotkey value="a" disabled={true} />
<limel-hotkey value="b" />
</Host>
);
}
}
33 changes: 33 additions & 0 deletions src/components/hotkey/hotkey.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@forward '../markdown/partial-styles/_kbd.scss';
:host(limel-hotkey) {
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}

:host(limel-hotkey[disabled]:not([disabled='false'])) {
opacity: 0.5;
}

kbd {
margin: 0;
font-size: 0.75rem;
box-shadow:
var(--button-shadow-pressed),
0 0.625rem 0.375px -0.5rem rgb(var(--color-black), 0.02),
0 0.025rem 0.5rem 0 rgb(var(--contrast-100)) inset;
}

span {
display: inline-block;
&::first-letter {
text-transform: uppercase;
}
}

kbd.is-glyph {
span {
transform: scale(1.2);
}
}
131 changes: 131 additions & 0 deletions src/components/hotkey/hotkey.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Component, Host, Prop, h } from '@stencil/core';
import { tokenizeHotkeyString } from '../../util/hotkeys';
import { isAppleDevice } from '../../util/device';

/**
* This is a display-only component used to visualize keyboard shortcuts.
* It renders hotkey strings as styled `<kbd>` elements with
* platform-aware glyphs (e.g. `⌘` on macOS, `⊞ Win` on Windows).
*
* It does **not** listen for or handle any keyboard events.
* Keyboard event handling is the responsibility of the parent component
* (e.g. `limel-menu` or `limel-select`).
*
* @exampleComponent limel-example-hotkey-basic
* @exampleComponent limel-example-hotkey-disabled
* @private
*/
@Component({
tag: 'limel-hotkey',
shadow: true,
styleUrl: 'hotkey.scss',
})
export class Hotkey {
/**
* The hotkey string to visualize, e.g. `"meta+c"` or `"shift+enter"`.
*/
@Prop({ reflect: true })
public value: string;

/**
* When `true`, the hotkey is rendered in a visually disabled state.
*/
@Prop({ reflect: true })
public disabled = false;

public render() {
const isApple = isAppleDevice();
const parts = tokenizeHotkeyString(this.value);
const ariaLabel = (this.value ?? '').trim();

return (
<Host aria-label={ariaLabel || undefined}>
{parts.map((part, index) => {
const { display, isGlyph } = this.formatDisplayToken(
part,
isApple
);

return (
<kbd
key={`${part}-${index}`}
class={isGlyph ? 'is-glyph' : undefined}
>
<span>{display}</span>
</kbd>
);
})}
</Host>
);
}

private formatDisplayToken(
token: string,
isApple: boolean
): {
display: string;
isGlyph: boolean;
} {
const trimmed = (token ?? '').trim();
if (!trimmed) {
return { display: '', isGlyph: false };
}

if (trimmed === '+') {
return { display: '+', isGlyph: false };
}

const lower = trimmed.toLowerCase();

switch (lower) {
case 'meta': {
return isApple
? { display: '⌘', isGlyph: true }
: { display: '⊞ Win', isGlyph: false };
}

case 'cmd':
case 'command': {
return { display: '⌘', isGlyph: true };
}

case 'alt':
case 'option': {
return isApple
? { display: '⌥', isGlyph: true }
: { display: 'Alt', isGlyph: false };
}

case 'shift': {
return { display: '⇧', isGlyph: true };
}

case 'enter':
case 'return': {
return { display: '↩', isGlyph: true };
}

case 'tab': {
return { display: '⇥', isGlyph: true };
}

case 'delete':
case 'del':
case 'backspace': {
if (isApple) {
return { display: '⌫', isGlyph: true };
}
return lower === 'backspace'
? { display: 'Backspace', isGlyph: false }
: { display: 'Del', isGlyph: false };
}
Comment thread
Kiarokh marked this conversation as resolved.

case 'ctrl':
case 'control': {
return { display: 'Ctrl', isGlyph: false };
}
}

return { display: trimmed, isGlyph: false };
}
}
20 changes: 19 additions & 1 deletion src/components/list-item/menu-item-meta/menu-item-meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,21 @@ export class MenuItemMeta {
/**
* Use to display optional keyboard shortcut or command hint, e.g. `⌘ + K`
*/
@Prop()
@Prop({ reflect: true })
public commandText?: string;

/**
* Hotkey to display. When provided, `commandText` is ignored.
*/
@Prop({ reflect: true })
public hotkey?: string;

/**
* Will be set to `true` when the menu item is disabled.
*/
@Prop({ reflect: true })
public disabled = false;

/**
* Optional badge value
*/
Expand All @@ -45,6 +57,12 @@ export class MenuItemMeta {
}

private renderCommandText() {
if (this.hotkey) {
return (
<limel-hotkey value={this.hotkey} disabled={this.disabled} />
);
}

if (!this.commandText) {
return;
}
Expand Down
Loading
Loading