@ahrowe/ui 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ export type ActionIconSlots = 'root' | 'icon';
6
6
  * A Button with only an icon, sized by that icon, so every Button prop applies except the ones
7
7
  * that describe a label or a size.
8
8
  */
9
- export interface ActionIconProps extends Omit<ButtonProps, 'icon' | 'size' | 'children' | 'isLoading' | 'classNames' | 'styles'> {
9
+ export interface ActionIconProps extends Omit<ButtonProps, 'icon' | 'size' | 'children' | 'isLoading' | 'shortcut' | 'classNames' | 'styles'> {
10
10
  icon: IconContent;
11
11
  classNames?: SlotClassNames<ActionIconSlots>;
12
12
  styles?: SlotStyles<ActionIconSlots>;
@@ -26,7 +26,7 @@ export declare enum ButtonType {
26
26
  Reset = "reset",
27
27
  Submit = "submit"
28
28
  }
29
- export type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label';
29
+ export type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label' | 'shortcut';
30
30
  /**
31
31
  * `DataProps` supplies the `data-*` index signature, which `ButtonHTMLAttributes` doesn't cover.
32
32
  * `type`, `color` and `onClick` below deliberately narrow their inherited counterparts.
@@ -43,6 +43,11 @@ export interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLB
43
43
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
44
44
  children?: React.ReactNode;
45
45
  icon?: IconContent;
46
+ /**
47
+ * A shortcut drawn after the label, in `useHotkeys` spelling (`'mod+s'`): `⌘S` on Apple
48
+ * platforms, `Ctrl+S` elsewhere. Drawn only; bind it with `useHotkeys`.
49
+ */
50
+ shortcut?: string;
46
51
  isLoading?: boolean;
47
52
  disabled?: boolean;
48
53
  title?: string;
@@ -18,6 +18,11 @@ export interface UseHotkeysOptions {
18
18
  * Default `true`.
19
19
  */
20
20
  preventDefault?: boolean;
21
+ /**
22
+ * Fire again for each repeat while the combo is held. Default `false`, so holding `mod+s` saves
23
+ * once. Turn it on for keys that step through something, such as arrows or zoom.
24
+ */
25
+ repeat?: boolean;
21
26
  }
22
27
  /**
23
28
  * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and
@@ -30,6 +35,12 @@ export interface UseHotkeysOptions {
30
35
  * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.
31
36
  */
32
37
  export declare function formatHotkey(combo: string): string;
38
+ /**
39
+ * A combo as `aria-keyshortcuts` spells it: `'mod+shift+k'` is `Control+Shift+K`, or
40
+ * `Meta+Shift+K` on an Apple platform. Resolved from the platform like `formatHotkey`, so the two
41
+ * always name the same keys.
42
+ */
43
+ export declare function ariaKeyshortcuts(combo: string | undefined): string | undefined;
33
44
  /**
34
45
  * Keyboard shortcuts, for a whole app or for one element.
35
46
  *
@@ -38,8 +38,8 @@ import { faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
38
38
  | `ref` | `Ref<HTMLButtonElement>` | Ref to the button |
39
39
 
40
40
  Every other Button prop and native `<button>` attribute passes through, except `size`,
41
- `children` and `isLoading`. The box is the icon plus `4px` on each side; set `width` and `height`
42
- for a different one.
41
+ `children`, `isLoading` and `shortcut`. The box is the icon plus `4px` on each side; set `width`
42
+ and `height` for a different one.
43
43
 
44
44
  **Slots:** `root` `icon`
45
45
 
package/docs/Button.md CHANGED
@@ -11,7 +11,7 @@ works with any colour. The colours are the library-wide `ThemeColor`s or any CSS
11
11
  [ThemeColor.md](ThemeColor.md).
12
12
 
13
13
  ```tsx
14
- import { Button, ButtonStyleType, ButtonSize, ButtonType, ThemeColor } from '@ahrowe/ui';
14
+ import { Button, ButtonStyleType, ButtonSize, ButtonType, ThemeColor, useHotkeys } from '@ahrowe/ui';
15
15
  import { faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
16
16
 
17
17
  // The main action
@@ -52,6 +52,10 @@ import { faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-ic
52
52
  // Small
53
53
  <Button size={ButtonSize.Small}>Cancel</Button>
54
54
 
55
+ // A keyboard shortcut: the button draws it, useHotkeys binds it
56
+ useHotkeys({ 'mod+s': handleSave });
57
+ <Button styleType={ButtonStyleType.Filled} shortcut='mod+s' onClick={handleSave}>Save</Button>
58
+
55
59
  // Press-and-hold: act on press rather than release
56
60
  <Button
57
61
  icon={faPlus}
@@ -81,6 +85,7 @@ import { faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-ic
81
85
  | `size` | `ButtonSize` | Size variant. `Icon` strips all padding, inherits the surrounding text size, and centres a lone icon — set the width/height yourself |
82
86
  | `type` | `ButtonType` | HTML button type |
83
87
  | `icon` | `IconDefinition \| ReactElement` | FontAwesome icon or any React element, shown before label |
88
+ | `shortcut` | `string` | A combo in [useHotkeys](Hooks.md) spelling, e.g. `'mod+s'`. Shown after the label, written for the platform. Binding it is yours, see below |
84
89
  | `isLoading` | `boolean` | Replaces content with a [Loader](Loader.md) in the button's text colour. Blocks clicks like `disabled` but keeps full colour |
85
90
  | `disabled` | `boolean` | Fades the button to half opacity |
86
91
  | `onClick` | `(e: MouseEvent) => void \| Promise<void>` | |
@@ -92,6 +97,17 @@ press-and-hold controls that act on press rather than release), `onContextMenu`,
92
97
  takes the `ButtonType` enum, and `onClick` may return a promise. `color` is Button's own prop, not
93
98
  the obsolete HTML attribute.
94
99
 
100
+ **Shortcuts are drawn, not bound.** `shortcut` writes the combo in a pill after the label,
101
+ `⌘S` on an Apple platform and `Ctrl+S` elsewhere. It also sets `aria-keyshortcuts`, so a screen
102
+ reader announces it apart from the button's name. It is hidden on a touch screen
103
+ (`pointer: coarse`), which usually has no keyboard to press it on.
104
+
105
+ Listening for it is yours, with `useHotkeys` and the same string, because only your app knows
106
+ when the action is available. A button cannot tell: it would still fire while hidden in an
107
+ inactive tab, behind an open Modal, or twice when the same action has a button in the toolbar and
108
+ another in the footer. Turn the binding off wherever the button is disabled:
109
+ `useHotkeys({ 'mod+s': handleSave }, { enabled: !isSaving })`.
110
+
95
111
  **Theming:** each colour reads the theme variables listed in [ThemeColor.md](ThemeColor.md). The
96
112
  `Filled` `Primary` button also paints `--primary-gradient` when the theme sets one. See
97
113
  [ThemeProvider.md](ThemeProvider.md).
@@ -112,7 +128,7 @@ sits inside the edge, in `--focus-border-color`, or in the text colour on a `Fil
112
128
  ripple is in the text colour too. Every button reserves a `1px` border, so switching style never
113
129
  shifts layout or changes its height.
114
130
 
115
- **Slots:** `background` `content` `loadingContainer` `label`
131
+ **Slots:** `background` `content` `loadingContainer` `label` `shortcut`
116
132
 
117
133
  **Sizing in a flex row:** a Button never shrinks below its own content (`min-width: min-content`),
118
134
  so an icon is never cut off by a flexible sibling. A multi-word label still wraps above that floor.
package/docs/Hooks.md CHANGED
@@ -59,6 +59,7 @@ Two rules worth knowing:
59
59
  | `target` | `RefObject<HTMLElement \| null>` | window | Scope the shortcuts to one element |
60
60
  | `enableInFields` | `boolean` | `false` | Let a combo fire while a text field or `contenteditable` has focus. Only combos the field would otherwise consume are held back: a single character, or an editing key (`arrowup`, `home`, `backspace`, `enter`, `tab`, …). Anything carrying Ctrl, Cmd or Alt fires regardless, and so do `f2`, `escape` and the other keys a field does not use |
61
61
  | `preventDefault` | `boolean` | `true` | Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog |
62
+ | `repeat` | `boolean` | `false` | Fire again for each repeat while the combo is held. Off, holding `mod+s` saves once; turn it on for keys that step through something |
62
63
 
63
64
  Shortcuts keep firing while a `Modal` or `Drawer` is open, which is usually what a command palette
64
65
  wants. Pass `enabled` for the ones that should not:
@@ -67,7 +68,14 @@ wants. Pass `enabled` for the ones that should not:
67
68
  useHotkeys({ n: createRecord }, { enabled: !isModalOpen });
68
69
  ```
69
70
 
70
- A held key repeats, as it does natively. Check `event.repeat` in the handler to ignore repeats.
71
+ A held combo fires once, where the browser's key repeat would save over and over or flicker a
72
+ toggle. The skipped repeats are still `preventDefault()`-ed. Where a repeat is the point, as with
73
+ arrows or zoom, ask for it, in a call of its own if the page also has actions:
74
+
75
+ ```tsx
76
+ useHotkeys({ 'mod+s': save, '?': toggleHelp });
77
+ useHotkeys({ j: selectNext, k: selectPrevious }, { repeat: true });
78
+ ```
71
79
 
72
80
  ### formatHotkey
73
81
 
@@ -79,9 +87,10 @@ formatHotkey('mod+shift+k'); // '⇧⌘K' on an Apple platform, 'Ctrl+Shift+K' e
79
87
  formatHotkey('f2'); // 'F2'
80
88
  ```
81
89
 
82
- This is what `Menu` uses to draw the shortcut on an entry, from the same string `menuHotkeys`
83
- turns into a binding, see [Menu.md](Menu.md). A combo it cannot parse is handed back unchanged. The platform is read at call
84
- time, so a server-rendered page renders the non-Apple spelling.
90
+ This is what `Menu`, `CommandPalette` and `Button` use to draw a shortcut, from the same string
91
+ `useHotkeys` binds, see [Menu.md](Menu.md) and [Button.md](Button.md). A combo it cannot parse is
92
+ handed back unchanged. The platform is read at call time, so a server-rendered page renders the
93
+ non-Apple spelling.
85
94
 
86
95
  ## useMediaQuery
87
96
 
@@ -266,3 +275,13 @@ Hooks documented with the thing they belong to:
266
275
  - `useComponentDefaults`, `useLabels` — see [ConfigProvider.md](ConfigProvider.md)
267
276
  - `useFormValidator` — see [FormValidator.md](FormValidator.md)
268
277
  - `useFormValidatorGroup` — see [FormValidatorGroup.md](FormValidatorGroup.md)
278
+
279
+ ## Migration from repeating hotkeys
280
+
281
+ Before 0.49, a held `useHotkeys` combo fired its handler for every repeat the browser sent. It now
282
+ fires once. Nothing fails to compile, so look for shortcuts that are meant to be held:
283
+
284
+ | Before | After |
285
+ |--------|-------|
286
+ | `useHotkeys({ arrowdown: next })`, relying on the repeat | `useHotkeys({ arrowdown: next }, { repeat: true })` |
287
+ | `if (event.repeat) return;` at the top of a handler | nothing: drop the check |
package/docs/Menu.md CHANGED
@@ -34,7 +34,7 @@ const items: MenuEntry[] = [
34
34
 
35
35
  `keywords` are extra search terms for [CommandPalette](CommandPalette.md), which takes the same entries. The menu ignores them.
36
36
 
37
- **Shortcuts are drawn, not bound.** A `shortcut` is a combo in [useHotkeys](Hooks.md) spelling, and the menu writes it on the trailing edge for the platform the reader is on: `mod+s` shows as `⌘S` on an Apple one and `Ctrl+S` everywhere else. So there is one spelling per shortcut and a hand-written `'⌘S'` can no longer be wrong on Windows.
37
+ **Shortcuts are drawn, not bound.** A `shortcut` is a combo in [useHotkeys](Hooks.md) spelling, and the menu writes it on the trailing edge for the platform the reader is on: `mod+s` shows as `⌘S` on an Apple one and `Ctrl+S` everywhere else. So there is one spelling per shortcut and a hand-written `'⌘S'` can no longer be wrong on Windows. Screen readers get it through `aria-keyshortcuts`, not as part of the item's name.
38
38
 
39
39
  Listening for it is yours, because only your app knows what else is listening: if it already binds `⌘S` and the menu bound it too, both would fire and the action would run twice. `menuHotkeys` turns the same entries into a map for `useHotkeys`, so the one spelling covers the hint and the binding:
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "A React UI component library with theming, CSS Modules, and TypeScript support",
5
5
  "keywords": [
6
6
  "agent",