@visns-studio/visns-components 6.15.2 → 6.16.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.
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.0",
|
|
99
99
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
100
100
|
"main": "src/index.js",
|
|
101
101
|
"files": [
|
|
@@ -3141,9 +3141,9 @@ const DataGrid = forwardRef(
|
|
|
3141
3141
|
backgroundColor:
|
|
3142
3142
|
'var(--light-bg-color, #f5f5f5)',
|
|
3143
3143
|
borderRadius: '4px',
|
|
3144
|
-
padding: '
|
|
3145
|
-
width: '
|
|
3146
|
-
height: '
|
|
3144
|
+
padding: '3px',
|
|
3145
|
+
width: '26px',
|
|
3146
|
+
height: '26px',
|
|
3147
3147
|
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
|
|
3148
3148
|
margin: 0,
|
|
3149
3149
|
cursor: 'pointer',
|
|
@@ -3190,9 +3190,9 @@ const DataGrid = forwardRef(
|
|
|
3190
3190
|
backgroundColor:
|
|
3191
3191
|
'var(--light-bg-color, #f5f5f5)',
|
|
3192
3192
|
borderRadius: '4px',
|
|
3193
|
-
padding: '
|
|
3194
|
-
width: '
|
|
3195
|
-
height: '
|
|
3193
|
+
padding: '3px',
|
|
3194
|
+
width: '26px',
|
|
3195
|
+
height: '26px',
|
|
3196
3196
|
boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
|
|
3197
3197
|
margin: 0,
|
|
3198
3198
|
cursor: 'pointer',
|
|
@@ -3283,8 +3283,8 @@ const DataGrid = forwardRef(
|
|
|
3283
3283
|
key={`coding-slot-${config.id[0]}-${configKey}`}
|
|
3284
3284
|
style={{
|
|
3285
3285
|
display: 'inline-flex',
|
|
3286
|
-
width: '
|
|
3287
|
-
height: '
|
|
3286
|
+
width: '26px',
|
|
3287
|
+
height: '26px',
|
|
3288
3288
|
margin: '0 1px',
|
|
3289
3289
|
}}
|
|
3290
3290
|
/>
|
|
@@ -3296,13 +3296,14 @@ const DataGrid = forwardRef(
|
|
|
3296
3296
|
return (
|
|
3297
3297
|
<div
|
|
3298
3298
|
style={{
|
|
3299
|
-
//
|
|
3300
|
-
//
|
|
3299
|
+
// One row of fixed slots, left-aligned so every slot
|
|
3300
|
+
// lands in the same place on every row (the other
|
|
3301
|
+
// coding columns - clients, workflow - read that way).
|
|
3301
3302
|
display: 'flex',
|
|
3302
3303
|
flexWrap: 'nowrap',
|
|
3303
3304
|
alignItems: 'center',
|
|
3304
|
-
justifyContent: '
|
|
3305
|
-
gap: '
|
|
3305
|
+
justifyContent: 'flex-start',
|
|
3306
|
+
gap: '2px',
|
|
3306
3307
|
width: '100%',
|
|
3307
3308
|
padding: 0,
|
|
3308
3309
|
minHeight: '24px',
|
|
@@ -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
|
>
|
|
@@ -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
|
+
};
|