@visns-studio/visns-components 6.15.3 → 6.16.1
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.
package/README.md
CHANGED
|
@@ -300,6 +300,7 @@ connected* rather than as an error, because it is not one.
|
|
|
300
300
|
| `echo` | `null` | An Echo instance **or** a factory (`() => getEcho()`). A factory is called inside the effect, so a user without messaging never opens a Pusher connection. |
|
|
301
301
|
| `channelFor` | `` (id) => `sms-line.${id}` `` | Return the environment-suffixed name if the deployment shares a Pusher app. |
|
|
302
302
|
| `inboxUrl` | `'/sms'` | A row navigates to `` `${inboxUrl}?thread=<id>` ``. |
|
|
303
|
+
| `shortcut` | `'mod+shift+m'` | Opens/closes the popover from anywhere - ⌘⇧M on a Mac, Ctrl+Shift+M elsewhere (⌘M minimises the window on macOS). A string or a list, like the vault's. |
|
|
303
304
|
| `limit` | `8` | Sent as `per_page`. |
|
|
304
305
|
|
|
305
306
|
The trigger is the same 40px chip as `Notification` and `VaultQuickSearch`,
|
package/package.json
CHANGED
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
96
96
|
},
|
|
97
97
|
"name": "@visns-studio/visns-components",
|
|
98
|
-
"version": "6.
|
|
98
|
+
"version": "6.16.1",
|
|
99
99
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
100
100
|
"main": "src/index.js",
|
|
101
101
|
"files": [
|
|
@@ -9,6 +9,11 @@ import styles from '../styles/Sms.module.scss';
|
|
|
9
9
|
// works inside a Router and outside one. Imported rather than copied so there
|
|
10
10
|
// is one implementation of that decision in the library.
|
|
11
11
|
import withVaultNavigation from '../vault/vaultNavigation';
|
|
12
|
+
import {
|
|
13
|
+
matchesShortcut,
|
|
14
|
+
shortcutLabel,
|
|
15
|
+
shouldIgnoreShortcut,
|
|
16
|
+
} from '../vault/vaultHelpers';
|
|
12
17
|
import { resolveSmsEndpoints } from './smsEndpoints';
|
|
13
18
|
import useSmsLive, { defaultChannelFor } from './useSmsLive';
|
|
14
19
|
import {
|
|
@@ -56,10 +61,35 @@ const SmsInboxBadgeInner = ({
|
|
|
56
61
|
inboxUrl = '/sms',
|
|
57
62
|
limit = 8,
|
|
58
63
|
navigate,
|
|
64
|
+
// ⌘⇧M / Ctrl+Shift+M. Not ⌘M: macOS minimises the window on that. A
|
|
65
|
+
// string or a list, like the vault's; the first one is what the tooltip
|
|
66
|
+
// prints.
|
|
67
|
+
shortcut = 'mod+shift+m',
|
|
59
68
|
}) => {
|
|
60
69
|
const routes = useMemo(() => resolveSmsEndpoints(endpoints), [endpoints]);
|
|
61
70
|
|
|
62
71
|
const [open, setOpen] = useState(false);
|
|
72
|
+
|
|
73
|
+
// Global, captured: same reasoning as the vault - the point of a chord is
|
|
74
|
+
// not reaching for the mouse, and a data grid or rich editor that stops
|
|
75
|
+
// keydown propagation must not swallow it.
|
|
76
|
+
const shortcutRef = useRef(shortcut);
|
|
77
|
+
shortcutRef.current = shortcut;
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const onKeyDown = (event) => {
|
|
81
|
+
if (!matchesShortcut(event, shortcutRef.current)) return;
|
|
82
|
+
if (shouldIgnoreShortcut(event)) return;
|
|
83
|
+
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
setOpen((prev) => !prev);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
90
|
+
|
|
91
|
+
return () => window.removeEventListener('keydown', onKeyDown, true);
|
|
92
|
+
}, []);
|
|
63
93
|
const [status, setStatus] = useState(null);
|
|
64
94
|
const [lines, setLines] = useState([]);
|
|
65
95
|
const [threads, setThreads] = useState([]);
|
|
@@ -293,6 +323,7 @@ const SmsInboxBadgeInner = ({
|
|
|
293
323
|
/* --------------------------------------------------------------- render */
|
|
294
324
|
|
|
295
325
|
const label = setting?.label || 'Messages';
|
|
326
|
+
const hint = shortcutLabel(shortcut);
|
|
296
327
|
const count = badgeCount(unread);
|
|
297
328
|
|
|
298
329
|
// Two different sentences, because they mean two different things: one is
|
|
@@ -368,8 +399,9 @@ const SmsInboxBadgeInner = ({
|
|
|
368
399
|
onClick={() => (open ? close() : setOpen(true))}
|
|
369
400
|
data-tooltip-id="system-tooltip"
|
|
370
401
|
data-tooltip-place="bottom"
|
|
371
|
-
data-tooltip-content={label}
|
|
402
|
+
data-tooltip-content={hint ? `${label} · ${hint}` : label}
|
|
372
403
|
aria-label={unread > 0 ? `${label} (${unread} unread)` : label}
|
|
404
|
+
aria-keyshortcuts="Meta+Shift+M Control+Shift+M"
|
|
373
405
|
aria-expanded={open}
|
|
374
406
|
aria-haspopup="dialog"
|
|
375
407
|
>
|
|
@@ -2216,12 +2216,18 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
|
|
|
2216
2216
|
}
|
|
2217
2217
|
}
|
|
2218
2218
|
|
|
2219
|
+
// Columns, not a single list: a practice has a dozen-plus staff and one name
|
|
2220
|
+
// per line turned the dialog into a scroll box.
|
|
2219
2221
|
.staffPicker {
|
|
2220
|
-
display:
|
|
2221
|
-
|
|
2222
|
-
gap: 0.3rem;
|
|
2222
|
+
display: grid;
|
|
2223
|
+
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
|
2224
|
+
gap: 0.3rem 1rem;
|
|
2223
2225
|
box-sizing: border-box;
|
|
2224
|
-
max-height:
|
|
2226
|
+
max-height: 220px;
|
|
2227
|
+
|
|
2228
|
+
> .help {
|
|
2229
|
+
grid-column: 1 / -1;
|
|
2230
|
+
}
|
|
2225
2231
|
padding: 0.5rem 0.6rem;
|
|
2226
2232
|
border: 1px solid var(--s-line);
|
|
2227
2233
|
border-radius: var(--s-btn-br);
|
|
@@ -23,46 +23,11 @@ import {
|
|
|
23
23
|
hostFromUrl,
|
|
24
24
|
isMacPlatform,
|
|
25
25
|
matchesShortcut,
|
|
26
|
+
shouldIgnoreShortcut,
|
|
26
27
|
openableUrl,
|
|
27
28
|
shortcutLabel,
|
|
28
29
|
} from './vaultHelpers';
|
|
29
30
|
|
|
30
|
-
/** What the shortcut must not interrupt. */
|
|
31
|
-
const MODAL_SELECTOR = [
|
|
32
|
-
'dialog[open]',
|
|
33
|
-
'[aria-modal="true"]',
|
|
34
|
-
// SweetAlert, which the library uses for confirmations, and which takes
|
|
35
|
-
// the keyboard for itself while it is up.
|
|
36
|
-
'.swal2-container',
|
|
37
|
-
].join(', ');
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Should this keystroke be left alone?
|
|
41
|
-
*
|
|
42
|
-
* ⌘K is a cheap chord, which is the point of it and also the risk: it is the
|
|
43
|
-
* kind of key an editor or a spreadsheet grid binds for itself. So the vault
|
|
44
|
-
* declines it when something modal is already on screen, and when the caret is
|
|
45
|
-
* in rich text — a TinyMCE body is `contenteditable` inside an iframe, and an
|
|
46
|
-
* editor toolbar's own ⌘K (insert link) has a better claim on the keystroke
|
|
47
|
-
* than a header popover does.
|
|
48
|
-
*/
|
|
49
|
-
const shouldIgnoreShortcut = (event) => {
|
|
50
|
-
if (typeof document === 'undefined') return false;
|
|
51
|
-
|
|
52
|
-
if (document.querySelector(MODAL_SELECTOR)) return true;
|
|
53
|
-
|
|
54
|
-
const target = event.target;
|
|
55
|
-
|
|
56
|
-
// A keydown inside an iframe never reaches this window in the first place;
|
|
57
|
-
// this catches the case where the iframe itself holds focus.
|
|
58
|
-
if (target?.tagName === 'IFRAME' || document.activeElement?.tagName === 'IFRAME') {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (target?.isContentEditable) return true;
|
|
63
|
-
|
|
64
|
-
return Boolean(target?.closest?.('[contenteditable="true"], [contenteditable=""]'));
|
|
65
|
-
};
|
|
66
31
|
|
|
67
32
|
/**
|
|
68
33
|
* The vault, from the header.
|
|
@@ -545,3 +545,40 @@ export const parseTags = (input) => {
|
|
|
545
545
|
|
|
546
546
|
return out;
|
|
547
547
|
};
|
|
548
|
+
|
|
549
|
+
/** What the shortcut must not interrupt. */
|
|
550
|
+
export const MODAL_SELECTOR = [
|
|
551
|
+
'dialog[open]',
|
|
552
|
+
'[aria-modal="true"]',
|
|
553
|
+
// SweetAlert, which the library uses for confirmations, and which takes
|
|
554
|
+
// the keyboard for itself while it is up.
|
|
555
|
+
'.swal2-container',
|
|
556
|
+
].join(', ');
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Should this keystroke be left alone?
|
|
560
|
+
*
|
|
561
|
+
* ⌘K is a cheap chord, which is the point of it and also the risk: it is the
|
|
562
|
+
* kind of key an editor or a spreadsheet grid binds for itself. So the vault
|
|
563
|
+
* declines it when something modal is already on screen, and when the caret is
|
|
564
|
+
* in rich text — a TinyMCE body is `contenteditable` inside an iframe, and an
|
|
565
|
+
* editor toolbar's own ⌘K (insert link) has a better claim on the keystroke
|
|
566
|
+
* than a header popover does.
|
|
567
|
+
*/
|
|
568
|
+
export const shouldIgnoreShortcut = (event) => {
|
|
569
|
+
if (typeof document === 'undefined') return false;
|
|
570
|
+
|
|
571
|
+
if (document.querySelector(MODAL_SELECTOR)) return true;
|
|
572
|
+
|
|
573
|
+
const target = event.target;
|
|
574
|
+
|
|
575
|
+
// A keydown inside an iframe never reaches this window in the first place;
|
|
576
|
+
// this catches the case where the iframe itself holds focus.
|
|
577
|
+
if (target?.tagName === 'IFRAME' || document.activeElement?.tagName === 'IFRAME') {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (target?.isContentEditable) return true;
|
|
582
|
+
|
|
583
|
+
return Boolean(target?.closest?.('[contenteditable="true"], [contenteditable=""]'));
|
|
584
|
+
};
|