colibri-ui 0.8.97 → 0.8.98

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.
Files changed (56) hide show
  1. package/NavigationMenu/NavigationMenu.d.ts +3 -0
  2. package/NavigationMenu/NavigationMenu.js +170 -0
  3. package/NavigationMenu/NavigationMenu.module.css +67 -0
  4. package/NavigationMenu/components/MoreMenu/MoreMenu.module.css +19 -0
  5. package/NavigationMenu/components/MoreMenu/index.d.ts +16 -0
  6. package/NavigationMenu/components/MoreMenu/index.js +155 -0
  7. package/NavigationMenu/components/NavigationLink/NavigationLink.module.css +59 -0
  8. package/NavigationMenu/components/NavigationLink/index.d.ts +16 -0
  9. package/NavigationMenu/components/NavigationLink/index.js +51 -0
  10. package/NavigationMenu/helpers.d.ts +9 -0
  11. package/NavigationMenu/helpers.js +20 -0
  12. package/NavigationMenu/index.d.ts +2 -0
  13. package/NavigationMenu/index.js +7 -0
  14. package/NavigationMenu/index.props.d.ts +40 -0
  15. package/NavigationMenu/package.json +9 -0
  16. package/NavigationMenu/splitItems.d.ts +30 -0
  17. package/NavigationMenu/splitItems.js +54 -0
  18. package/NavigationMenu/useOverflowSplit.d.ts +25 -0
  19. package/NavigationMenu/useOverflowSplit.js +63 -0
  20. package/Theme/index.d.ts +8 -0
  21. package/Theme/themes/buenos_aires.d.ts +4 -0
  22. package/Theme/themes/buenos_aires.js +4 -0
  23. package/Theme/themes/jaipur.d.ts +4 -0
  24. package/Theme/themes/jaipur.js +4 -0
  25. package/esm/NavigationMenu/NavigationMenu.d.ts +3 -0
  26. package/esm/NavigationMenu/NavigationMenu.js +162 -0
  27. package/esm/NavigationMenu/NavigationMenu.module.css +67 -0
  28. package/esm/NavigationMenu/components/MoreMenu/MoreMenu.module.css +19 -0
  29. package/esm/NavigationMenu/components/MoreMenu/index.d.ts +16 -0
  30. package/esm/NavigationMenu/components/MoreMenu/index.js +147 -0
  31. package/esm/NavigationMenu/components/NavigationLink/NavigationLink.module.css +59 -0
  32. package/esm/NavigationMenu/components/NavigationLink/index.d.ts +16 -0
  33. package/esm/NavigationMenu/components/NavigationLink/index.js +41 -0
  34. package/esm/NavigationMenu/helpers.d.ts +9 -0
  35. package/esm/NavigationMenu/helpers.js +15 -0
  36. package/esm/NavigationMenu/index.d.ts +2 -0
  37. package/esm/NavigationMenu/index.js +1 -0
  38. package/esm/NavigationMenu/index.props.d.ts +40 -0
  39. package/esm/NavigationMenu/splitItems.d.ts +30 -0
  40. package/esm/NavigationMenu/splitItems.js +51 -0
  41. package/esm/NavigationMenu/useOverflowSplit.d.ts +25 -0
  42. package/esm/NavigationMenu/useOverflowSplit.js +61 -0
  43. package/esm/Theme/index.d.ts +8 -0
  44. package/esm/Theme/themes/buenos_aires.d.ts +4 -0
  45. package/esm/Theme/themes/buenos_aires.js +4 -0
  46. package/esm/Theme/themes/jaipur.d.ts +4 -0
  47. package/esm/Theme/themes/jaipur.js +4 -0
  48. package/esm/index.d.ts +2 -0
  49. package/esm/index.js +1 -0
  50. package/esm/theme_ba_variables.css +4 -0
  51. package/esm/theme_jaipur_variables.css +4 -0
  52. package/index.d.ts +2 -0
  53. package/index.js +3 -0
  54. package/package.json +1 -1
  55. package/theme_ba_variables.css +4 -0
  56. package/theme_jaipur_variables.css +4 -0
@@ -0,0 +1,30 @@
1
+ export interface SplitInput {
2
+ itemWidths: number[];
3
+ /** Pinned items are always visible and go after the main items. */
4
+ pinnedWidths: number[];
5
+ moreWidth: number;
6
+ containerWidth: number;
7
+ /** CSS gap between items. */
8
+ gap: number;
9
+ /** Index of the active item or -1. */
10
+ activeIndex: number;
11
+ /** "More" is needed anyway, for example for `moreItems`. */
12
+ forceMore: boolean;
13
+ }
14
+ export interface SplitResult {
15
+ /** Indexes of the items left in the row, in the original order. */
16
+ visible: number[];
17
+ /** Indexes of the items moved to "More", in the original order. */
18
+ hidden: number[];
19
+ }
20
+ /**
21
+ * Splits items into the row and "More".
22
+ *
23
+ * Items go from left to right while they fit; the first one that does not fit
24
+ * and all after it move to "More". Space for the active item is reserved
25
+ * first, so it stays in the row and its neighbours move instead. Every element
26
+ * costs its width plus gap; the budget has one extra gap because N elements
27
+ * have N − 1 gaps.
28
+ */
29
+ export declare const splitItems: ({ itemWidths, pinnedWidths, moreWidth, containerWidth, gap, activeIndex, forceMore, }: SplitInput) => SplitResult;
30
+ export declare const isSameSplit: (a: SplitResult, b: SplitResult) => boolean;
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const sum = list => list.reduce((acc, n) => acc + n, 0);
4
+ /**
5
+ * Splits items into the row and "More".
6
+ *
7
+ * Items go from left to right while they fit; the first one that does not fit
8
+ * and all after it move to "More". Space for the active item is reserved
9
+ * first, so it stays in the row and its neighbours move instead. Every element
10
+ * costs its width plus gap; the budget has one extra gap because N elements
11
+ * have N − 1 gaps.
12
+ */
13
+ const splitItems = ({
14
+ itemWidths,
15
+ pinnedWidths,
16
+ moreWidth,
17
+ containerWidth,
18
+ gap,
19
+ activeIndex,
20
+ forceMore
21
+ }) => {
22
+ const all = itemWidths.map((_, index) => index);
23
+ const cost = width => width + gap;
24
+ const pinnedCost = sum(pinnedWidths.map(cost));
25
+ if (!forceMore && sum(itemWidths.map(cost)) + pinnedCost <= containerWidth + gap) {
26
+ return {
27
+ visible: all,
28
+ hidden: []
29
+ };
30
+ }
31
+ const hasActive = activeIndex >= 0 && activeIndex < itemWidths.length;
32
+ let budget = containerWidth + gap - pinnedCost - cost(moreWidth);
33
+ if (hasActive) budget -= cost(itemWidths[activeIndex]);
34
+ const visible = [];
35
+ for (let index = 0; index < itemWidths.length; index++) {
36
+ if (index === activeIndex) continue;
37
+ if (cost(itemWidths[index]) > budget) break;
38
+ budget -= cost(itemWidths[index]);
39
+ visible.push(index);
40
+ }
41
+ if (hasActive) {
42
+ visible.push(activeIndex);
43
+ visible.sort((a, b) => a - b);
44
+ }
45
+ return {
46
+ visible,
47
+ hidden: all.filter(index => !visible.includes(index))
48
+ };
49
+ };
50
+ const isSameList = (a, b) => a.length === b.length && a.every((index, position) => index === b[position]);
51
+ const isSameSplit = (a, b) => isSameList(a.visible, b.visible) && isSameList(a.hidden, b.hidden);
52
+
53
+ exports.isSameSplit = isSameSplit;
54
+ exports.splitItems = splitItems;
@@ -0,0 +1,25 @@
1
+ import type { SplitResult } from "./splitItems";
2
+ interface Options {
3
+ itemCount: number;
4
+ pinnedCount: number;
5
+ activeIndex: number;
6
+ forceMore: boolean;
7
+ /** Changes with the item labels to recalculate. */
8
+ contentKey: string;
9
+ }
10
+ /**
11
+ * Measures the items and splits them into the row and "More".
12
+ *
13
+ * Widths come from a hidden measurer that always renders every item, so the
14
+ * items inside "More" are measured too, for example after a web font loads.
15
+ * ResizeObserver watches the container, the measurer and the "More" trigger.
16
+ * Before the first measurement, and on the server, `split` is `null`.
17
+ */
18
+ export declare const useOverflowSplit: ({ itemCount, pinnedCount, activeIndex, forceMore, contentKey, }: Options) => {
19
+ split: SplitResult | null;
20
+ containerRef: import("react").RefObject<HTMLElement | null>;
21
+ listRef: import("react").RefObject<HTMLUListElement | null>;
22
+ measurerRef: import("react").RefObject<HTMLDivElement | null>;
23
+ moreRef: import("react").RefObject<HTMLLIElement | null>;
24
+ };
25
+ export {};
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var helpers = require('./helpers.js');
5
+ var splitItems = require('./splitItems.js');
6
+
7
+ const getWidth = element => element ? element.getBoundingClientRect().width : 0;
8
+ /**
9
+ * Measures the items and splits them into the row and "More".
10
+ *
11
+ * Widths come from a hidden measurer that always renders every item, so the
12
+ * items inside "More" are measured too, for example after a web font loads.
13
+ * ResizeObserver watches the container, the measurer and the "More" trigger.
14
+ * Before the first measurement, and on the server, `split` is `null`.
15
+ */
16
+ const useOverflowSplit = ({
17
+ itemCount,
18
+ pinnedCount,
19
+ activeIndex,
20
+ forceMore,
21
+ contentKey
22
+ }) => {
23
+ const containerRef = React.useRef(null);
24
+ const listRef = React.useRef(null);
25
+ const measurerRef = React.useRef(null);
26
+ const moreRef = React.useRef(null);
27
+ const [split, setSplit] = React.useState(null);
28
+ helpers.useIsomorphicLayoutEffect(() => {
29
+ const container = containerRef.current;
30
+ const measurer = measurerRef.current;
31
+ if (!container || !measurer) return undefined;
32
+ const recalculate = () => {
33
+ const widths = Array.from(measurer.children, getWidth);
34
+ const gap = listRef.current ? parseFloat(getComputedStyle(listRef.current).columnGap) || 0 : 0;
35
+ const next = splitItems.splitItems({
36
+ itemWidths: widths.slice(0, itemCount),
37
+ pinnedWidths: widths.slice(itemCount, itemCount + pinnedCount),
38
+ moreWidth: getWidth(moreRef.current),
39
+ containerWidth: getWidth(container),
40
+ gap,
41
+ activeIndex,
42
+ forceMore
43
+ });
44
+ setSplit(prev => prev && splitItems.isSameSplit(prev, next) ? prev : next);
45
+ };
46
+ recalculate();
47
+ if (typeof ResizeObserver === "undefined") return undefined;
48
+ const observer = new ResizeObserver(recalculate);
49
+ observer.observe(container);
50
+ observer.observe(measurer);
51
+ if (moreRef.current) observer.observe(moreRef.current);
52
+ return () => observer.disconnect();
53
+ }, [itemCount, pinnedCount, activeIndex, forceMore, contentKey]);
54
+ return {
55
+ split,
56
+ containerRef,
57
+ listRef,
58
+ measurerRef,
59
+ moreRef
60
+ };
61
+ };
62
+
63
+ exports.useOverflowSplit = useOverflowSplit;
package/Theme/index.d.ts CHANGED
@@ -692,7 +692,11 @@ export declare const THEMES: {
692
692
  "component-pagination-bg-current": string;
693
693
  "component-pagination-bg-hover": string;
694
694
  "component-pagination-focus-ring": string;
695
+ "component-navigation-item-text": string;
696
+ "component-navigation-item-text-selected": string;
697
+ "component-navigation-item-bg-hover": string;
695
698
  "component-navigation-item-bg-selected": string;
699
+ "component-navigation-item-focus-ring": string;
696
700
  "component-chip-bg-selected": string;
697
701
  "component-chip-bg-selected-strong": string;
698
702
  "palette-black": string;
@@ -1536,7 +1540,11 @@ export declare const THEMES: {
1536
1540
  "component-pagination-bg-current": string;
1537
1541
  "component-pagination-bg-hover": string;
1538
1542
  "component-pagination-focus-ring": string;
1543
+ "component-navigation-item-text": string;
1544
+ "component-navigation-item-text-selected": string;
1545
+ "component-navigation-item-bg-hover": string;
1539
1546
  "component-navigation-item-bg-selected": string;
1547
+ "component-navigation-item-focus-ring": string;
1540
1548
  "component-chip-bg-selected": string;
1541
1549
  "component-chip-bg-selected-strong": string;
1542
1550
  "palette-black": string;
@@ -613,7 +613,11 @@ declare const _default: {
613
613
  "component-pagination-bg-current": string;
614
614
  "component-pagination-bg-hover": string;
615
615
  "component-pagination-focus-ring": string;
616
+ "component-navigation-item-text": string;
617
+ "component-navigation-item-text-selected": string;
618
+ "component-navigation-item-bg-hover": string;
616
619
  "component-navigation-item-bg-selected": string;
620
+ "component-navigation-item-focus-ring": string;
617
621
  "component-chip-bg-selected": string;
618
622
  "component-chip-bg-selected-strong": string;
619
623
  "palette-black": string;
@@ -619,7 +619,11 @@ var THEME_BA = {
619
619
  "component-pagination-bg-current": theme_buenos_aires.default.BRAND_BA_FOREST,
620
620
  "component-pagination-bg-hover": theme_buenos_aires.default.NEUTRAL_BA_100,
621
621
  "component-pagination-focus-ring": theme_buenos_aires.default.BRAND_BA_LIME,
622
+ "component-navigation-item-text": theme_buenos_aires.default.BRAND_BA_FOREST,
623
+ "component-navigation-item-text-selected": theme_buenos_aires.default.BRAND_BA_LIME,
624
+ "component-navigation-item-bg-hover": theme_buenos_aires.default.NEUTRAL_BA_100,
622
625
  "component-navigation-item-bg-selected": theme_buenos_aires.default.BRAND_BA_FOREST,
626
+ "component-navigation-item-focus-ring": theme_buenos_aires.default.BRAND_BA_LIME,
623
627
  "component-chip-bg-selected": theme_buenos_aires.default.BRAND_BA_FOREST,
624
628
  "component-chip-bg-selected-strong": theme_buenos_aires.default.BRAND_BA_LIME,
625
629
  "palette-black": theme_buenos_aires.default.BRAND_BA_FOREST,
@@ -613,7 +613,11 @@ declare const _default: {
613
613
  "component-pagination-bg-current": string;
614
614
  "component-pagination-bg-hover": string;
615
615
  "component-pagination-focus-ring": string;
616
+ "component-navigation-item-text": string;
617
+ "component-navigation-item-text-selected": string;
618
+ "component-navigation-item-bg-hover": string;
616
619
  "component-navigation-item-bg-selected": string;
620
+ "component-navigation-item-focus-ring": string;
617
621
  "component-chip-bg-selected": string;
618
622
  "component-chip-bg-selected-strong": string;
619
623
  "palette-black": string;
@@ -619,7 +619,11 @@ var theme_jaipur = {
619
619
  "component-pagination-bg-current": theme_jaipur$1.default.BRAND_JP_EGGPLANT,
620
620
  "component-pagination-bg-hover": theme_jaipur$1.default.NEUTRAL_JP_100,
621
621
  "component-pagination-focus-ring": theme_jaipur$1.default.BRAND_JP_PINK,
622
+ "component-navigation-item-text": theme_jaipur$1.default.BRAND_JP_EGGPLANT,
623
+ "component-navigation-item-text-selected": theme_jaipur$1.default.BRAND_JP_PINK,
624
+ "component-navigation-item-bg-hover": theme_jaipur$1.default.NEUTRAL_JP_100,
622
625
  "component-navigation-item-bg-selected": theme_jaipur$1.default.BRAND_JP_EGGPLANT,
626
+ "component-navigation-item-focus-ring": theme_jaipur$1.default.BRAND_JP_PINK,
623
627
  "component-chip-bg-selected": theme_jaipur$1.default.BRAND_JP_EGGPLANT,
624
628
  "component-chip-bg-selected-strong": theme_jaipur$1.default.BRAND_JP_PINK,
625
629
  "palette-black": theme_jaipur$1.default.BRAND_JP_EGGPLANT,
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import type { NavigationMenuProps } from "./index.props";
3
+ export declare const NavigationMenu: React.ForwardRefExoticComponent<NavigationMenuProps & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,162 @@
1
+ import { __rest } from 'tslib';
2
+ import React, { forwardRef, useRef } from 'react';
3
+ import clsx from 'clsx';
4
+ import { MoreMenu } from './components/MoreMenu/index.js';
5
+ import { NavigationLink, getLinkClassName, LinkContent } from './components/NavigationLink/index.js';
6
+ import { useIsomorphicLayoutEffect, getItemKey, assignRef, isSameItem } from './helpers.js';
7
+ import { useOverflowSplit } from './useOverflowSplit.js';
8
+ import styles from './NavigationMenu.module.css';
9
+
10
+ const FOCUSABLE = "a[href], button, [tabindex]";
11
+ const getActiveIndex = items => items.findIndex(item => item.active);
12
+ const OverflowMenu = _a => {
13
+ var {
14
+ items,
15
+ pinnedItems = [],
16
+ moreItems = [],
17
+ moreLabel,
18
+ onItemClick,
19
+ renderLink,
20
+ navRef,
21
+ rootClassName
22
+ } = _a,
23
+ props = __rest(_a, ["items", "pinnedItems", "moreItems", "moreLabel", "onItemClick", "renderLink", "navRef", "rootClassName"]);
24
+ const {
25
+ split,
26
+ containerRef,
27
+ listRef,
28
+ measurerRef,
29
+ moreRef
30
+ } = useOverflowSplit({
31
+ itemCount: items.length,
32
+ pinnedCount: pinnedItems.length,
33
+ activeIndex: getActiveIndex(items),
34
+ forceMore: moreItems.length > 0,
35
+ contentKey: [...items, ...pinnedItems].map(item => item.label).join("\n")
36
+ });
37
+ // After `items` change and before the layout effect remeasures, the old
38
+ // split may point to missing indexes: drop them, new items go to "More".
39
+ const visibleIndexes = split === null || split === void 0 ? void 0 : split.visible.filter(index => index < items.length);
40
+ const visibleItems = visibleIndexes ? visibleIndexes.map(index => items[index]) : items;
41
+ const hiddenItems = visibleIndexes ? [...items.filter((_, index) => !visibleIndexes.includes(index)), ...moreItems] : [];
42
+ const isMoreHidden = hiddenItems.length === 0;
43
+ // "More" disappeared with a focused link inside: focus the same item in the
44
+ // row, or the first row link when it is not there.
45
+ const restoreFocus = item => {
46
+ var _a, _b, _c, _d, _e;
47
+ const rows = Array.from((_b = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.children) !== null && _b !== void 0 ? _b : []);
48
+ const rowItems = [...visibleItems, ...pinnedItems];
49
+ const row = rows[rowItems.findIndex(rowItem => isSameItem(rowItem, item))];
50
+ (_e = (_c = row === null || row === void 0 ? void 0 : row.querySelector(FOCUSABLE)) !== null && _c !== void 0 ? _c : (_d = listRef.current) === null || _d === void 0 ? void 0 : _d.querySelector(FOCUSABLE)) === null || _e === void 0 ? void 0 : _e.focus();
51
+ };
52
+ const renderItem = (item, index) => (/*#__PURE__*/React.createElement("li", {
53
+ key: getItemKey(item, index),
54
+ className: styles.list_item
55
+ }, /*#__PURE__*/React.createElement(NavigationLink, {
56
+ item: item,
57
+ onClick: onItemClick,
58
+ renderLink: renderLink
59
+ })));
60
+ return /*#__PURE__*/React.createElement("nav", Object.assign({}, props, {
61
+ ref: node => {
62
+ containerRef.current = node;
63
+ assignRef(navRef, node);
64
+ },
65
+ className: clsx(rootClassName, styles.overflow, {
66
+ [styles.overflow_measuring]: !split
67
+ })
68
+ }), /*#__PURE__*/React.createElement("ul", {
69
+ ref: listRef,
70
+ className: styles.list
71
+ }, visibleItems.map(renderItem), pinnedItems.map(renderItem), /*#__PURE__*/React.createElement("li", {
72
+ ref: moreRef,
73
+ className: clsx(styles.list_item, {
74
+ [styles.more_hidden]: isMoreHidden
75
+ }),
76
+ "aria-hidden": isMoreHidden || undefined
77
+ }, /*#__PURE__*/React.createElement(MoreMenu, {
78
+ items: hiddenItems,
79
+ label: moreLabel,
80
+ hidden: isMoreHidden,
81
+ onItemClick: onItemClick,
82
+ renderLink: renderLink,
83
+ onFocusLost: restoreFocus
84
+ }))), /*#__PURE__*/React.createElement("div", {
85
+ ref: measurerRef,
86
+ className: styles.measurer,
87
+ "aria-hidden": "true"
88
+ }, [...items, ...pinnedItems].map((item, index) => (/*#__PURE__*/React.createElement("span", {
89
+ key: getItemKey(item, index),
90
+ className: getLinkClassName("item", item.active)
91
+ }, /*#__PURE__*/React.createElement(LinkContent, {
92
+ item: item
93
+ }))))));
94
+ };
95
+ const ScrollMenu = _a => {
96
+ var {
97
+ items,
98
+ pinnedItems = [],
99
+ moreItems = [],
100
+ onItemClick,
101
+ renderLink,
102
+ navRef,
103
+ rootClassName
104
+ } = _a,
105
+ props = __rest(_a, ["items", "pinnedItems", "moreItems", "onItemClick", "renderLink", "navRef", "rootClassName"]);
106
+ const scrollRef = useRef(null);
107
+ const activeRef = useRef(null);
108
+ const allItems = [...items, ...pinnedItems, ...moreItems];
109
+ const activeIndex = getActiveIndex(allItems);
110
+ // Center the active item when it is not fully visible.
111
+ useIsomorphicLayoutEffect(() => {
112
+ const container = scrollRef.current;
113
+ const active = activeRef.current;
114
+ if (!container || !active) return;
115
+ const {
116
+ offsetLeft,
117
+ offsetWidth
118
+ } = active;
119
+ const isVisible = offsetLeft >= container.scrollLeft && offsetLeft + offsetWidth <= container.scrollLeft + container.clientWidth;
120
+ if (!isVisible) {
121
+ container.scrollLeft = offsetLeft + offsetWidth / 2 - container.clientWidth / 2;
122
+ }
123
+ }, [activeIndex]);
124
+ return /*#__PURE__*/React.createElement("nav", Object.assign({}, props, {
125
+ ref: navRef,
126
+ className: clsx(rootClassName, styles.scroll)
127
+ }), /*#__PURE__*/React.createElement("div", {
128
+ ref: scrollRef,
129
+ className: styles.scroll_area
130
+ }, /*#__PURE__*/React.createElement("ul", {
131
+ className: styles.list
132
+ }, allItems.map((item, index) => (/*#__PURE__*/React.createElement("li", {
133
+ key: getItemKey(item, index),
134
+ ref: index === activeIndex ? activeRef : undefined,
135
+ className: styles.list_item
136
+ }, /*#__PURE__*/React.createElement(NavigationLink, {
137
+ item: item,
138
+ onClick: onItemClick,
139
+ renderLink: renderLink
140
+ })))))));
141
+ };
142
+ const NavigationMenu = /*#__PURE__*/forwardRef((_a, ref) => {
143
+ var {
144
+ layout = "overflow",
145
+ size = "m",
146
+ moreLabel = "More",
147
+ className
148
+ } = _a,
149
+ props = __rest(_a, ["layout", "size", "moreLabel", "className"]);
150
+ const rootClassName = clsx(styles.root, styles[`size_${size}`], className);
151
+ return layout === "scroll" ? (/*#__PURE__*/React.createElement(ScrollMenu, Object.assign({}, props, {
152
+ navRef: ref,
153
+ rootClassName: rootClassName
154
+ }))) : (/*#__PURE__*/React.createElement(OverflowMenu, Object.assign({}, props, {
155
+ moreLabel: moreLabel,
156
+ navRef: ref,
157
+ rootClassName: rootClassName
158
+ })));
159
+ });
160
+ NavigationMenu.displayName = "NavigationMenu";
161
+
162
+ export { NavigationMenu };
@@ -0,0 +1,67 @@
1
+ .root {
2
+ font-family: var(--font-family-ui);
3
+ min-inline-size: 0;
4
+ }
5
+ .root.size_m {
6
+ --navigation-item-height: calc(var(--space-8) + var(--space-1));
7
+ --navigation-item-font-size: var(--font-size-text-md);
8
+ --navigation-item-line-height: var(--line-height-text-md);
9
+ --navigation-item-padding: var(--space-4);
10
+ --navigation-menu-gap: var(--space-2);
11
+ }
12
+ .root.size_s {
13
+ --navigation-item-height: var(--space-8);
14
+ --navigation-item-font-size: var(--font-size-text-sm);
15
+ --navigation-item-line-height: var(--line-height-text-sm);
16
+ --navigation-item-padding: var(--space-3);
17
+ --navigation-menu-gap: var(--space-1);
18
+ }
19
+
20
+ .list {
21
+ align-items: center;
22
+ display: flex;
23
+ gap: var(--navigation-menu-gap);
24
+ list-style: none;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+ .list_item {
29
+ display: flex;
30
+ flex-shrink: 0;
31
+ }
32
+
33
+ .overflow {
34
+ display: block;
35
+ position: relative;
36
+ }
37
+ .overflow_measuring {
38
+ overflow: hidden;
39
+ }
40
+
41
+ .more_hidden {
42
+ pointer-events: none;
43
+ position: absolute;
44
+ visibility: hidden;
45
+ }
46
+
47
+ .measurer {
48
+ block-size: 0;
49
+ display: flex;
50
+ inline-size: max-content;
51
+ inset-block-start: 0;
52
+ inset-inline-start: 0;
53
+ overflow: hidden;
54
+ pointer-events: none;
55
+ position: absolute;
56
+ visibility: hidden;
57
+ }
58
+
59
+ .scroll_area {
60
+ overflow: auto hidden;
61
+ overscroll-behavior-x: contain;
62
+ position: relative;
63
+ scrollbar-width: none;
64
+ }
65
+ .scroll_area::-webkit-scrollbar {
66
+ display: none;
67
+ }
@@ -0,0 +1,19 @@
1
+ .root {
2
+ display: contents;
3
+ }
4
+
5
+ .chevron {
6
+ flex-shrink: 0;
7
+ transition: transform 0.15s ease;
8
+ }
9
+ .chevron_open {
10
+ transform: rotate(180deg);
11
+ }
12
+
13
+ .options {
14
+ display: flex;
15
+ flex-direction: column;
16
+ font-family: var(--font-family-ui);
17
+ min-inline-size: calc(var(--space-32) + var(--space-16));
18
+ padding: var(--space-1);
19
+ }
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { NavigationMenuItem, NavigationMenuRenderLink } from "../../index.props";
4
+ interface MoreMenuProps {
5
+ items: NavigationMenuItem[];
6
+ label: ReactNode;
7
+ /** Nothing to show: the trigger stays in the DOM only to be measured. */
8
+ hidden: boolean;
9
+ onItemClick?: (item: NavigationMenuItem) => void;
10
+ renderLink?: NavigationMenuRenderLink;
11
+ /** The menu became hidden while this item had focus inside the dropdown. */
12
+ onFocusLost: (item: NavigationMenuItem) => void;
13
+ }
14
+ /** "More" trigger with a dropdown of the items that do not fit. */
15
+ export declare const MoreMenu: ({ items, label, hidden, onItemClick, renderLink, onFocusLost, }: MoreMenuProps) => React.JSX.Element;
16
+ export {};
@@ -0,0 +1,147 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Dropdown } from '../../../Dropdown/index.js';
4
+ import 'tslib';
5
+ import { Chevron } from '../../../Icons/Chevron.js';
6
+ import { useIsomorphicLayoutEffect, getItemKey } from '../../helpers.js';
7
+ import { getLinkClassName, NavigationLink } from '../NavigationLink/index.js';
8
+ import styles from './MoreMenu.module.css';
9
+
10
+ const getLinkPosition = event => {
11
+ const links = Array.from(event.currentTarget.querySelectorAll('[role="menuitem"]'));
12
+ return {
13
+ links,
14
+ index: links.indexOf(event.target)
15
+ };
16
+ };
17
+ const MoreOptions = ({
18
+ items,
19
+ onItemClick,
20
+ renderLink,
21
+ rootRef,
22
+ onItemFocus,
23
+ onTabOut
24
+ }) => {
25
+ // The dropdown is portaled to the end of the document: move focus into it,
26
+ // to the current section or to the first link.
27
+ useIsomorphicLayoutEffect(() => {
28
+ var _a, _b;
29
+ const root = rootRef.current;
30
+ (_b = (_a = root === null || root === void 0 ? void 0 : root.querySelector('[aria-current="page"]')) !== null && _a !== void 0 ? _a : root === null || root === void 0 ? void 0 : root.querySelector('[role="menuitem"]')) === null || _b === void 0 ? void 0 : _b.focus();
31
+ }, [rootRef]);
32
+ // Focus events bubble from the links, whatever `renderLink` renders.
33
+ const handleFocus = event => {
34
+ const {
35
+ index
36
+ } = getLinkPosition(event);
37
+ if (index !== -1) onItemFocus(items[index]);
38
+ };
39
+ // Arrow keys move focus through the links, Home/End go to the first and the
40
+ // last one. Tab leaves through the trigger, so focus continues from it.
41
+ const handleKeyDown = event => {
42
+ var _a;
43
+ if (event.key === "Tab") {
44
+ onTabOut();
45
+ return;
46
+ }
47
+ const {
48
+ links,
49
+ index
50
+ } = getLinkPosition(event);
51
+ const last = links.length - 1;
52
+ const targets = {
53
+ ArrowDown: index === last ? 0 : index + 1,
54
+ ArrowUp: index === 0 ? last : index - 1,
55
+ Home: 0,
56
+ End: last
57
+ };
58
+ if (index === -1 || !(event.key in targets)) return;
59
+ event.preventDefault();
60
+ (_a = links[targets[event.key]]) === null || _a === void 0 ? void 0 : _a.focus();
61
+ };
62
+ return (
63
+ /*#__PURE__*/
64
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- events come from the focused links
65
+ React.createElement("div", {
66
+ ref: rootRef,
67
+ className: styles.options,
68
+ onKeyDown: handleKeyDown,
69
+ onFocus: handleFocus
70
+ }, items.map((item, index) => (/*#__PURE__*/React.createElement(NavigationLink, {
71
+ key: getItemKey(item, index),
72
+ item: item,
73
+ view: "option",
74
+ role: "menuitem",
75
+ onClick: onItemClick,
76
+ renderLink: renderLink
77
+ }))))
78
+ );
79
+ };
80
+ /** "More" trigger with a dropdown of the items that do not fit. */
81
+ const MoreMenu = ({
82
+ items,
83
+ label,
84
+ hidden,
85
+ onItemClick,
86
+ renderLink,
87
+ onFocusLost
88
+ }) => {
89
+ const [isOpen, setOpen] = useState(false);
90
+ const rootRef = useRef(null);
91
+ const overlayRef = useRef(null);
92
+ const focusedItemRef = useRef(null);
93
+ // A resize hides "More" and removes the dropdown with the focused link.
94
+ // Layout effect: focus moves before the browser paints the new row.
95
+ useIsomorphicLayoutEffect(() => {
96
+ if (!hidden) return;
97
+ const item = focusedItemRef.current;
98
+ const isFocusLost = !document.activeElement || document.activeElement === document.body;
99
+ focusedItemRef.current = null;
100
+ setOpen(false);
101
+ if (item && isFocusLost) onFocusLost(item);
102
+ }, [hidden]);
103
+ // Selection, Escape and Tab remove the focused link together with the
104
+ // dropdown: return focus to the trigger.
105
+ const close = () => {
106
+ var _a, _b;
107
+ const trigger = (_a = rootRef.current) === null || _a === void 0 ? void 0 : _a.firstElementChild;
108
+ if (((_b = overlayRef.current) === null || _b === void 0 ? void 0 : _b.contains(document.activeElement)) && trigger instanceof HTMLElement) {
109
+ trigger.focus();
110
+ }
111
+ focusedItemRef.current = null;
112
+ setOpen(false);
113
+ };
114
+ return /*#__PURE__*/React.createElement("span", {
115
+ ref: rootRef,
116
+ className: styles.root
117
+ }, /*#__PURE__*/React.createElement(Dropdown, {
118
+ trigger: ["click"],
119
+ placement: "bottom-end",
120
+ visible: isOpen,
121
+ disabled: hidden,
122
+ onVisibleChange: visible => visible ? setOpen(true) : close(),
123
+ overlay: /*#__PURE__*/React.createElement(MoreOptions, {
124
+ items: items,
125
+ onItemClick: onItemClick,
126
+ renderLink: renderLink,
127
+ rootRef: overlayRef,
128
+ onItemFocus: item => {
129
+ focusedItemRef.current = item;
130
+ },
131
+ onTabOut: close
132
+ })
133
+ }, /*#__PURE__*/React.createElement("button", {
134
+ type: "button",
135
+ className: getLinkClassName(),
136
+ tabIndex: hidden ? -1 : undefined
137
+ }, label, /*#__PURE__*/React.createElement(Chevron, {
138
+ width: 16,
139
+ height: 16,
140
+ "aria-hidden": "true",
141
+ className: clsx(styles.chevron, {
142
+ [styles.chevron_open]: isOpen
143
+ })
144
+ }))));
145
+ };
146
+
147
+ export { MoreMenu };