From 96beb60122e5c59a03aa5e5a91aab0e4797c49a8 Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Fri, 24 Apr 2026 16:11:15 +0200 Subject: [PATCH 1/9] fix(tooltip): generic UI improvements --- src/components/tooltip/tooltip-content.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/tooltip/tooltip-content.scss b/src/components/tooltip/tooltip-content.scss index 7e455c9624..4c77123cb3 100644 --- a/src/components/tooltip/tooltip-content.scss +++ b/src/components/tooltip/tooltip-content.scss @@ -1,15 +1,14 @@ :host(limel-tooltip-content) { display: flex; - border-radius: 0.25rem; padding: 0.25rem 0.5rem; background-color: rgb(var(--contrast-1300)); - box-shadow: var(--shadow-depth-16); + box-shadow: var(--shadow-depth-16), var(--shadow-brighten-edges-inside); } text { font-size: var(--limel-theme-default-font-size); // 14px - line-height: 1.25; + line-height: normal; display: flex; column-gap: 1rem; From 7b0a317e4f4bff603c69f1d9eed4346c5979c8fb Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Fri, 24 Apr 2026 16:13:51 +0200 Subject: [PATCH 2/9] feat(tooltip): add a new `hotkey` prop to visualize a hotkey inside the tooltip's content --- etc/lime-elements.api.md | 8 +++ .../tooltip/examples/tooltip-basic.tsx | 2 +- .../tooltip/examples/tooltip-hotkey.tsx | 62 +++++++++++++++++++ .../examples/tooltip-max-character.scss | 2 +- src/components/tooltip/tooltip-content.tsx | 21 +++++++ src/components/tooltip/tooltip.spec.tsx | 40 +++++++++++- src/components/tooltip/tooltip.tsx | 12 +++- 7 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/components/tooltip/examples/tooltip-hotkey.tsx diff --git a/etc/lime-elements.api.md b/etc/lime-elements.api.md index a36df2b77a..b73f33b68b 100644 --- a/etc/lime-elements.api.md +++ b/etc/lime-elements.api.md @@ -907,6 +907,7 @@ export namespace Components { export interface LimelTooltip { "elementId": string; "helperLabel"?: string; + "hotkey"?: string; "label": string; "maxlength"?: number; "openDirection": OpenDirection; @@ -914,6 +915,7 @@ export namespace Components { // @internal export interface LimelTooltipContent { "helperLabel"?: string; + "hotkey"?: string; "label": string; "maxlength"?: number; } @@ -3597,6 +3599,7 @@ export namespace JSX { export interface LimelTooltip { "elementId": string; "helperLabel"?: string; + "hotkey"?: string; "label": string; "maxlength"?: number; "openDirection"?: OpenDirection; @@ -3609,6 +3612,8 @@ export namespace JSX { // (undocumented) "helperLabel": string; // (undocumented) + "hotkey": string; + // (undocumented) "label": string; // (undocumented) "maxlength": number; @@ -3619,6 +3624,7 @@ export namespace JSX { // @internal export interface LimelTooltipContent { "helperLabel"?: string; + "hotkey"?: string; "label": string; "maxlength"?: number; } @@ -3628,6 +3634,8 @@ export namespace JSX { // (undocumented) "helperLabel": string; // (undocumented) + "hotkey": string; + // (undocumented) "label": string; // (undocumented) "maxlength": number; diff --git a/src/components/tooltip/examples/tooltip-basic.tsx b/src/components/tooltip/examples/tooltip-basic.tsx index 4dc8d8835a..5dcc6cb8ef 100644 --- a/src/components/tooltip/examples/tooltip-basic.tsx +++ b/src/components/tooltip/examples/tooltip-basic.tsx @@ -13,7 +13,7 @@ export class TooltipBasicExample { , , ]; diff --git a/src/components/tooltip/examples/tooltip-hotkey.tsx b/src/components/tooltip/examples/tooltip-hotkey.tsx new file mode 100644 index 0000000000..0216ec7038 --- /dev/null +++ b/src/components/tooltip/examples/tooltip-hotkey.tsx @@ -0,0 +1,62 @@ +import { Component, h, Host } from '@stencil/core'; + +/** + * Visualizing a keyboard shortcut + * + * Use the `hotkey` property to render a keyboard shortcut inside the tooltip, + * next to the `label` (and the optional `helperLabel`). The keyboard shortcut + * that you will define will automatically have platform-aware glyphs. + * For example, `meta+c` renders as C on macOS and + * as ⊞ Win + C on Windows. + * + * :::important + * The `hotkey` property is for visualization purposes only. + * The tooltip does **not** listen for, or handle any keyboard events + * on its own. Catching the key combination and running + * the associated action is the responsibility of the consumer of the tooltip. + * ::: + * + * :::note + * 1. `meta` means the Meta key. + * It is rendered as on Apple devices, and ⊞ Win on + * Windows/Linux. (`cmd`, `command`, `win`, `windows` are aliases for `meta`.) + * + * If you want "primary modifier" hotkeys (e.g. on macOS and + * Ctrl on Windows/Linux), detect OS in your application and + * provide different `hotkey` values. + * + * `ctrl` means the Control key on all platforms. + * + * 2. All browsers and operating systems have some built-in hotkeys + * that may conflict with your defined hotkeys. + * For example, `cmd+p` is often used to print the current page. + * Make sure to choose hotkeys that do not conflict with common browser + * hotkeys, and user's expected behavior. + * ::: + */ +@Component({ + tag: 'limel-example-tooltip-hotkey', + shadow: true, + styleUrl: 'tooltip-max-character.scss', +}) +export class TooltipHotkeyExample { + public render() { + return ( + + + + + + + ); + } +} diff --git a/src/components/tooltip/examples/tooltip-max-character.scss b/src/components/tooltip/examples/tooltip-max-character.scss index 276e2060c5..072d939f1a 100644 --- a/src/components/tooltip/examples/tooltip-max-character.scss +++ b/src/components/tooltip/examples/tooltip-max-character.scss @@ -1,4 +1,4 @@ -:host(limel-example-tooltip-max-character) { +:host { display: flex; gap: 1rem; justify-content: space-between; diff --git a/src/components/tooltip/tooltip-content.tsx b/src/components/tooltip/tooltip-content.tsx index 772a28099d..45dab1dde1 100644 --- a/src/components/tooltip/tooltip-content.tsx +++ b/src/components/tooltip/tooltip-content.tsx @@ -1,4 +1,5 @@ import { Component, h, Prop } from '@stencil/core'; +import { normalizeHotkeyString } from '../../util/hotkeys'; /** * This component is used internally by `limel-tooltip`. @@ -29,6 +30,12 @@ export class TooltipContent { @Prop({ reflect: true }) maxlength?: number; + /** + * Read more in tooltip.tsx + */ + @Prop({ reflect: true }) + hotkey?: string; + public render() { let isLabelsTextLong = false; if (this.helperLabel && this.maxlength) { @@ -47,7 +54,21 @@ export class TooltipContent {
{this.label}
{this.helperLabel}
+ {this.renderHotkey()}
, ]; } + + private renderHotkey() { + if (!this.hotkey) { + return; + } + + const normalized = normalizeHotkeyString(this.hotkey); + if (!normalized) { + return; + } + + return ; + } } diff --git a/src/components/tooltip/tooltip.spec.tsx b/src/components/tooltip/tooltip.spec.tsx index 74c321e1d9..f7e0910634 100644 --- a/src/components/tooltip/tooltip.spec.tsx +++ b/src/components/tooltip/tooltip.spec.tsx @@ -14,13 +14,16 @@ vi.mock('./tooltip-timer', () => { import { render, h } from '@stencil/vitest'; describe('limel-tooltip', () => { - async function setup() { + async function setup( + props: { hotkey?: string; helperLabel?: string } = {} + ) { const { root, waitForChanges } = await render( ); @@ -34,8 +37,17 @@ describe('limel-tooltip', () => { 'limel-tooltip-content' ); const portal = tooltip?.shadowRoot?.querySelector('limel-portal'); + const hotkey = content?.shadowRoot?.querySelector('limel-hotkey'); - return { root, anchor, tooltip, content, portal, waitForChanges }; + return { + root, + anchor, + tooltip, + content, + portal, + hotkey, + waitForChanges, + }; } test('the component renders', async () => { @@ -67,4 +79,28 @@ describe('limel-tooltip', () => { expect(content).not.toBeNull(); expect(content!.getAttribute('aria-hidden')).not.toBe('true'); }); + + describe('hotkey prop', () => { + test('does not render limel-hotkey when hotkey is not set', async () => { + const { hotkey } = await setup(); + expect(hotkey).toBeFalsy(); + }); + + test('renders limel-hotkey when hotkey is set', async () => { + const { hotkey } = await setup({ hotkey: 'ctrl+f' }); + expect(hotkey).toBeTruthy(); + expect(hotkey!.getAttribute('value')).toBe('ctrl+f'); + }); + + test('normalizes the hotkey string before passing it on', async () => { + const { hotkey } = await setup({ hotkey: 'CMD+Enter' }); + expect(hotkey).toBeTruthy(); + expect(hotkey!.getAttribute('value')).toBe('meta+enter'); + }); + + test('does not render limel-hotkey for an invalid hotkey', async () => { + const { hotkey } = await setup({ hotkey: 'ctrl+' }); + expect(hotkey).toBeFalsy(); + }); + }); }); diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index f898a19663..5b614d6414 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -49,6 +49,7 @@ const DEFAULT_MAX_LENGTH = 50; * * @exampleComponent limel-example-tooltip-basic * @exampleComponent limel-example-tooltip-max-character + * @exampleComponent limel-example-tooltip-hotkey * @exampleComponent limel-example-tooltip-composite */ @Component({ @@ -73,12 +74,18 @@ export class Tooltip { /** * Additional helper text for the element. - * Example usage can be a keyboard shortcut to activate the function of the - * owner element. */ @Prop({ reflect: true }) public helperLabel?: string; + /** + * Keyboard shortcut to visualize inside the tooltip, e.g. `"ctrl+f"`. + * Display-only: the tooltip does not listen for the keystroke. + * Catching the hotkey is the consumer's responsibility. + */ + @Prop({ reflect: true }) + public hotkey?: string; + /** * The maximum amount of characters before rendering 'label' and * 'helperLabel' in two rows. @@ -142,6 +149,7 @@ export class Tooltip { Date: Fri, 24 Apr 2026 16:15:43 +0200 Subject: [PATCH 3/9] refactor(tooltip): don't render helper label when not provided --- src/components/tooltip/tooltip-content.scss | 4 ---- src/components/tooltip/tooltip-content.tsx | 10 +++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/tooltip/tooltip-content.scss b/src/components/tooltip/tooltip-content.scss index 4c77123cb3..0278a0bdec 100644 --- a/src/components/tooltip/tooltip-content.scss +++ b/src/components/tooltip/tooltip-content.scss @@ -31,8 +31,4 @@ text { .helper-label { color: rgb(var(--contrast-800)); - - &:empty { - display: none; - } } diff --git a/src/components/tooltip/tooltip-content.tsx b/src/components/tooltip/tooltip-content.tsx index 45dab1dde1..3280f264b3 100644 --- a/src/components/tooltip/tooltip-content.tsx +++ b/src/components/tooltip/tooltip-content.tsx @@ -53,12 +53,20 @@ export class TooltipContent { return [
{this.label}
-
{this.helperLabel}
+ {this.renderHelperLabel()} {this.renderHotkey()}
, ]; } + private renderHelperLabel() { + if (!this.helperLabel) { + return; + } + + return
{this.helperLabel}
; + } + private renderHotkey() { if (!this.hotkey) { return; From 06634cf6ca12ae1ae789f46eaa62676f9fbb85dd Mon Sep 17 00:00:00 2001 From: Kiarokh Moattar Date: Mon, 27 Apr 2026 10:07:57 +0200 Subject: [PATCH 4/9] fix(icon-button): use `label` prop as `aria-label` on the `button` element This honors the original intent of `label`. But screen readers will read it out load twice, since the `label` is also passed to the tooltip, and `limel-tooltip` adds it as an `aria-describedby` to the `button` element too. But it's "slightly noisy" vs "broken", and noisy wins. --- src/components/icon-button/icon-button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/icon-button/icon-button.tsx b/src/components/icon-button/icon-button.tsx index d5627f9aac..b84d847cae 100644 --- a/src/components/icon-button/icon-button.tsx +++ b/src/components/icon-button/icon-button.tsx @@ -95,6 +95,7 @@ export class IconButton { return (