@dloizides/ui-nav 1.2.0 → 1.3.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/CHANGELOG.md +23 -0
- package/README.md +15 -2
- package/dist/index.d.mts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +190 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StyleSheet, TouchableOpacity, View, Text, useWindowDimensions, ActivityIndicator, ScrollView } from 'react-native';
|
|
1
|
+
import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, Pressable, ActivityIndicator, ScrollView } from 'react-native';
|
|
2
2
|
import { useUi } from '@dloizides/ui-feedback';
|
|
3
3
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
|
4
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
@@ -79,7 +79,12 @@ var navBarStyles = StyleSheet.create({
|
|
|
79
79
|
marginLeft: "auto",
|
|
80
80
|
paddingHorizontal: 10,
|
|
81
81
|
paddingVertical: 6,
|
|
82
|
-
borderRadius: 4
|
|
82
|
+
borderRadius: 4,
|
|
83
|
+
// a11y: keep the hamburger a comfortable ≥44×44 touch target.
|
|
84
|
+
minWidth: 44,
|
|
85
|
+
minHeight: 44,
|
|
86
|
+
alignItems: "center",
|
|
87
|
+
justifyContent: "center"
|
|
83
88
|
},
|
|
84
89
|
toggleGlyph: { fontSize: 20, fontWeight: "700" },
|
|
85
90
|
links: {
|
|
@@ -104,7 +109,10 @@ var navBarStyles = StyleSheet.create({
|
|
|
104
109
|
paddingHorizontal: 14,
|
|
105
110
|
borderRadius: 8,
|
|
106
111
|
flexDirection: "row",
|
|
107
|
-
alignItems: "center"
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
// a11y: comfortable ≥44 tall touch target (rounded-pill hover/active affordance).
|
|
114
|
+
minHeight: 44,
|
|
115
|
+
justifyContent: "center"
|
|
108
116
|
},
|
|
109
117
|
linkStacked: { paddingVertical: 10 },
|
|
110
118
|
linkIcon: { marginRight: 6 },
|
|
@@ -147,6 +155,31 @@ var expandableStyles = StyleSheet.create({
|
|
|
147
155
|
var BASE_INDENT = 12;
|
|
148
156
|
var NAV_ICON_SIZE = 14;
|
|
149
157
|
var CHEVRON_ICON_SIZE = 12;
|
|
158
|
+
var IS_WEB = Platform.OS === "web";
|
|
159
|
+
var FOCUS_RING_WIDTH = 2;
|
|
160
|
+
var FOCUS_RING_OFFSET = 2;
|
|
161
|
+
var HOVER_TRANSITION_MS = 150;
|
|
162
|
+
function focusRingStyle(focused, ringColor) {
|
|
163
|
+
if (!IS_WEB || !focused) return void 0;
|
|
164
|
+
const ring = {
|
|
165
|
+
outlineWidth: FOCUS_RING_WIDTH,
|
|
166
|
+
outlineStyle: "solid",
|
|
167
|
+
outlineColor: ringColor,
|
|
168
|
+
outlineOffset: FOCUS_RING_OFFSET
|
|
169
|
+
};
|
|
170
|
+
return ring;
|
|
171
|
+
}
|
|
172
|
+
function hoverTransitionStyle(reducedMotion) {
|
|
173
|
+
if (!IS_WEB) return void 0;
|
|
174
|
+
const transition = {
|
|
175
|
+
transitionProperty: "color, background-color",
|
|
176
|
+
transitionDuration: reducedMotion ? "0ms" : `${HOVER_TRANSITION_MS}ms`
|
|
177
|
+
};
|
|
178
|
+
return transition;
|
|
179
|
+
}
|
|
180
|
+
function ariaCurrentProps(isActive) {
|
|
181
|
+
return isActive ? { "aria-current": "page" } : {};
|
|
182
|
+
}
|
|
150
183
|
var NavExpandableItem = ({
|
|
151
184
|
item,
|
|
152
185
|
pathname,
|
|
@@ -158,6 +191,7 @@ var NavExpandableItem = ({
|
|
|
158
191
|
depth = 0
|
|
159
192
|
}) => {
|
|
160
193
|
const [expanded, setExpanded] = useState(false);
|
|
194
|
+
const [focused, setFocused] = useState(false);
|
|
161
195
|
const { theme } = useUi();
|
|
162
196
|
const colors = theme.colors;
|
|
163
197
|
const primaryColor = theme.palette.primary["500"];
|
|
@@ -178,9 +212,18 @@ var NavExpandableItem = ({
|
|
|
178
212
|
accessibilityHint: navigateHint(item.label),
|
|
179
213
|
accessibilityLabel: item.label,
|
|
180
214
|
accessibilityRole: "button",
|
|
181
|
-
|
|
215
|
+
accessibilityState: { selected: isActive },
|
|
216
|
+
style: [
|
|
217
|
+
expandableStyles.childItem,
|
|
218
|
+
paddingStyle,
|
|
219
|
+
isActive ? activeItemStyle : void 0,
|
|
220
|
+
focusRingStyle(focused, primaryColor)
|
|
221
|
+
],
|
|
182
222
|
testID: item.testID ?? item.key,
|
|
223
|
+
onBlur: () => setFocused(false),
|
|
224
|
+
onFocus: () => setFocused(true),
|
|
183
225
|
onPress: () => onNavigate(item.route),
|
|
226
|
+
...ariaCurrentProps(isActive),
|
|
184
227
|
children: [
|
|
185
228
|
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.textSecondary, NAV_ICON_SIZE) }) : null,
|
|
186
229
|
/* @__PURE__ */ jsx(
|
|
@@ -204,8 +247,10 @@ var NavExpandableItem = ({
|
|
|
204
247
|
accessibilityLabel: item.label,
|
|
205
248
|
accessibilityRole: "button",
|
|
206
249
|
accessibilityState: { expanded },
|
|
207
|
-
style: [expandableStyles.header, headerPaddingStyle],
|
|
250
|
+
style: [expandableStyles.header, headerPaddingStyle, focusRingStyle(focused, primaryColor)],
|
|
208
251
|
testID: item.testID ?? item.key,
|
|
252
|
+
onBlur: () => setFocused(false),
|
|
253
|
+
onFocus: () => setFocused(true),
|
|
209
254
|
onPress: toggle,
|
|
210
255
|
children: [
|
|
211
256
|
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: expandableStyles.iconWrapper, children: item.renderIcon(colors.text, NAV_ICON_SIZE) }) : null,
|
|
@@ -305,6 +350,32 @@ var APP_SHELL_SUFFIX = {
|
|
|
305
350
|
error: "-error",
|
|
306
351
|
forbidden: "-forbidden"
|
|
307
352
|
};
|
|
353
|
+
var FocusableTouchable = ({
|
|
354
|
+
onPress,
|
|
355
|
+
accessibilityLabel,
|
|
356
|
+
accessibilityHint,
|
|
357
|
+
accessibilityRole = "button",
|
|
358
|
+
ringColor,
|
|
359
|
+
style,
|
|
360
|
+
testID,
|
|
361
|
+
children
|
|
362
|
+
}) => {
|
|
363
|
+
const [focused, setFocused] = useState(false);
|
|
364
|
+
return /* @__PURE__ */ jsx(
|
|
365
|
+
Pressable,
|
|
366
|
+
{
|
|
367
|
+
accessibilityHint,
|
|
368
|
+
accessibilityLabel,
|
|
369
|
+
accessibilityRole,
|
|
370
|
+
style: [style, focusRingStyle(focused, ringColor)],
|
|
371
|
+
testID,
|
|
372
|
+
onBlur: () => setFocused(false),
|
|
373
|
+
onFocus: () => setFocused(true),
|
|
374
|
+
onPress,
|
|
375
|
+
children
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
};
|
|
308
379
|
var TEXT_ON_PRIMARY = "#ffffff";
|
|
309
380
|
function isAccountState(account) {
|
|
310
381
|
return "displayName" in account;
|
|
@@ -342,11 +413,11 @@ var Topbar = ({
|
|
|
342
413
|
/* @__PURE__ */ jsx(View, { style: navStyles.topbarLeft, children: left }),
|
|
343
414
|
/* @__PURE__ */ jsxs(View, { style: navStyles.topbarRight, children: [
|
|
344
415
|
language ? /* @__PURE__ */ jsx(
|
|
345
|
-
|
|
416
|
+
FocusableTouchable,
|
|
346
417
|
{
|
|
347
418
|
accessibilityHint: language.hint,
|
|
348
419
|
accessibilityLabel: language.label,
|
|
349
|
-
|
|
420
|
+
ringColor: primaryColor,
|
|
350
421
|
style: navStyles.topbarRowItem,
|
|
351
422
|
onPress: language.onPress,
|
|
352
423
|
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.topbarLabel, { color: colors.text }], children: language.label })
|
|
@@ -359,11 +430,11 @@ var Topbar = ({
|
|
|
359
430
|
] }) : null,
|
|
360
431
|
richAccount ? /* @__PURE__ */ jsxs(View, { style: navStyles.userBlock, children: [
|
|
361
432
|
richAccount.onAccount ? /* @__PURE__ */ jsx(
|
|
362
|
-
|
|
433
|
+
FocusableTouchable,
|
|
363
434
|
{
|
|
364
435
|
accessibilityHint: richAccount.displayName,
|
|
365
436
|
accessibilityLabel: richAccount.displayName,
|
|
366
|
-
|
|
437
|
+
ringColor: primaryColor,
|
|
367
438
|
testID: NAV_TEST_IDS.accountName,
|
|
368
439
|
onPress: richAccount.onAccount,
|
|
369
440
|
children: /* @__PURE__ */ jsx(Text, { style: [navStyles.userName, { color: colors.text }], children: richAccount.displayName })
|
|
@@ -387,11 +458,11 @@ var Topbar = ({
|
|
|
387
458
|
) : null
|
|
388
459
|
] }) : null,
|
|
389
460
|
legacyAccount ? /* @__PURE__ */ jsx(
|
|
390
|
-
|
|
461
|
+
FocusableTouchable,
|
|
391
462
|
{
|
|
392
463
|
accessibilityHint: legacyAccount.hint,
|
|
393
464
|
accessibilityLabel: legacyAccount.label,
|
|
394
|
-
|
|
465
|
+
ringColor: primaryColor,
|
|
395
466
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
396
467
|
testID: legacyAccount.testID,
|
|
397
468
|
onPress: legacyAccount.onPress,
|
|
@@ -399,11 +470,11 @@ var Topbar = ({
|
|
|
399
470
|
}
|
|
400
471
|
) : null,
|
|
401
472
|
upgrade ? /* @__PURE__ */ jsx(
|
|
402
|
-
|
|
473
|
+
FocusableTouchable,
|
|
403
474
|
{
|
|
404
475
|
accessibilityHint: upgrade.hint,
|
|
405
476
|
accessibilityLabel: upgrade.label,
|
|
406
|
-
|
|
477
|
+
ringColor: primaryColor,
|
|
407
478
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
408
479
|
testID: upgrade.testID ?? NAV_TEST_IDS.accountUpgrade,
|
|
409
480
|
onPress: upgrade.onPress,
|
|
@@ -411,12 +482,11 @@ var Topbar = ({
|
|
|
411
482
|
}
|
|
412
483
|
) : null,
|
|
413
484
|
/* @__PURE__ */ jsx(
|
|
414
|
-
|
|
485
|
+
FocusableTouchable,
|
|
415
486
|
{
|
|
416
|
-
accessible: true,
|
|
417
487
|
accessibilityHint: logout.hint,
|
|
418
488
|
accessibilityLabel: logout.label,
|
|
419
|
-
|
|
489
|
+
ringColor: primaryColor,
|
|
420
490
|
style: [navStyles.accountBtn, { backgroundColor: primaryColor }],
|
|
421
491
|
testID: logout.testID,
|
|
422
492
|
onPress: richAccount ? richAccount.onLogout : logout.onPress,
|
|
@@ -429,10 +499,75 @@ var Topbar = ({
|
|
|
429
499
|
);
|
|
430
500
|
};
|
|
431
501
|
var TEXT_ON_PRIMARY2 = "#ffffff";
|
|
502
|
+
function ariaCurrentProps2(isActive) {
|
|
503
|
+
return isActive ? { "aria-current": "page" } : {};
|
|
504
|
+
}
|
|
505
|
+
var NavBarLink = ({
|
|
506
|
+
item,
|
|
507
|
+
isActive,
|
|
508
|
+
collapsed,
|
|
509
|
+
iconSize,
|
|
510
|
+
reducedMotion,
|
|
511
|
+
colors,
|
|
512
|
+
navigateHint,
|
|
513
|
+
onPress
|
|
514
|
+
}) => {
|
|
515
|
+
const [hovered, setHovered] = useState(false);
|
|
516
|
+
const [focused, setFocused] = useState(false);
|
|
517
|
+
const isHovered = hovered && !isActive;
|
|
518
|
+
const textColor = isActive ? TEXT_ON_PRIMARY2 : isHovered ? colors.hoverText : colors.rest;
|
|
519
|
+
const pillStyle = isActive ? { backgroundColor: colors.activeBg } : isHovered ? { backgroundColor: colors.hoverBg } : void 0;
|
|
520
|
+
return /* @__PURE__ */ jsxs(
|
|
521
|
+
Pressable,
|
|
522
|
+
{
|
|
523
|
+
accessibilityHint: navigateHint(item.label),
|
|
524
|
+
accessibilityLabel: item.label,
|
|
525
|
+
accessibilityRole: "link",
|
|
526
|
+
accessibilityState: { selected: isActive },
|
|
527
|
+
style: [
|
|
528
|
+
navBarStyles.link,
|
|
529
|
+
collapsed ? navBarStyles.linkStacked : void 0,
|
|
530
|
+
pillStyle,
|
|
531
|
+
hoverTransitionStyle(reducedMotion),
|
|
532
|
+
focusRingStyle(focused, colors.ring)
|
|
533
|
+
],
|
|
534
|
+
testID: item.testID ?? item.key,
|
|
535
|
+
onBlur: () => setFocused(false),
|
|
536
|
+
onFocus: () => setFocused(true),
|
|
537
|
+
onHoverIn: () => setHovered(true),
|
|
538
|
+
onHoverOut: () => setHovered(false),
|
|
539
|
+
onPress: () => onPress(item.route),
|
|
540
|
+
...ariaCurrentProps2(isActive),
|
|
541
|
+
children: [
|
|
542
|
+
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: navBarStyles.linkIcon, children: item.renderIcon(textColor, iconSize) }) : null,
|
|
543
|
+
/* @__PURE__ */ jsx(Text, { style: [navBarStyles.linkText, { color: textColor }], children: item.label })
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
549
|
+
function getReducedMotionQuery() {
|
|
550
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
|
|
551
|
+
return window.matchMedia(REDUCED_MOTION_QUERY);
|
|
552
|
+
}
|
|
553
|
+
function useReducedMotion() {
|
|
554
|
+
const [reduced, setReduced] = useState(() => getReducedMotionQuery()?.matches ?? false);
|
|
555
|
+
useEffect(() => {
|
|
556
|
+
const mql = getReducedMotionQuery();
|
|
557
|
+
if (mql === void 0 || mql.addEventListener === void 0) return void 0;
|
|
558
|
+
const onChange = () => setReduced(mql.matches);
|
|
559
|
+
mql.addEventListener("change", onChange);
|
|
560
|
+
return () => {
|
|
561
|
+
mql.removeEventListener?.("change", onChange);
|
|
562
|
+
};
|
|
563
|
+
}, []);
|
|
564
|
+
return reduced;
|
|
565
|
+
}
|
|
566
|
+
var LINKS_REGION_ID = "navbar-links-region";
|
|
432
567
|
var DEFAULT_COLLAPSE_BELOW = 760;
|
|
433
568
|
var MENU_GLYPH = "\u2630";
|
|
434
|
-
function
|
|
435
|
-
return
|
|
569
|
+
function ariaControlsProps(id) {
|
|
570
|
+
return { "aria-controls": id };
|
|
436
571
|
}
|
|
437
572
|
var NavBar = ({
|
|
438
573
|
items,
|
|
@@ -452,7 +587,9 @@ var NavBar = ({
|
|
|
452
587
|
const colors = theme.colors;
|
|
453
588
|
const primaryColor = theme.palette.primary["500"];
|
|
454
589
|
const { width } = useWindowDimensions();
|
|
590
|
+
const reducedMotion = useReducedMotion();
|
|
455
591
|
const [open, setOpen] = useState(false);
|
|
592
|
+
const [toggleFocused, setToggleFocused] = useState(false);
|
|
456
593
|
const collapsed = width < collapseBelow;
|
|
457
594
|
const showLinks = !collapsed || open;
|
|
458
595
|
const toggleMenu = useCallback(() => setOpen((v) => !v), []);
|
|
@@ -463,33 +600,16 @@ var NavBar = ({
|
|
|
463
600
|
},
|
|
464
601
|
[onNavigate]
|
|
465
602
|
);
|
|
466
|
-
const
|
|
467
|
-
() => ({
|
|
468
|
-
|
|
603
|
+
const linkColors = useMemo(
|
|
604
|
+
() => ({
|
|
605
|
+
rest: colors.textSecondary,
|
|
606
|
+
hoverText: colors.text,
|
|
607
|
+
hoverBg: colors.surfaceElevated,
|
|
608
|
+
activeBg: primaryColor,
|
|
609
|
+
ring: primaryColor
|
|
610
|
+
}),
|
|
611
|
+
[colors.textSecondary, colors.text, colors.surfaceElevated, primaryColor]
|
|
469
612
|
);
|
|
470
|
-
const renderLink = (item) => {
|
|
471
|
-
const isActive = isRouteActive(pathname, item.route);
|
|
472
|
-
const textColor = isActive ? TEXT_ON_PRIMARY2 : colors.textSecondary;
|
|
473
|
-
return /* @__PURE__ */ jsxs(
|
|
474
|
-
TouchableOpacity,
|
|
475
|
-
{
|
|
476
|
-
accessible: true,
|
|
477
|
-
accessibilityHint: navigateHint(item.label),
|
|
478
|
-
accessibilityLabel: item.label,
|
|
479
|
-
accessibilityRole: "link",
|
|
480
|
-
accessibilityState: { selected: isActive },
|
|
481
|
-
style: [navBarStyles.link, collapsed ? navBarStyles.linkStacked : void 0, isActive ? activeLinkStyle : void 0],
|
|
482
|
-
testID: item.testID ?? item.key,
|
|
483
|
-
onPress: () => handlePress(item.route),
|
|
484
|
-
...activeAriaProps(isActive),
|
|
485
|
-
children: [
|
|
486
|
-
typeof item.renderIcon === "function" ? /* @__PURE__ */ jsx(View, { style: navBarStyles.linkIcon, children: item.renderIcon(textColor, NAV_ICON_SIZE) }) : null,
|
|
487
|
-
/* @__PURE__ */ jsx(Text, { style: [navBarStyles.linkText, { color: textColor }], children: item.label })
|
|
488
|
-
]
|
|
489
|
-
},
|
|
490
|
-
item.key
|
|
491
|
-
);
|
|
492
|
-
};
|
|
493
613
|
return /* @__PURE__ */ jsx(
|
|
494
614
|
View,
|
|
495
615
|
{
|
|
@@ -504,21 +624,44 @@ var NavBar = ({
|
|
|
504
624
|
children: /* @__PURE__ */ jsxs(View, { style: navBarStyles.inner, children: [
|
|
505
625
|
brand !== void 0 ? /* @__PURE__ */ jsx(View, { style: navBarStyles.brand, children: brand }) : null,
|
|
506
626
|
collapsed ? /* @__PURE__ */ jsx(
|
|
507
|
-
|
|
627
|
+
Pressable,
|
|
508
628
|
{
|
|
509
|
-
accessible: true,
|
|
510
629
|
accessibilityHint: menuHint,
|
|
511
630
|
accessibilityLabel: menuLabel,
|
|
512
631
|
accessibilityRole: "button",
|
|
513
632
|
accessibilityState: { expanded: open },
|
|
514
633
|
"aria-expanded": open,
|
|
515
|
-
style: navBarStyles.toggle,
|
|
634
|
+
style: [navBarStyles.toggle, focusRingStyle(toggleFocused, primaryColor)],
|
|
516
635
|
testID: NAV_TEST_IDS.navBarToggle,
|
|
636
|
+
onBlur: () => setToggleFocused(false),
|
|
637
|
+
onFocus: () => setToggleFocused(true),
|
|
517
638
|
onPress: toggleMenu,
|
|
639
|
+
...ariaControlsProps(LINKS_REGION_ID),
|
|
518
640
|
children: typeof renderMenuIcon === "function" ? renderMenuIcon(colors.text, open) : /* @__PURE__ */ jsx(Text, { style: [navBarStyles.toggleGlyph, { color: colors.text }], children: MENU_GLYPH })
|
|
519
641
|
}
|
|
520
642
|
) : null,
|
|
521
|
-
showLinks ? /* @__PURE__ */ jsx(
|
|
643
|
+
showLinks ? /* @__PURE__ */ jsx(
|
|
644
|
+
View,
|
|
645
|
+
{
|
|
646
|
+
nativeID: LINKS_REGION_ID,
|
|
647
|
+
style: collapsed ? navBarStyles.linksStacked : navBarStyles.links,
|
|
648
|
+
testID: NAV_TEST_IDS.navBarLinks,
|
|
649
|
+
children: items.map((item) => /* @__PURE__ */ jsx(
|
|
650
|
+
NavBarLink,
|
|
651
|
+
{
|
|
652
|
+
collapsed,
|
|
653
|
+
colors: linkColors,
|
|
654
|
+
iconSize: NAV_ICON_SIZE,
|
|
655
|
+
isActive: isRouteActive(pathname, item.route),
|
|
656
|
+
item,
|
|
657
|
+
navigateHint,
|
|
658
|
+
reducedMotion,
|
|
659
|
+
onPress: handlePress
|
|
660
|
+
},
|
|
661
|
+
item.key
|
|
662
|
+
))
|
|
663
|
+
}
|
|
664
|
+
) : null,
|
|
522
665
|
showLinks && right !== void 0 ? /* @__PURE__ */ jsx(View, { style: collapsed ? navBarStyles.rightStacked : navBarStyles.right, children: right }) : null
|
|
523
666
|
] })
|
|
524
667
|
}
|