Skip to content

Commit f46419e

Browse files
fix(KeyComboTag): Better rendering on non-Mac operating systems (#7025)
Co-authored-by: svc-changelog <[email protected]>
1 parent bee98b2 commit f46419e

File tree

7 files changed

+79
-26
lines changed

7 files changed

+79
-26
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: fix
2+
fix:
3+
description: The KeyComboTag component no longer renders modifier key icons on non-Mac
4+
operating systems
5+
links:
6+
- https://github.com/palantir/blueprint/pull/7025

packages/core/src/components/hotkeys/_hotkeys.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
@import "../../common/mixins";
55

66
.#{$ns}-key-combo {
7-
@include pt-flex-container(row, $pt-grid-size * 0.5);
7+
&:not(.#{$ns}-minimal) {
8+
@include pt-flex-container(row, $pt-grid-size * 0.5);
9+
}
10+
811
align-items: center;
912
}
1013

packages/core/src/components/hotkeys/hotkeyParser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const CONFIG_ALIASES: KeyMap = {
5050
esc: "escape",
5151
escape: "escape",
5252
minus: "-",
53-
mod: isMac() ? "meta" : "ctrl",
53+
mod: isMac(undefined) ? "meta" : "ctrl",
5454
option: "alt",
5555
plus: "+",
5656
return: "enter",
@@ -232,15 +232,15 @@ export const getKeyCombo = (e: KeyboardEvent): KeyCombo => {
232232
* Unlike the parseKeyCombo method, this method does NOT convert shifted
233233
* action keys. So `"@"` will NOT be converted to `["shift", "2"]`).
234234
*/
235-
export const normalizeKeyCombo = (combo: string, platformOverride?: string): string[] => {
235+
export const normalizeKeyCombo = (combo: string, platformOverride: string | undefined): string[] => {
236236
const keys = combo.replace(/\s/g, "").split("+");
237237
return keys.map(key => {
238238
const keyName = CONFIG_ALIASES[key] != null ? CONFIG_ALIASES[key] : key;
239239
return keyName === "meta" ? (isMac(platformOverride) ? "cmd" : "ctrl") : keyName;
240240
});
241241
};
242242

243-
function isMac(platformOverride?: string) {
243+
export function isMac(platformOverride: string | undefined) {
244244
// HACKHACK: see https://github.com/palantir/blueprint/issues/5174
245245
// eslint-disable-next-line deprecation/deprecation
246246
const platform = platformOverride ?? (typeof navigator !== "undefined" ? navigator.platform : undefined);

packages/core/src/components/hotkeys/keyComboTag.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,20 @@ import {
3333
import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, type Props } from "../../common";
3434
import { Icon } from "../icon/icon";
3535

36-
import { normalizeKeyCombo } from "./hotkeyParser";
36+
import { isMac, normalizeKeyCombo } from "./hotkeyParser";
3737

38-
const KEY_ICONS: Record<string, { icon: React.JSX.Element; iconTitle: string }> = {
39-
ArrowDown: { icon: <ArrowDown />, iconTitle: "Down key" },
40-
ArrowLeft: { icon: <ArrowLeft />, iconTitle: "Left key" },
41-
ArrowRight: { icon: <ArrowRight />, iconTitle: "Right key" },
42-
ArrowUp: { icon: <ArrowUp />, iconTitle: "Up key" },
43-
alt: { icon: <KeyOption />, iconTitle: "Alt/Option key" },
44-
cmd: { icon: <KeyCommand />, iconTitle: "Command key" },
45-
ctrl: { icon: <KeyControl />, iconTitle: "Control key" },
38+
const KEY_ICONS: Record<string, { icon: React.JSX.Element; iconTitle: string; isMacOnly?: boolean }> = {
39+
alt: { icon: <KeyOption />, iconTitle: "Alt/Option key", isMacOnly: true },
40+
arrowdown: { icon: <ArrowDown />, iconTitle: "Down key" },
41+
arrowleft: { icon: <ArrowLeft />, iconTitle: "Left key" },
42+
arrowright: { icon: <ArrowRight />, iconTitle: "Right key" },
43+
arrowup: { icon: <ArrowUp />, iconTitle: "Up key" },
44+
cmd: { icon: <KeyCommand />, iconTitle: "Command key", isMacOnly: true },
45+
ctrl: { icon: <KeyControl />, iconTitle: "Control key", isMacOnly: true },
4646
delete: { icon: <KeyDelete />, iconTitle: "Delete key" },
4747
enter: { icon: <KeyEnter />, iconTitle: "Enter key" },
48-
meta: { icon: <KeyCommand />, iconTitle: "Command key" },
49-
shift: { icon: <KeyShift />, iconTitle: "Shift key" },
48+
meta: { icon: <KeyCommand />, iconTitle: "Command key", isMacOnly: true },
49+
shift: { icon: <KeyShift />, iconTitle: "Shift key", isMacOnly: true },
5050
};
5151

5252
/** Reverse table of some CONFIG_ALIASES fields, for display by KeyComboTag */
@@ -71,20 +71,30 @@ export interface KeyComboTagProps extends Props {
7171
minimal?: boolean;
7272
}
7373

74-
export class KeyComboTag extends AbstractPureComponent<KeyComboTagProps> {
74+
interface KeyComboTagInternalProps extends KeyComboTagProps {
75+
/** Override the oeprating system rendering for internal testing purposes */
76+
platformOverride?: string;
77+
}
78+
79+
export class KeyComboTagInternal extends AbstractPureComponent<KeyComboTagInternalProps> {
7580
public static displayName = `${DISPLAYNAME_PREFIX}.KeyComboTag`;
7681

7782
public render() {
78-
const { className, combo, minimal } = this.props;
79-
const keys = normalizeKeyCombo(combo)
83+
const { className, combo, minimal, platformOverride } = this.props;
84+
const normalizedKeys = normalizeKeyCombo(combo, platformOverride);
85+
const keys = normalizedKeys
8086
.map(key => (key.length === 1 ? key.toUpperCase() : key))
81-
.map(minimal ? this.renderMinimalKey : this.renderKey);
82-
return <span className={classNames(Classes.KEY_COMBO, className)}>{keys}</span>;
87+
.map((key, index) =>
88+
minimal
89+
? this.renderMinimalKey(key, index, index === normalizedKeys.length - 1)
90+
: this.renderKey(key, index),
91+
);
92+
return <span className={classNames(Classes.KEY_COMBO, className, { [Classes.MINIMAL]: minimal })}>{keys}</span>;
8393
}
8494

8595
private renderKey = (key: string, index: number) => {
8696
const keyString = DISPLAY_ALIASES[key] ?? key;
87-
const icon = KEY_ICONS[key];
97+
const icon = this.getKeyIcon(key);
8898
const reactKey = `key-${index}`;
8999
return (
90100
<kbd className={classNames(Classes.KEY, { [Classes.MODIFIER_KEY]: icon != null })} key={reactKey}>
@@ -94,8 +104,22 @@ export class KeyComboTag extends AbstractPureComponent<KeyComboTagProps> {
94104
);
95105
};
96106

97-
private renderMinimalKey = (key: string, index: number) => {
98-
const icon = KEY_ICONS[key];
99-
return icon == null ? key : <Icon icon={icon.icon} title={icon.iconTitle} key={`key-${index}`} />;
107+
private renderMinimalKey = (key: string, index: number, isLastKey: boolean) => {
108+
const icon = this.getKeyIcon(key);
109+
if (icon == null) {
110+
return isLastKey ? key : <React.Fragment key={`key-${index}`}>{key}&nbsp;+&nbsp;</React.Fragment>;
111+
}
112+
return <Icon icon={icon.icon} title={icon.iconTitle} key={`key-${index}`} />;
100113
};
114+
115+
private getKeyIcon(key: string) {
116+
const { platformOverride } = this.props;
117+
const icon = KEY_ICONS[key];
118+
if (icon?.isMacOnly && !isMac(platformOverride)) {
119+
return undefined;
120+
}
121+
return icon;
122+
}
101123
}
124+
125+
export const KeyComboTag: React.ComponentType<KeyComboTagProps> = KeyComboTagInternal;

packages/core/test/hotkeys/keyComboTagTests.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,24 @@ import { render, screen } from "@testing-library/react";
1818
import { expect } from "chai";
1919
import * as React from "react";
2020

21-
import { KeyComboTag } from "../../src/components/hotkeys";
21+
import { KeyComboTagInternal } from "../../src/components/hotkeys/keyComboTag";
2222

2323
describe("KeyCombo", () => {
2424
it("renders key combo", () => {
25-
render(<KeyComboTag combo="cmd+C" />);
25+
render(<KeyComboTagInternal combo="cmd+C" platformOverride="Mac" />);
2626
expect(screen.getByText("C")).not.to.be.undefined;
2727
});
28+
29+
it("should render minimal key combos on Mac using icons", () => {
30+
render(<KeyComboTagInternal combo="mod+C" minimal={true} platformOverride="Mac" />);
31+
expect(() => screen.getByText("cmd + C", { exact: false })).to.throw;
32+
});
33+
34+
it("should render minimal key combos on non-Macs using text", () => {
35+
render(<KeyComboTagInternal combo="mod+C" minimal={true} platformOverride="Win32" />);
36+
const text = screen.getByText("ctrl + C", { exact: false }).innerText;
37+
expect(text).to.contain("ctrl");
38+
expect(text).to.contain("+");
39+
expect(text).to.contain("C");
40+
});
2841
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type: fix
2+
fix:
3+
description: The useHotkeys documentation now shows minimal and non-minimal states
4+
for the KeyComboTag component.
5+
links:
6+
- https://github.com/palantir/blueprint/pull/7025

packages/docs-app/src/examples/core-examples/hotkeyTesterExample.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class HotkeyTesterExample extends React.PureComponent<ExampleProps, Hotke
5151
return (
5252
<>
5353
<KeyComboTag combo={combo} />
54+
<KeyComboTag combo={combo} minimal={true} />
5455
<Code>{combo}</Code>
5556
</>
5657
);

0 commit comments

Comments
 (0)