@visns-studio/visns-components 6.24.4 → 6.26.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/package.json +4 -2
- package/src/components/Autocomplete.jsx +189 -119
- package/src/components/DataGrid.jsx +472 -31
- package/src/components/Navigation.jsx +475 -51
- package/src/components/auth/ClientAuthFrame.jsx +5 -0
- package/src/components/auth/ClientAuthScreen.jsx +29 -0
- package/src/components/callQueue/CallQueueDiagnostics.jsx +1043 -0
- package/src/components/callQueue/CallQueuePop.jsx +713 -90
- package/src/components/callQueue/CallQueueSettings.jsx +308 -146
- package/src/components/callQueue/callPopStatus.js +236 -0
- package/src/components/callQueue/callQueueHelpers.js +284 -2
- package/src/components/columns/ColumnRenderers.jsx +3 -46
- package/src/components/columns/StackedRow.jsx +186 -0
- package/src/components/controls/DataGridSearch.jsx +110 -2
- package/src/components/controls/DataGridSortSheet.jsx +155 -0
- package/src/components/generic/GenericAuth.jsx +50 -18
- package/src/components/generic/GenericDashboard.jsx +20 -1
- package/src/components/generic/GenericDetail.jsx +446 -259
- package/src/components/mapboxSearchBox.js +640 -0
- package/src/components/navBadges.js +63 -1
- package/src/components/navDrawer.js +147 -0
- package/src/components/sms/SmsThreadPanel.jsx +34 -6
- package/src/components/sms/smsHelpers.js +15 -0
- package/src/components/styles/CallQueueDiagnostics.module.scss +398 -0
- package/src/components/styles/CallQueuePop.module.scss +29 -0
- package/src/components/styles/CallQueueSettings.module.scss +93 -0
- package/src/components/styles/ClientAuth.module.scss +39 -0
- package/src/components/styles/DataGrid.module.scss +158 -5
- package/src/components/styles/Field.module.scss +52 -1
- package/src/components/styles/Form.module.scss +82 -0
- package/src/components/styles/GenericClientPortal.module.scss +72 -20
- package/src/components/styles/GenericDashboard.module.scss +50 -0
- package/src/components/styles/GenericDetail.module.scss +63 -1
- package/src/components/styles/GenericDynamic.module.scss +23 -0
- package/src/components/styles/GenericFormBuilder.module.scss +11 -0
- package/src/components/styles/GenericIndex.module.scss +6 -1
- package/src/components/styles/Navigation.module.scss +460 -7
- package/src/components/styles/Sms.module.scss +92 -0
- package/src/components/styles/StackedRow.module.scss +182 -0
- package/src/components/styles/TicketConversation.module.scss +76 -0
- package/src/components/styles/Vault.module.scss +192 -0
- package/src/components/styles/density.css +10 -0
- package/src/components/styles/global-datagrid.css +163 -0
- package/src/components/styles/global.css +20 -0
- package/src/components/tickets/TicketConversation.jsx +13 -8
- package/src/components/utils/ConfirmDialog.js +22 -3
- package/src/components/utils/cardLayout.js +666 -0
- package/src/components/utils/contactChannels.js +130 -0
- package/src/components/utils/editPlacement.js +95 -0
- package/src/components/utils/useDensity.js +303 -7
- package/src/index.js +42 -0
|
@@ -33,9 +33,99 @@ import Notification from './Notification';
|
|
|
33
33
|
import SwitchAccount from './SwitchAccount';
|
|
34
34
|
import BusinessCardOcr from './BusinessCardOcr';
|
|
35
35
|
import { isNavActive, bestMatchingUrl, cleanUrl } from './navActive';
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
navBadge,
|
|
38
|
+
badgeFor,
|
|
39
|
+
badgeAriaLabel,
|
|
40
|
+
DEFAULT_BADGE_TONE,
|
|
41
|
+
} from './navBadges';
|
|
42
|
+
import { drawerRows } from './navDrawer';
|
|
37
43
|
import styles from './styles/Navigation.module.scss';
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Where the header layout stops being a bar and becomes a drawer.
|
|
47
|
+
*
|
|
48
|
+
* The same 1024px the sidebar layouts have always used (see the media block
|
|
49
|
+
* in Navigation.module.scss), stated once here because the JS and the
|
|
50
|
+
* stylesheet now have to agree: below it the nav strip is hidden by CSS and
|
|
51
|
+
* the drawer is mounted by JS, and a disagreement is a screen with no
|
|
52
|
+
* navigation on it at all.
|
|
53
|
+
*/
|
|
54
|
+
const DRAWER_BREAKPOINT = '(max-width: 1024px)';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A pointer that cannot hover. iPads in landscape are 1024px wide — above the
|
|
58
|
+
* drawer breakpoint — so the DESKTOP bar is what a large tablet gets, and its
|
|
59
|
+
* group dropdowns opened on `:hover` alone. This is the query that tells the
|
|
60
|
+
* bar to drive them from state instead. See the group `<button>` below.
|
|
61
|
+
*/
|
|
62
|
+
const COARSE_POINTER = '(hover: none)';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The account actions that stay in the bar once the drawer exists, by
|
|
66
|
+
* settings id. Everything else moves into the drawer.
|
|
67
|
+
*
|
|
68
|
+
* THREE, AND WHICH THREE IS THE DECISION: search, the notification bell and
|
|
69
|
+
* the ticket-queue count. Two of them carry a number that is the reason to
|
|
70
|
+
* look at the bar at all — a badge inside a closed drawer counts nothing —
|
|
71
|
+
* and search is the fastest route to any record in the CRM, which is what a
|
|
72
|
+
* one-handed reader on a phone is almost always after. The rest (messages,
|
|
73
|
+
* calendar, vault, settings, profile, sign out) are places you go, not
|
|
74
|
+
* numbers you watch, and they read better as full-width labelled rows than as
|
|
75
|
+
* unlabelled 34px circles crushed against the edge of a 390px screen.
|
|
76
|
+
*
|
|
77
|
+
* An id that is not in this list and not in the drawer would simply vanish,
|
|
78
|
+
* so the two are derived from ONE array: the bar renders the ids in it, the
|
|
79
|
+
* drawer renders the rest. A project whose ids differ from these keeps every
|
|
80
|
+
* action — they all fall through to the drawer.
|
|
81
|
+
*
|
|
82
|
+
* It is also what stops a component being mounted twice. `renderers` entries
|
|
83
|
+
* such as a universal-search palette or an SMS badge register global
|
|
84
|
+
* keystrokes and open websockets; a bar copy hidden with `display: none`
|
|
85
|
+
* beside a live drawer copy would run both.
|
|
86
|
+
*/
|
|
87
|
+
const MOBILE_BAR_ACTIONS = ['search', 'notifications', 'ticketQueue'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A media query as React state.
|
|
91
|
+
*
|
|
92
|
+
* Read SYNCHRONOUSLY in the initialiser, not in an effect: the first paint
|
|
93
|
+
* decides whether the bar draws nine chips or three, and a render at the
|
|
94
|
+
* wrong width followed by a correction is a visible flash of the desktop bar
|
|
95
|
+
* on every page load on a phone. The effect only keeps it honest afterwards —
|
|
96
|
+
* on rotation, on a resized window, on a desktop browser's device toolbar.
|
|
97
|
+
*
|
|
98
|
+
* Guarded for a window-less render even though nothing here runs on a server
|
|
99
|
+
* today; the guard costs one comparison and the alternative is a crash.
|
|
100
|
+
*/
|
|
101
|
+
const useMediaQuery = (query) => {
|
|
102
|
+
const [matches, setMatches] = useState(() =>
|
|
103
|
+
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
104
|
+
? window.matchMedia(query).matches
|
|
105
|
+
: false
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (
|
|
110
|
+
typeof window === 'undefined' ||
|
|
111
|
+
typeof window.matchMedia !== 'function'
|
|
112
|
+
) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mql = window.matchMedia(query);
|
|
117
|
+
const onChange = (event) => setMatches(event.matches);
|
|
118
|
+
|
|
119
|
+
// The width may have changed between the initialiser and this effect.
|
|
120
|
+
setMatches(mql.matches);
|
|
121
|
+
mql.addEventListener('change', onChange);
|
|
122
|
+
|
|
123
|
+
return () => mql.removeEventListener('change', onChange);
|
|
124
|
+
}, [query]);
|
|
125
|
+
|
|
126
|
+
return matches;
|
|
127
|
+
};
|
|
128
|
+
|
|
39
129
|
// Opt-in icons for the sidebar (simple/cms) layouts: a nav entry names one
|
|
40
130
|
// via its `icon` key, falling back to its `id`. Unknown names render
|
|
41
131
|
// nothing, and the header layout never renders these, so existing nav
|
|
@@ -133,10 +223,25 @@ const actionLabel = (n) => {
|
|
|
133
223
|
*
|
|
134
224
|
* Renders nothing without a badge, so every caller can hand it whatever
|
|
135
225
|
* navBadge() returned without guarding first.
|
|
226
|
+
*
|
|
227
|
+
* The tone becomes a modifier class and only when it is not the default: `ok`
|
|
228
|
+
* IS the base rule, so a `.nav-badge-ok` would be an empty block that every
|
|
229
|
+
* pill in the bar carried for nothing. navBadges.js has already reduced an
|
|
230
|
+
* unrecognised tone to the default, so the lookup below cannot miss.
|
|
136
231
|
*/
|
|
137
232
|
const BadgePill = ({ badge }) =>
|
|
138
233
|
badge ? (
|
|
139
|
-
<span
|
|
234
|
+
<span
|
|
235
|
+
className={[
|
|
236
|
+
styles['nav-badge'],
|
|
237
|
+
badge.tone && badge.tone !== DEFAULT_BADGE_TONE
|
|
238
|
+
? styles[`nav-badge-${badge.tone}`]
|
|
239
|
+
: '',
|
|
240
|
+
]
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.join(' ')}
|
|
243
|
+
aria-hidden="true"
|
|
244
|
+
>
|
|
140
245
|
{badge.display}
|
|
141
246
|
</span>
|
|
142
247
|
) : null;
|
|
@@ -325,9 +430,11 @@ function Navigation({
|
|
|
325
430
|
renderers = {},
|
|
326
431
|
/*
|
|
327
432
|
* Live counts for navigation items: `{ [navId]: number }`, or the object
|
|
328
|
-
* form `{ [navId]: { count, label } }` when the accessible name
|
|
329
|
-
* noun other than "open"
|
|
330
|
-
*
|
|
433
|
+
* form `{ [navId]: { count, label, tone } }` when the accessible name
|
|
434
|
+
* needs a noun other than "open", or the number needs a severity —
|
|
435
|
+
* `tone: 'ok' | 'warn' | 'danger'`, default 'ok', which is the green pill
|
|
436
|
+
* everything wore before tones existed. See navBadges.js, which owns every
|
|
437
|
+
* rule about what renders and what does not.
|
|
331
438
|
*
|
|
332
439
|
* Empty by default and additive: an id with no entry, a zero, or a value
|
|
333
440
|
* that will not parse draws nothing at all, so a consumer that publishes
|
|
@@ -349,6 +456,13 @@ function Navigation({
|
|
|
349
456
|
const currentPage = location.pathname.substr(1) || 'dashboard';
|
|
350
457
|
const [isOpen, setOpen] = useState(false);
|
|
351
458
|
|
|
459
|
+
// Below 1024px BOTH layouts are a drawer: the sidebar layouts slide their
|
|
460
|
+
// rail in, and the header layout — which has no rail — mounts one. Read as
|
|
461
|
+
// state rather than left to CSS because the two surfaces must not both
|
|
462
|
+
// render the same account action (see MOBILE_BAR_ACTIONS).
|
|
463
|
+
const isNarrow = useMediaQuery(DRAWER_BREAKPOINT);
|
|
464
|
+
const isCoarsePointer = useMediaQuery(COARSE_POINTER);
|
|
465
|
+
|
|
352
466
|
// The mobile drawer is a full-viewport fixed overlay; without a scroll
|
|
353
467
|
// lock the page behind it keeps scrolling under the user's thumb.
|
|
354
468
|
useEffect(() => {
|
|
@@ -361,6 +475,32 @@ function Navigation({
|
|
|
361
475
|
}
|
|
362
476
|
return undefined;
|
|
363
477
|
}, [isOpen]);
|
|
478
|
+
|
|
479
|
+
// Widening past the breakpoint takes the drawer away — including, in the
|
|
480
|
+
// header layout, the element it was rendered into. Left open, the state
|
|
481
|
+
// would be true with nothing on screen holding it, and the scroll lock
|
|
482
|
+
// above would keep the page frozen with no way to release it.
|
|
483
|
+
useEffect(() => {
|
|
484
|
+
if (!isNarrow) {
|
|
485
|
+
setOpen(false);
|
|
486
|
+
}
|
|
487
|
+
}, [isNarrow]);
|
|
488
|
+
|
|
489
|
+
/*
|
|
490
|
+
* A page change closes the drawer, whoever asked for it.
|
|
491
|
+
*
|
|
492
|
+
* Every link IN the drawer already closes it on the way out, and that is
|
|
493
|
+
* not enough: the account actions at the foot of it are mounted
|
|
494
|
+
* components — an SMS inbox, a credential palette — that open a panel of
|
|
495
|
+
* their own and then navigate from inside it, with nothing passing
|
|
496
|
+
* through those handlers. The drawer stayed sitting over the page they
|
|
497
|
+
* had just taken you to. Keyed on the PATH only, so a screen that
|
|
498
|
+
* rewrites its own query string cannot slam the menu shut mid-scroll.
|
|
499
|
+
*/
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
setOpen(false);
|
|
502
|
+
}, [currentPage]);
|
|
503
|
+
|
|
364
504
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
365
505
|
const [navData, setNavData] = useState(navConfig);
|
|
366
506
|
const [showBusinessCardOcr, setShowBusinessCardOcr] = useState(false);
|
|
@@ -390,6 +530,18 @@ function Navigation({
|
|
|
390
530
|
);
|
|
391
531
|
};
|
|
392
532
|
|
|
533
|
+
/**
|
|
534
|
+
* One account action as a header chip.
|
|
535
|
+
*
|
|
536
|
+
* Every branch's `<li>` carries `data-setting={n.id}`. Nothing in this
|
|
537
|
+
* library styles off it — WHICH chips survive the drawer breakpoint is a
|
|
538
|
+
* JS decision (MOBILE_BAR_ACTIONS), because a chip hidden in CSS is still
|
|
539
|
+
* a mounted component holding a websocket and a global keystroke. It is
|
|
540
|
+
* here so a consuming app can reach a chip it mounted by name from its own
|
|
541
|
+
* stylesheet, and so the cluster is legible in the inspector: nine
|
|
542
|
+
* identical `<li><a>` with an icon inside told you nothing about which was
|
|
543
|
+
* which.
|
|
544
|
+
*/
|
|
393
545
|
const renderSetting = (n) => {
|
|
394
546
|
const iconComponents = SETTING_ICONS;
|
|
395
547
|
|
|
@@ -403,7 +555,7 @@ function Navigation({
|
|
|
403
555
|
// A consumer-supplied renderer wins over the built-in chain.
|
|
404
556
|
if (typeof renderers[n.id] === 'function') {
|
|
405
557
|
return (
|
|
406
|
-
<li>
|
|
558
|
+
<li data-setting={n.id}>
|
|
407
559
|
{renderers[n.id]({
|
|
408
560
|
setting: n,
|
|
409
561
|
userProfile,
|
|
@@ -415,13 +567,13 @@ function Navigation({
|
|
|
415
567
|
|
|
416
568
|
if (n.id === 'notifications') {
|
|
417
569
|
return (
|
|
418
|
-
<li>
|
|
570
|
+
<li data-setting={n.id}>
|
|
419
571
|
<Notification setSystemAuth={setSystemAuth} />
|
|
420
572
|
</li>
|
|
421
573
|
);
|
|
422
574
|
} else if (n.id === 'businessCardOcr') {
|
|
423
575
|
return (
|
|
424
|
-
<li>
|
|
576
|
+
<li data-setting={n.id}>
|
|
425
577
|
<button
|
|
426
578
|
type="button"
|
|
427
579
|
data-tooltip-id="system-tooltip"
|
|
@@ -437,7 +589,7 @@ function Navigation({
|
|
|
437
589
|
);
|
|
438
590
|
} else if (n.id === 'switchAccount') {
|
|
439
591
|
return (
|
|
440
|
-
<li>
|
|
592
|
+
<li data-setting={n.id}>
|
|
441
593
|
<SwitchAccount
|
|
442
594
|
setSystemAuth={setSystemAuth}
|
|
443
595
|
setting={n}
|
|
@@ -449,7 +601,7 @@ function Navigation({
|
|
|
449
601
|
const IconComponent = iconComponents[n.icon];
|
|
450
602
|
const label = actionLabel(n);
|
|
451
603
|
return (
|
|
452
|
-
<li>
|
|
604
|
+
<li data-setting={n.id}>
|
|
453
605
|
<button
|
|
454
606
|
type="button"
|
|
455
607
|
data-tooltip-id="system-tooltip"
|
|
@@ -475,7 +627,7 @@ function Navigation({
|
|
|
475
627
|
const settingBadge = badgeFor(badges, n.id);
|
|
476
628
|
|
|
477
629
|
return (
|
|
478
|
-
<li>
|
|
630
|
+
<li data-setting={n.id}>
|
|
479
631
|
<Link
|
|
480
632
|
to={n.url}
|
|
481
633
|
data-tooltip-id="system-tooltip"
|
|
@@ -602,9 +754,59 @@ function Navigation({
|
|
|
602
754
|
);
|
|
603
755
|
};
|
|
604
756
|
|
|
605
|
-
|
|
757
|
+
/**
|
|
758
|
+
* Which group's dropdown a TOUCH reader has opened.
|
|
759
|
+
*
|
|
760
|
+
* Null on a mouse, always: hover and `:focus-within` open the desktop menu
|
|
761
|
+
* in CSS and this state is not consulted (see the `aria-expanded` note on
|
|
762
|
+
* the group button). It exists because a coarse pointer has neither — the
|
|
763
|
+
* old code even blurred the button on click, so on a touch screen the menu
|
|
764
|
+
* opened for exactly as long as the tap lasted.
|
|
765
|
+
*
|
|
766
|
+
* This replaces an `activeDropdown` state whose value was discarded at the
|
|
767
|
+
* point of declaration (`const [, setActiveDropdown]`): it re-rendered the
|
|
768
|
+
* whole bar on every hover to change nothing, because the only thing it
|
|
769
|
+
* did was toggle the scroll container's overflow, which is a DOM poke.
|
|
770
|
+
* That poke is `liftNavClipping` now, and it is called from the tap path
|
|
771
|
+
* as well — the dropdown was clipped by the strip's `overflow-x: auto`
|
|
772
|
+
* everywhere the pointer had not been.
|
|
773
|
+
*/
|
|
774
|
+
const [openGroup, setOpenGroup] = useState(null);
|
|
606
775
|
const navWrapRef = useRef(null);
|
|
607
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Let an open dropdown out of the horizontally-scrolling nav strip.
|
|
779
|
+
*
|
|
780
|
+
* The strip is `overflow-x: auto`, and a scroll container clips on BOTH
|
|
781
|
+
* axes however `visible` the other one is written — so a menu hanging
|
|
782
|
+
* below the bar is cut off at the bar's own bottom edge.
|
|
783
|
+
*/
|
|
784
|
+
const liftNavClipping = (lift) => {
|
|
785
|
+
if (navWrapRef.current) {
|
|
786
|
+
navWrapRef.current.style.overflowX = lift ? 'visible' : 'auto';
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// A tap anywhere else closes the group a touch reader opened. Without it
|
|
791
|
+
// the only way to dismiss the menu is to tap the heading again, which is
|
|
792
|
+
// not how any other menu on the device behaves. Mouse users never get
|
|
793
|
+
// here: `openGroup` is only ever set on a coarse pointer.
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
if (openGroup === null) {
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const onDown = (event) => {
|
|
800
|
+
if (!navWrapRef.current?.contains(event.target)) {
|
|
801
|
+
setOpenGroup(null);
|
|
802
|
+
liftNavClipping(false);
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
document.addEventListener('pointerdown', onDown);
|
|
807
|
+
return () => document.removeEventListener('pointerdown', onDown);
|
|
808
|
+
}, [openGroup]);
|
|
809
|
+
|
|
608
810
|
const renderNav = (n) => {
|
|
609
811
|
let navItemClasses;
|
|
610
812
|
const NavItemIcon =
|
|
@@ -628,21 +830,13 @@ function Navigation({
|
|
|
628
830
|
|
|
629
831
|
const handleMouseEnter = () => {
|
|
630
832
|
if (hasChildren) {
|
|
631
|
-
|
|
632
|
-
// Temporarily disable overflow on the navwrap when dropdown is active
|
|
633
|
-
if (navWrapRef.current) {
|
|
634
|
-
navWrapRef.current.style.overflowX = 'visible';
|
|
635
|
-
}
|
|
833
|
+
liftNavClipping(true);
|
|
636
834
|
}
|
|
637
835
|
};
|
|
638
836
|
|
|
639
837
|
const handleMouseLeave = () => {
|
|
640
838
|
if (hasChildren) {
|
|
641
|
-
|
|
642
|
-
// Restore overflow when dropdown is no longer active
|
|
643
|
-
if (navWrapRef.current) {
|
|
644
|
-
navWrapRef.current.style.overflowX = 'auto';
|
|
645
|
-
}
|
|
839
|
+
liftNavClipping(false);
|
|
646
840
|
}
|
|
647
841
|
};
|
|
648
842
|
|
|
@@ -679,7 +873,15 @@ function Navigation({
|
|
|
679
873
|
? styles.active
|
|
680
874
|
: ''
|
|
681
875
|
}
|
|
682
|
-
|
|
876
|
+
/* Closes both menus this row can sit
|
|
877
|
+
inside: the drawer, and — on a
|
|
878
|
+
touch screen — the dropdown the tap
|
|
879
|
+
on the heading opened. */
|
|
880
|
+
onClick={() => {
|
|
881
|
+
setOpen(false);
|
|
882
|
+
setOpenGroup(null);
|
|
883
|
+
liftNavClipping(false);
|
|
884
|
+
}}
|
|
683
885
|
>
|
|
684
886
|
{child.label}
|
|
685
887
|
<BadgePill badge={childBadge} />
|
|
@@ -735,25 +937,49 @@ function Navigation({
|
|
|
735
937
|
* screen. A button takes focus, and the stylesheet opens
|
|
736
938
|
* the dropdown on :focus-within as well as :hover.
|
|
737
939
|
*
|
|
738
|
-
*
|
|
739
|
-
* in
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
* state this component does not hold would be a lie to
|
|
940
|
+
* ON A FINE POINTER the open state still lives in CSS, not
|
|
941
|
+
* in React, and `aria-expanded` is therefore absent: the
|
|
942
|
+
* menu opens on hover, this component is never told, and
|
|
943
|
+
* publishing a state it does not hold would be a lie to
|
|
743
944
|
* the screen reader rather than a help.
|
|
945
|
+
*
|
|
946
|
+
* ON A COARSE POINTER there is no hover to open it with,
|
|
947
|
+
* so the tap does — and then React DOES hold the state,
|
|
948
|
+
* `aria-expanded` is real, and the stylesheet opens the
|
|
949
|
+
* menu off that attribute. Same markup, two mechanisms,
|
|
950
|
+
* each honest about the input it is serving.
|
|
744
951
|
*/
|
|
745
952
|
<button
|
|
746
953
|
type="button"
|
|
747
954
|
aria-haspopup="true"
|
|
955
|
+
aria-expanded={
|
|
956
|
+
isCoarsePointer ? openGroup === n.id : undefined
|
|
957
|
+
}
|
|
748
958
|
aria-label={ariaLabel}
|
|
749
|
-
/*
|
|
750
|
-
click leaves the button
|
|
751
|
-
opened stayed pinned while
|
|
752
|
-
opened that one too — two
|
|
753
|
-
on pointer clicks only:
|
|
754
|
-
count, 0 for keyboard
|
|
755
|
-
users keep the focus that
|
|
959
|
+
/* Fine pointer: the menu opens on :hover OR
|
|
960
|
+
:focus-within. A MOUSE click leaves the button
|
|
961
|
+
focused, so the menu it opened stayed pinned while
|
|
962
|
+
hovering the next group opened that one too — two
|
|
963
|
+
dropdowns at once. Blur on pointer clicks only:
|
|
964
|
+
e.detail is the click count, 0 for keyboard
|
|
965
|
+
"clicks", so Tab + Enter users keep the focus that
|
|
966
|
+
holds their menu open.
|
|
967
|
+
|
|
968
|
+
Coarse pointer: NEVER blur — the blur is precisely
|
|
969
|
+
what made the menu impossible to open by tapping,
|
|
970
|
+
since the tap that opened it removed the focus
|
|
971
|
+
holding it open in the same gesture. Toggle instead,
|
|
972
|
+
and lift the strip's clipping the way a hover does,
|
|
973
|
+
or the menu opens inside a scroll container that
|
|
974
|
+
cuts it off at the bar's bottom edge. */
|
|
756
975
|
onClick={(e) => {
|
|
976
|
+
if (isCoarsePointer) {
|
|
977
|
+
const next = openGroup === n.id ? null : n.id;
|
|
978
|
+
setOpenGroup(next);
|
|
979
|
+
liftNavClipping(next !== null);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
757
983
|
if (e.detail > 0) {
|
|
758
984
|
e.currentTarget.blur();
|
|
759
985
|
}
|
|
@@ -1071,6 +1297,169 @@ function Navigation({
|
|
|
1071
1297
|
</div>
|
|
1072
1298
|
);
|
|
1073
1299
|
|
|
1300
|
+
/* ------------------------------------------------------------------ *
|
|
1301
|
+
* The header layout's drawer (≤1024px)
|
|
1302
|
+
* ------------------------------------------------------------------ *
|
|
1303
|
+
* Everything below this comment is for the `full` layout — the one-line
|
|
1304
|
+
* bar — and exists because it had NO mobile story at all: the hamburger,
|
|
1305
|
+
* the sliding panel, the backdrop and the body scroll lock were all
|
|
1306
|
+
* written for the sidebar layouts, and the bar's answer to a 390px screen
|
|
1307
|
+
* was a horizontally-scrolling strip with the scrollbar hidden. Nine
|
|
1308
|
+
* account chips at `width: max-content` then set the bar's minimum width
|
|
1309
|
+
* at roughly 530px, which is wider than the phone: the navigation column
|
|
1310
|
+
* collapsed to nothing and the whole page panned sideways.
|
|
1311
|
+
*
|
|
1312
|
+
* So the drawer machinery is REUSED rather than rebuilt — the same
|
|
1313
|
+
* `aside--cms` panel, the same `aside--cms--open` transform, the same
|
|
1314
|
+
* `drawer-backdrop`, the same `rail-actions` footer and the same
|
|
1315
|
+
* `isOpen` state and scroll-lock effect that were already in scope. What
|
|
1316
|
+
* is new is the flat row list (groups have no dropdown to open here; see
|
|
1317
|
+
* navDrawer.js) and the anchor: this drawer hangs off the bottom of the
|
|
1318
|
+
* header bar rather than the 50px content header, which is what
|
|
1319
|
+
* `--nav-header-height` in the stylesheet measures.
|
|
1320
|
+
*/
|
|
1321
|
+
const headerLayout = layout !== 'simple' && layout !== 'cms';
|
|
1322
|
+
const hasDrawer = headerLayout && isNarrow;
|
|
1323
|
+
|
|
1324
|
+
/*
|
|
1325
|
+
* The account actions, split in two.
|
|
1326
|
+
*
|
|
1327
|
+
* Above the breakpoint there is no split: `barActions` is every action
|
|
1328
|
+
* and `drawerActions` is empty, so the desktop bar renders exactly what
|
|
1329
|
+
* it always rendered. Below it, an action appears in ONE of the two
|
|
1330
|
+
* surfaces and never both — see MOBILE_BAR_ACTIONS for why that matters
|
|
1331
|
+
* more than it looks like it should.
|
|
1332
|
+
*/
|
|
1333
|
+
const visibleSettings = navData.settings.filter(
|
|
1334
|
+
(setting) => setting.hidden !== true && setting.permission === true
|
|
1335
|
+
);
|
|
1336
|
+
const barActions = hasDrawer
|
|
1337
|
+
? visibleSettings.filter((setting) =>
|
|
1338
|
+
MOBILE_BAR_ACTIONS.includes(setting.id)
|
|
1339
|
+
)
|
|
1340
|
+
: visibleSettings;
|
|
1341
|
+
const drawerActions = hasDrawer
|
|
1342
|
+
? visibleSettings.filter(
|
|
1343
|
+
(setting) => !MOBILE_BAR_ACTIONS.includes(setting.id)
|
|
1344
|
+
)
|
|
1345
|
+
: [];
|
|
1346
|
+
|
|
1347
|
+
const headerDrawer = !hasDrawer ? null : (
|
|
1348
|
+
<>
|
|
1349
|
+
<aside
|
|
1350
|
+
id="nav-drawer"
|
|
1351
|
+
aria-label="Main menu"
|
|
1352
|
+
className={[
|
|
1353
|
+
styles['aside--cms'],
|
|
1354
|
+
styles['aside--drawer'],
|
|
1355
|
+
isOpen ? styles['aside--cms--open'] : '',
|
|
1356
|
+
]
|
|
1357
|
+
.filter(Boolean)
|
|
1358
|
+
.join(' ')}
|
|
1359
|
+
>
|
|
1360
|
+
<nav className={styles['drawer-nav']}>
|
|
1361
|
+
<ul>
|
|
1362
|
+
{drawerRows(navData.navigations, currentPage).map(
|
|
1363
|
+
(row, rowKey) => {
|
|
1364
|
+
/*
|
|
1365
|
+
* `badgeFor`, not `navBadge`: the group's own
|
|
1366
|
+
* count, never a roll-up of its children's.
|
|
1367
|
+
* The bar rolls a group up because its
|
|
1368
|
+
* children are hidden inside a closed menu;
|
|
1369
|
+
* here they are rows of their own, two
|
|
1370
|
+
* fingers below, each wearing its own pill —
|
|
1371
|
+
* so a roll-up would show the same work
|
|
1372
|
+
* twice and the totals would not add up.
|
|
1373
|
+
*/
|
|
1374
|
+
const rowBadge = badgeFor(badges, row.id);
|
|
1375
|
+
|
|
1376
|
+
return (
|
|
1377
|
+
<li
|
|
1378
|
+
key={`drawer-row-${rowKey}`}
|
|
1379
|
+
className={
|
|
1380
|
+
row.depth > 0
|
|
1381
|
+
? styles['drawer-child']
|
|
1382
|
+
: undefined
|
|
1383
|
+
}
|
|
1384
|
+
>
|
|
1385
|
+
{row.kind === 'heading' ? (
|
|
1386
|
+
/* Inert on purpose. A group
|
|
1387
|
+
parent has no page, so there is
|
|
1388
|
+
nothing to navigate to and
|
|
1389
|
+
nothing to disclose — the rows
|
|
1390
|
+
it names are already open
|
|
1391
|
+
underneath it. */
|
|
1392
|
+
<span
|
|
1393
|
+
className={
|
|
1394
|
+
styles['drawer-heading']
|
|
1395
|
+
}
|
|
1396
|
+
>
|
|
1397
|
+
{row.label}
|
|
1398
|
+
</span>
|
|
1399
|
+
) : (
|
|
1400
|
+
<Link
|
|
1401
|
+
to={row.url}
|
|
1402
|
+
target={
|
|
1403
|
+
row.target
|
|
1404
|
+
? row.target
|
|
1405
|
+
: '_self'
|
|
1406
|
+
}
|
|
1407
|
+
className={
|
|
1408
|
+
row.active
|
|
1409
|
+
? styles.active
|
|
1410
|
+
: ''
|
|
1411
|
+
}
|
|
1412
|
+
aria-label={badgeAriaLabel(
|
|
1413
|
+
row.label,
|
|
1414
|
+
rowBadge
|
|
1415
|
+
)}
|
|
1416
|
+
onClick={() => setOpen(false)}
|
|
1417
|
+
>
|
|
1418
|
+
<span>{row.label}</span>
|
|
1419
|
+
<BadgePill badge={rowBadge} />
|
|
1420
|
+
</Link>
|
|
1421
|
+
)}
|
|
1422
|
+
</li>
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
)}
|
|
1426
|
+
</ul>
|
|
1427
|
+
</nav>
|
|
1428
|
+
|
|
1429
|
+
{/* The same footer the sidebar rail uses, carrying the
|
|
1430
|
+
actions the bar gave up — `renderRailSetting` already
|
|
1431
|
+
knows how to draw every one of them as a labelled row,
|
|
1432
|
+
mounted components included, and already closes the
|
|
1433
|
+
drawer when one is tapped. `data-nav-actions` is what
|
|
1434
|
+
tells those components' own stylesheets to open their
|
|
1435
|
+
panels away from a header chip they are no longer in. */}
|
|
1436
|
+
<div
|
|
1437
|
+
className={styles['rail-actions']}
|
|
1438
|
+
data-nav-actions="sidebar"
|
|
1439
|
+
>
|
|
1440
|
+
<ul>
|
|
1441
|
+
{drawerActions.map((setting, settingKey) => (
|
|
1442
|
+
<React.Fragment
|
|
1443
|
+
key={`drawer-setting-${settingKey}`}
|
|
1444
|
+
>
|
|
1445
|
+
{renderRailSetting(setting)}
|
|
1446
|
+
</React.Fragment>
|
|
1447
|
+
))}
|
|
1448
|
+
</ul>
|
|
1449
|
+
</div>
|
|
1450
|
+
</aside>
|
|
1451
|
+
|
|
1452
|
+
{isOpen ? (
|
|
1453
|
+
<button
|
|
1454
|
+
type="button"
|
|
1455
|
+
className={styles['drawer-backdrop']}
|
|
1456
|
+
aria-label="Close menu"
|
|
1457
|
+
onClick={() => setOpen(false)}
|
|
1458
|
+
/>
|
|
1459
|
+
) : null}
|
|
1460
|
+
</>
|
|
1461
|
+
);
|
|
1462
|
+
|
|
1074
1463
|
return layout === 'simple' || layout === 'cms' ? (
|
|
1075
1464
|
<div className={styles.cwrap}>
|
|
1076
1465
|
<aside
|
|
@@ -1199,6 +1588,35 @@ function Navigation({
|
|
|
1199
1588
|
</div>
|
|
1200
1589
|
) : (
|
|
1201
1590
|
<div className={styles.hwrap}>
|
|
1591
|
+
{/* First in the source, and therefore first in the tab order and
|
|
1592
|
+
first in the bar's grid — the thumb reaches the left edge of a
|
|
1593
|
+
phone and the menu is the thing it is reaching for. Rendered
|
|
1594
|
+
only below the breakpoint: above it the strip beside the logo
|
|
1595
|
+
IS the menu, and a hamburger next to a visible menu is two
|
|
1596
|
+
doors into one room. */}
|
|
1597
|
+
{hasDrawer ? (
|
|
1598
|
+
<button
|
|
1599
|
+
type="button"
|
|
1600
|
+
className={[
|
|
1601
|
+
styles.mobmenu,
|
|
1602
|
+
styles.hmenu,
|
|
1603
|
+
isOpen ? styles['mobmenu--open'] : '',
|
|
1604
|
+
]
|
|
1605
|
+
.filter(Boolean)
|
|
1606
|
+
.join(' ')}
|
|
1607
|
+
aria-label={isOpen ? 'Close menu' : 'Open menu'}
|
|
1608
|
+
aria-expanded={isOpen}
|
|
1609
|
+
aria-controls="nav-drawer"
|
|
1610
|
+
onClick={() => setOpen(!isOpen)}
|
|
1611
|
+
>
|
|
1612
|
+
<span></span>
|
|
1613
|
+
<span></span>
|
|
1614
|
+
<span></span>
|
|
1615
|
+
<span></span>
|
|
1616
|
+
<span></span>
|
|
1617
|
+
<span></span>
|
|
1618
|
+
</button>
|
|
1619
|
+
) : null}
|
|
1202
1620
|
<div className={styles.logo}>
|
|
1203
1621
|
<Link to="/">
|
|
1204
1622
|
<img src={logo} alt="System Logo" />
|
|
@@ -1217,25 +1635,31 @@ function Navigation({
|
|
|
1217
1635
|
)}
|
|
1218
1636
|
</ul>
|
|
1219
1637
|
</div>
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1638
|
+
{/* The cluster draws a pill-shaped tray behind its chips, so an
|
|
1639
|
+
empty one is a small blank capsule floating in the bar. It can
|
|
1640
|
+
only happen below the breakpoint, in a project whose settings
|
|
1641
|
+
ids do not overlap MOBILE_BAR_ACTIONS at all — every action
|
|
1642
|
+
then lives in the drawer, and the tray has nothing to hold. */}
|
|
1643
|
+
{barActions.length > 0 ? (
|
|
1644
|
+
<div
|
|
1645
|
+
className={`${
|
|
1646
|
+
themeType === 'alternate'
|
|
1647
|
+
? styles['hactions-alternate']
|
|
1648
|
+
: styles.hactions
|
|
1649
|
+
} ${debugIcons ? styles['debug-icons'] : ''}`}
|
|
1650
|
+
ref={actionItemsRef}
|
|
1651
|
+
>
|
|
1652
|
+
<ul>
|
|
1653
|
+
{barActions.map((setting, settingKey) => (
|
|
1232
1654
|
<React.Fragment key={`setting-${settingKey}`}>
|
|
1233
1655
|
{renderSetting(setting)}
|
|
1234
1656
|
</React.Fragment>
|
|
1235
|
-
)
|
|
1236
|
-
|
|
1237
|
-
</
|
|
1238
|
-
|
|
1657
|
+
))}
|
|
1658
|
+
</ul>
|
|
1659
|
+
</div>
|
|
1660
|
+
) : null}
|
|
1661
|
+
|
|
1662
|
+
{headerDrawer}
|
|
1239
1663
|
|
|
1240
1664
|
<BusinessCardOcr
|
|
1241
1665
|
isOpen={showBusinessCardOcr}
|