@diabolic/hangover 0.1.4 → 0.1.6
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 +1 -0
- package/dist/hangover.css +31 -2
- package/dist/index.cjs.js +140 -1
- package/dist/index.esm.js +140 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A React 18 compound-component Dropdown / Field Picker library.
|
|
|
11
11
|
|
|
12
12
|
- **Compound Components** - Composable `Dropdown.Trigger`, `Panel`, `Navigation`, `Section`, `Group`, `Item` API
|
|
13
13
|
- **Fuzzy Search** - Built-in fuzzy filtering across items (powered by fuse.js)
|
|
14
|
+
- **Keyboard Navigation** - Arrow up/down to move between visible items (before and after searching), Enter to trigger
|
|
14
15
|
- **Two Display Modes** - Scroll-spy with smooth scroll or one-section-at-a-time tab mode
|
|
15
16
|
- **Left Navigation** - Optional nav column with auto-collapse and single-section auto-transform
|
|
16
17
|
- **Checkbox Items** - Multi-select with select-all support
|
package/dist/hangover.css
CHANGED
|
@@ -59,6 +59,12 @@
|
|
|
59
59
|
.hangoverDropdown--dark .hangoverDropdown-list:hover::-webkit-scrollbar-thumb, .hangoverDropdown--dark .hangoverDropdown-list:focus-within::-webkit-scrollbar-thumb {
|
|
60
60
|
background: rgba(255, 255, 255, 0.18);
|
|
61
61
|
}
|
|
62
|
+
.hangoverDropdown--dark .hangoverDropdown-column.forNavigation:hover, .hangoverDropdown--dark .hangoverDropdown-column.forNavigation:focus-within {
|
|
63
|
+
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
|
|
64
|
+
}
|
|
65
|
+
.hangoverDropdown--dark .hangoverDropdown-column.forNavigation:hover::-webkit-scrollbar-thumb, .hangoverDropdown--dark .hangoverDropdown-column.forNavigation:focus-within::-webkit-scrollbar-thumb {
|
|
66
|
+
background: rgba(255, 255, 255, 0.18);
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
@keyframes hangoverDropdownIn {
|
|
64
70
|
from {
|
|
@@ -147,7 +153,28 @@
|
|
|
147
153
|
box-sizing: border-box;
|
|
148
154
|
flex-shrink: 0;
|
|
149
155
|
flex: none;
|
|
150
|
-
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
overscroll-behavior: none;
|
|
158
|
+
scrollbar-width: none;
|
|
159
|
+
scrollbar-color: transparent transparent;
|
|
160
|
+
transition: width 330ms ease, min-width 330ms ease, scrollbar-color 330ms ease;
|
|
161
|
+
}
|
|
162
|
+
.hangoverDropdown-column.forNavigation:hover, .hangoverDropdown-column.forNavigation:focus-within {
|
|
163
|
+
scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
|
|
164
|
+
}
|
|
165
|
+
.hangoverDropdown-column.forNavigation::-webkit-scrollbar {
|
|
166
|
+
width: 4px;
|
|
167
|
+
}
|
|
168
|
+
.hangoverDropdown-column.forNavigation::-webkit-scrollbar-track {
|
|
169
|
+
background: transparent;
|
|
170
|
+
}
|
|
171
|
+
.hangoverDropdown-column.forNavigation::-webkit-scrollbar-thumb {
|
|
172
|
+
background: transparent;
|
|
173
|
+
border-radius: 2px;
|
|
174
|
+
transition: background 330ms ease;
|
|
175
|
+
}
|
|
176
|
+
.hangoverDropdown-column.forNavigation:hover::-webkit-scrollbar-thumb, .hangoverDropdown-column.forNavigation:focus-within::-webkit-scrollbar-thumb {
|
|
177
|
+
background: rgba(0, 0, 0, 0.18);
|
|
151
178
|
}
|
|
152
179
|
.hangoverDropdown-column.forNavigation .hangoverDropdown-nav-item {
|
|
153
180
|
transition: width 330ms ease, padding 330ms ease, justify-content 330ms ease, gap 330ms ease, background 330ms ease;
|
|
@@ -463,7 +490,9 @@
|
|
|
463
490
|
background: var(--hangover-color-bg-hover);
|
|
464
491
|
}
|
|
465
492
|
.hangoverDropdown-item:focus-visible {
|
|
466
|
-
|
|
493
|
+
background: var(--hangover-color-bg-hover);
|
|
494
|
+
box-shadow: none;
|
|
495
|
+
outline: none;
|
|
467
496
|
}
|
|
468
497
|
.hangoverDropdown-item.isSelected {
|
|
469
498
|
background: var(--hangover-color-bg-selected);
|
package/dist/index.cjs.js
CHANGED
|
@@ -450,9 +450,39 @@ function DropdownNavItem({
|
|
|
450
450
|
t
|
|
451
451
|
} = useDropdownContext();
|
|
452
452
|
const isActive = activeNavId === id;
|
|
453
|
+
const buttonRef = react.useRef(null);
|
|
453
454
|
react.useEffect(() => {
|
|
454
455
|
registerNavLabel(id, typeof children === 'string' ? children : '');
|
|
455
456
|
}, [id, children, registerNavLabel]);
|
|
457
|
+
|
|
458
|
+
// When this item becomes active (e.g. via scroll-spy on the right column),
|
|
459
|
+
// keep it visible inside the scrollable nav column.
|
|
460
|
+
react.useEffect(() => {
|
|
461
|
+
if (!isActive) return;
|
|
462
|
+
const el = buttonRef.current;
|
|
463
|
+
if (!el) return;
|
|
464
|
+
const container = el.closest('.hangoverDropdown-column.forNavigation');
|
|
465
|
+
if (!container) return;
|
|
466
|
+
|
|
467
|
+
// Leave a gap-sized breathing space so the active item never sits flush
|
|
468
|
+
// against the top/bottom edge of the nav column.
|
|
469
|
+
const navList = el.parentElement;
|
|
470
|
+
const gap = navList ? parseFloat(getComputedStyle(navList).rowGap || getComputedStyle(navList).gap) || 0 : 0;
|
|
471
|
+
const containerRect = container.getBoundingClientRect();
|
|
472
|
+
const elRect = el.getBoundingClientRect();
|
|
473
|
+
let delta = 0;
|
|
474
|
+
if (elRect.top < containerRect.top + gap) {
|
|
475
|
+
delta = elRect.top - containerRect.top - gap;
|
|
476
|
+
} else if (elRect.bottom > containerRect.bottom - gap) {
|
|
477
|
+
delta = elRect.bottom - containerRect.bottom + gap;
|
|
478
|
+
}
|
|
479
|
+
if (delta !== 0) {
|
|
480
|
+
container.scrollTo({
|
|
481
|
+
top: container.scrollTop + delta,
|
|
482
|
+
behavior: 'smooth'
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}, [isActive]);
|
|
456
486
|
function handleClick() {
|
|
457
487
|
fireEvent('navChange', {
|
|
458
488
|
id
|
|
@@ -498,6 +528,7 @@ function DropdownNavItem({
|
|
|
498
528
|
}
|
|
499
529
|
return /*#__PURE__*/jsxRuntime.jsxs("button", {
|
|
500
530
|
type: "button",
|
|
531
|
+
ref: buttonRef,
|
|
501
532
|
className: `hangoverDropdown-nav-item${isActive ? ' isActive' : ''}`,
|
|
502
533
|
onClick: () => {
|
|
503
534
|
handleClick();
|
|
@@ -647,6 +678,8 @@ function DropdownContent({
|
|
|
647
678
|
setScrollSpyActive,
|
|
648
679
|
t
|
|
649
680
|
} = useDropdownContext();
|
|
681
|
+
const searchInputRef = react.useRef(null);
|
|
682
|
+
const bottomPadRef = react.useRef(0);
|
|
650
683
|
|
|
651
684
|
// Scroll spy: update active nav based on scroll position
|
|
652
685
|
react.useEffect(() => {
|
|
@@ -695,11 +728,114 @@ function DropdownContent({
|
|
|
695
728
|
if (displayMode !== 'tab') return;
|
|
696
729
|
if (contentRef.current) contentRef.current.scrollTop = 0;
|
|
697
730
|
}, [displayMode, activeNavId, contentRef]);
|
|
731
|
+
|
|
732
|
+
// Scroll mode: reserve enough space at the bottom of the list so the last
|
|
733
|
+
// section (even a short single-entry one) can be scrolled all the way to the
|
|
734
|
+
// top. Without this, a short last section can never reach the top because the
|
|
735
|
+
// container has already hit its maximum scroll position.
|
|
736
|
+
react.useEffect(() => {
|
|
737
|
+
if (displayMode !== 'scroll') return;
|
|
738
|
+
const list = contentRef.current;
|
|
739
|
+
if (!list) return;
|
|
740
|
+
function updateBottomSpace() {
|
|
741
|
+
const sections = list.querySelectorAll('[data-section-for]');
|
|
742
|
+
const lastSection = sections[sections.length - 1];
|
|
743
|
+
if (!lastSection) {
|
|
744
|
+
list.style.paddingBottom = '';
|
|
745
|
+
bottomPadRef.current = 0;
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Derive the natural content height by subtracting our own reserved
|
|
750
|
+
// space, so recomputes never compound. Using real geometry for the last
|
|
751
|
+
// section's top keeps this margin-safe (bottom padding sits after it, so
|
|
752
|
+
// it never shifts the section's top position).
|
|
753
|
+
const available = list.clientHeight;
|
|
754
|
+
const naturalScrollHeight = list.scrollHeight - bottomPadRef.current;
|
|
755
|
+
const listTop = list.getBoundingClientRect().top;
|
|
756
|
+
const lastTopWithinContent = lastSection.getBoundingClientRect().top - listTop + list.scrollTop;
|
|
757
|
+
const spaceBelowLastTop = naturalScrollHeight - lastTopWithinContent;
|
|
758
|
+
const pad = Math.max(0, available - spaceBelowLastTop);
|
|
759
|
+
bottomPadRef.current = pad;
|
|
760
|
+
list.style.paddingBottom = `${pad}px`;
|
|
761
|
+
}
|
|
762
|
+
updateBottomSpace();
|
|
763
|
+
|
|
764
|
+
// Observe both the container (viewport resize) and every section (group
|
|
765
|
+
// expand/collapse, late reflows) so the reserved space stays accurate.
|
|
766
|
+
const observer = new ResizeObserver(updateBottomSpace);
|
|
767
|
+
observer.observe(list);
|
|
768
|
+
list.querySelectorAll('[data-section-for]').forEach(section => {
|
|
769
|
+
observer.observe(section);
|
|
770
|
+
});
|
|
771
|
+
return () => observer.disconnect();
|
|
772
|
+
}, [displayMode, contentRef, children, searchQuery]);
|
|
698
773
|
function handleSearch(e) {
|
|
699
774
|
fireEvent('search', {
|
|
700
775
|
query: e.target.value
|
|
701
776
|
});
|
|
702
777
|
}
|
|
778
|
+
|
|
779
|
+
// Keyboard navigation: ArrowDown/ArrowUp move focus across the currently
|
|
780
|
+
// visible items (works before and after searching, since filtered items
|
|
781
|
+
// are removed from the DOM). Enter is handled by each item itself.
|
|
782
|
+
function scrollItemIntoView(el) {
|
|
783
|
+
const list = contentRef.current;
|
|
784
|
+
if (!list || !el) return;
|
|
785
|
+
|
|
786
|
+
// Offset by the sticky section header so the item stays fully visible
|
|
787
|
+
// when navigating upwards.
|
|
788
|
+
const stickyEl = list.querySelector('.hangoverDropdown-section-title');
|
|
789
|
+
const stickyHeight = stickyEl ? stickyEl.offsetHeight : 0;
|
|
790
|
+
const listRect = list.getBoundingClientRect();
|
|
791
|
+
const elRect = el.getBoundingClientRect();
|
|
792
|
+
let delta = 0;
|
|
793
|
+
if (elRect.top < listRect.top + stickyHeight) {
|
|
794
|
+
delta = elRect.top - (listRect.top + stickyHeight);
|
|
795
|
+
} else if (elRect.bottom > listRect.bottom) {
|
|
796
|
+
delta = elRect.bottom - listRect.bottom;
|
|
797
|
+
}
|
|
798
|
+
if (delta !== 0) {
|
|
799
|
+
list.scrollTo({
|
|
800
|
+
top: list.scrollTop + delta,
|
|
801
|
+
behavior: 'smooth'
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function moveItemFocus(direction) {
|
|
806
|
+
const list = contentRef.current;
|
|
807
|
+
if (!list) return;
|
|
808
|
+
const items = Array.from(list.querySelectorAll('.hangoverDropdown-item')).filter(el => el.offsetParent !== null);
|
|
809
|
+
if (items.length === 0) return;
|
|
810
|
+
const active = document.activeElement;
|
|
811
|
+
const idx = items.indexOf(active);
|
|
812
|
+
if (direction === 1) {
|
|
813
|
+
const next = idx < 0 ? items[0] : items[idx + 1];
|
|
814
|
+
if (next) {
|
|
815
|
+
next.focus({
|
|
816
|
+
preventScroll: true
|
|
817
|
+
});
|
|
818
|
+
scrollItemIntoView(next);
|
|
819
|
+
}
|
|
820
|
+
} else if (idx > 0) {
|
|
821
|
+
const prev = items[idx - 1];
|
|
822
|
+
prev.focus({
|
|
823
|
+
preventScroll: true
|
|
824
|
+
});
|
|
825
|
+
scrollItemIntoView(prev);
|
|
826
|
+
} else if (idx === 0 && searchInputRef.current) {
|
|
827
|
+
searchInputRef.current.focus();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function handleKeyNav(e) {
|
|
831
|
+
if (e.key === 'ArrowDown') {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
moveItemFocus(1);
|
|
834
|
+
} else if (e.key === 'ArrowUp') {
|
|
835
|
+
e.preventDefault();
|
|
836
|
+
moveItemFocus(-1);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
703
839
|
const isEmpty = react.Children.count(children) === 0;
|
|
704
840
|
const inner = /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
|
|
705
841
|
children: [!isEmpty && /*#__PURE__*/jsxRuntime.jsxs("label", {
|
|
@@ -713,12 +849,15 @@ function DropdownContent({
|
|
|
713
849
|
placeholder: t(searchPlaceholder),
|
|
714
850
|
"aria-label": t(searchPlaceholder),
|
|
715
851
|
value: searchQuery,
|
|
716
|
-
onChange: handleSearch
|
|
852
|
+
onChange: handleSearch,
|
|
853
|
+
onKeyDown: handleKeyNav,
|
|
854
|
+
ref: searchInputRef
|
|
717
855
|
})]
|
|
718
856
|
}), /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
719
857
|
role: "listbox",
|
|
720
858
|
className: `hangoverDropdown-list${displayMode === 'tab' ? ' isTabMode' : ''}${displayMode === 'tab' && activeNavId === '__all__' ? ' isAllActive' : ''}`,
|
|
721
859
|
ref: contentRef,
|
|
860
|
+
onKeyDown: handleKeyNav,
|
|
722
861
|
children: isEmpty ? /*#__PURE__*/jsxRuntime.jsx("div", {
|
|
723
862
|
className: "hangoverDropdown-content-empty",
|
|
724
863
|
children: t(emptyText)
|
package/dist/index.esm.js
CHANGED
|
@@ -446,9 +446,39 @@ function DropdownNavItem({
|
|
|
446
446
|
t
|
|
447
447
|
} = useDropdownContext();
|
|
448
448
|
const isActive = activeNavId === id;
|
|
449
|
+
const buttonRef = useRef(null);
|
|
449
450
|
useEffect(() => {
|
|
450
451
|
registerNavLabel(id, typeof children === 'string' ? children : '');
|
|
451
452
|
}, [id, children, registerNavLabel]);
|
|
453
|
+
|
|
454
|
+
// When this item becomes active (e.g. via scroll-spy on the right column),
|
|
455
|
+
// keep it visible inside the scrollable nav column.
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
if (!isActive) return;
|
|
458
|
+
const el = buttonRef.current;
|
|
459
|
+
if (!el) return;
|
|
460
|
+
const container = el.closest('.hangoverDropdown-column.forNavigation');
|
|
461
|
+
if (!container) return;
|
|
462
|
+
|
|
463
|
+
// Leave a gap-sized breathing space so the active item never sits flush
|
|
464
|
+
// against the top/bottom edge of the nav column.
|
|
465
|
+
const navList = el.parentElement;
|
|
466
|
+
const gap = navList ? parseFloat(getComputedStyle(navList).rowGap || getComputedStyle(navList).gap) || 0 : 0;
|
|
467
|
+
const containerRect = container.getBoundingClientRect();
|
|
468
|
+
const elRect = el.getBoundingClientRect();
|
|
469
|
+
let delta = 0;
|
|
470
|
+
if (elRect.top < containerRect.top + gap) {
|
|
471
|
+
delta = elRect.top - containerRect.top - gap;
|
|
472
|
+
} else if (elRect.bottom > containerRect.bottom - gap) {
|
|
473
|
+
delta = elRect.bottom - containerRect.bottom + gap;
|
|
474
|
+
}
|
|
475
|
+
if (delta !== 0) {
|
|
476
|
+
container.scrollTo({
|
|
477
|
+
top: container.scrollTop + delta,
|
|
478
|
+
behavior: 'smooth'
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}, [isActive]);
|
|
452
482
|
function handleClick() {
|
|
453
483
|
fireEvent('navChange', {
|
|
454
484
|
id
|
|
@@ -494,6 +524,7 @@ function DropdownNavItem({
|
|
|
494
524
|
}
|
|
495
525
|
return /*#__PURE__*/jsxs("button", {
|
|
496
526
|
type: "button",
|
|
527
|
+
ref: buttonRef,
|
|
497
528
|
className: `hangoverDropdown-nav-item${isActive ? ' isActive' : ''}`,
|
|
498
529
|
onClick: () => {
|
|
499
530
|
handleClick();
|
|
@@ -643,6 +674,8 @@ function DropdownContent({
|
|
|
643
674
|
setScrollSpyActive,
|
|
644
675
|
t
|
|
645
676
|
} = useDropdownContext();
|
|
677
|
+
const searchInputRef = useRef(null);
|
|
678
|
+
const bottomPadRef = useRef(0);
|
|
646
679
|
|
|
647
680
|
// Scroll spy: update active nav based on scroll position
|
|
648
681
|
useEffect(() => {
|
|
@@ -691,11 +724,114 @@ function DropdownContent({
|
|
|
691
724
|
if (displayMode !== 'tab') return;
|
|
692
725
|
if (contentRef.current) contentRef.current.scrollTop = 0;
|
|
693
726
|
}, [displayMode, activeNavId, contentRef]);
|
|
727
|
+
|
|
728
|
+
// Scroll mode: reserve enough space at the bottom of the list so the last
|
|
729
|
+
// section (even a short single-entry one) can be scrolled all the way to the
|
|
730
|
+
// top. Without this, a short last section can never reach the top because the
|
|
731
|
+
// container has already hit its maximum scroll position.
|
|
732
|
+
useEffect(() => {
|
|
733
|
+
if (displayMode !== 'scroll') return;
|
|
734
|
+
const list = contentRef.current;
|
|
735
|
+
if (!list) return;
|
|
736
|
+
function updateBottomSpace() {
|
|
737
|
+
const sections = list.querySelectorAll('[data-section-for]');
|
|
738
|
+
const lastSection = sections[sections.length - 1];
|
|
739
|
+
if (!lastSection) {
|
|
740
|
+
list.style.paddingBottom = '';
|
|
741
|
+
bottomPadRef.current = 0;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Derive the natural content height by subtracting our own reserved
|
|
746
|
+
// space, so recomputes never compound. Using real geometry for the last
|
|
747
|
+
// section's top keeps this margin-safe (bottom padding sits after it, so
|
|
748
|
+
// it never shifts the section's top position).
|
|
749
|
+
const available = list.clientHeight;
|
|
750
|
+
const naturalScrollHeight = list.scrollHeight - bottomPadRef.current;
|
|
751
|
+
const listTop = list.getBoundingClientRect().top;
|
|
752
|
+
const lastTopWithinContent = lastSection.getBoundingClientRect().top - listTop + list.scrollTop;
|
|
753
|
+
const spaceBelowLastTop = naturalScrollHeight - lastTopWithinContent;
|
|
754
|
+
const pad = Math.max(0, available - spaceBelowLastTop);
|
|
755
|
+
bottomPadRef.current = pad;
|
|
756
|
+
list.style.paddingBottom = `${pad}px`;
|
|
757
|
+
}
|
|
758
|
+
updateBottomSpace();
|
|
759
|
+
|
|
760
|
+
// Observe both the container (viewport resize) and every section (group
|
|
761
|
+
// expand/collapse, late reflows) so the reserved space stays accurate.
|
|
762
|
+
const observer = new ResizeObserver(updateBottomSpace);
|
|
763
|
+
observer.observe(list);
|
|
764
|
+
list.querySelectorAll('[data-section-for]').forEach(section => {
|
|
765
|
+
observer.observe(section);
|
|
766
|
+
});
|
|
767
|
+
return () => observer.disconnect();
|
|
768
|
+
}, [displayMode, contentRef, children, searchQuery]);
|
|
694
769
|
function handleSearch(e) {
|
|
695
770
|
fireEvent('search', {
|
|
696
771
|
query: e.target.value
|
|
697
772
|
});
|
|
698
773
|
}
|
|
774
|
+
|
|
775
|
+
// Keyboard navigation: ArrowDown/ArrowUp move focus across the currently
|
|
776
|
+
// visible items (works before and after searching, since filtered items
|
|
777
|
+
// are removed from the DOM). Enter is handled by each item itself.
|
|
778
|
+
function scrollItemIntoView(el) {
|
|
779
|
+
const list = contentRef.current;
|
|
780
|
+
if (!list || !el) return;
|
|
781
|
+
|
|
782
|
+
// Offset by the sticky section header so the item stays fully visible
|
|
783
|
+
// when navigating upwards.
|
|
784
|
+
const stickyEl = list.querySelector('.hangoverDropdown-section-title');
|
|
785
|
+
const stickyHeight = stickyEl ? stickyEl.offsetHeight : 0;
|
|
786
|
+
const listRect = list.getBoundingClientRect();
|
|
787
|
+
const elRect = el.getBoundingClientRect();
|
|
788
|
+
let delta = 0;
|
|
789
|
+
if (elRect.top < listRect.top + stickyHeight) {
|
|
790
|
+
delta = elRect.top - (listRect.top + stickyHeight);
|
|
791
|
+
} else if (elRect.bottom > listRect.bottom) {
|
|
792
|
+
delta = elRect.bottom - listRect.bottom;
|
|
793
|
+
}
|
|
794
|
+
if (delta !== 0) {
|
|
795
|
+
list.scrollTo({
|
|
796
|
+
top: list.scrollTop + delta,
|
|
797
|
+
behavior: 'smooth'
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function moveItemFocus(direction) {
|
|
802
|
+
const list = contentRef.current;
|
|
803
|
+
if (!list) return;
|
|
804
|
+
const items = Array.from(list.querySelectorAll('.hangoverDropdown-item')).filter(el => el.offsetParent !== null);
|
|
805
|
+
if (items.length === 0) return;
|
|
806
|
+
const active = document.activeElement;
|
|
807
|
+
const idx = items.indexOf(active);
|
|
808
|
+
if (direction === 1) {
|
|
809
|
+
const next = idx < 0 ? items[0] : items[idx + 1];
|
|
810
|
+
if (next) {
|
|
811
|
+
next.focus({
|
|
812
|
+
preventScroll: true
|
|
813
|
+
});
|
|
814
|
+
scrollItemIntoView(next);
|
|
815
|
+
}
|
|
816
|
+
} else if (idx > 0) {
|
|
817
|
+
const prev = items[idx - 1];
|
|
818
|
+
prev.focus({
|
|
819
|
+
preventScroll: true
|
|
820
|
+
});
|
|
821
|
+
scrollItemIntoView(prev);
|
|
822
|
+
} else if (idx === 0 && searchInputRef.current) {
|
|
823
|
+
searchInputRef.current.focus();
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function handleKeyNav(e) {
|
|
827
|
+
if (e.key === 'ArrowDown') {
|
|
828
|
+
e.preventDefault();
|
|
829
|
+
moveItemFocus(1);
|
|
830
|
+
} else if (e.key === 'ArrowUp') {
|
|
831
|
+
e.preventDefault();
|
|
832
|
+
moveItemFocus(-1);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
699
835
|
const isEmpty = Children.count(children) === 0;
|
|
700
836
|
const inner = /*#__PURE__*/jsxs(Fragment, {
|
|
701
837
|
children: [!isEmpty && /*#__PURE__*/jsxs("label", {
|
|
@@ -709,12 +845,15 @@ function DropdownContent({
|
|
|
709
845
|
placeholder: t(searchPlaceholder),
|
|
710
846
|
"aria-label": t(searchPlaceholder),
|
|
711
847
|
value: searchQuery,
|
|
712
|
-
onChange: handleSearch
|
|
848
|
+
onChange: handleSearch,
|
|
849
|
+
onKeyDown: handleKeyNav,
|
|
850
|
+
ref: searchInputRef
|
|
713
851
|
})]
|
|
714
852
|
}), /*#__PURE__*/jsx("div", {
|
|
715
853
|
role: "listbox",
|
|
716
854
|
className: `hangoverDropdown-list${displayMode === 'tab' ? ' isTabMode' : ''}${displayMode === 'tab' && activeNavId === '__all__' ? ' isAllActive' : ''}`,
|
|
717
855
|
ref: contentRef,
|
|
856
|
+
onKeyDown: handleKeyNav,
|
|
718
857
|
children: isEmpty ? /*#__PURE__*/jsx("div", {
|
|
719
858
|
className: "hangoverDropdown-content-empty",
|
|
720
859
|
children: t(emptyText)
|