@avenue-ticketing/ui 0.9.0 → 0.10.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.
@@ -200,6 +200,85 @@ var Button = React5.forwardRef(
200
200
  }
201
201
  );
202
202
  Button.displayName = "Button";
203
+
204
+ // src/lib/typeahead.ts
205
+ var TYPEAHEAD_TIMEOUT_MS = 500;
206
+ function createTypeaheadState() {
207
+ return { search: "", timer: null };
208
+ }
209
+ function resetTypeahead(state) {
210
+ state.search = "";
211
+ if (state.timer) {
212
+ clearTimeout(state.timer);
213
+ state.timer = null;
214
+ }
215
+ }
216
+ function getItemLabel(item) {
217
+ const aria = item.getAttribute("aria-label")?.trim();
218
+ if (aria) return aria;
219
+ const marked = item.querySelector("[data-menu-label]");
220
+ if (marked?.textContent) return marked.textContent.replace(/\s+/g, " ").trim();
221
+ return (item.textContent ?? "").replace(/\s+/g, " ").trim();
222
+ }
223
+ function normalizeSearch(value) {
224
+ return value.trim().toLocaleLowerCase();
225
+ }
226
+ function isTypeaheadTarget(target) {
227
+ if (!(target instanceof HTMLElement)) return true;
228
+ if (target.isContentEditable) return false;
229
+ const tag = target.tagName;
230
+ return tag !== "INPUT" && tag !== "TEXTAREA" && tag !== "SELECT";
231
+ }
232
+ function handleTypeaheadKeyDown(event, items, state, options) {
233
+ if (options?.enabled === false || items.length === 0) return false;
234
+ if (event.ctrlKey || event.metaKey || event.altKey) return false;
235
+ if (event.key.length !== 1 || !isTypeaheadTarget(event.target)) return false;
236
+ event.preventDefault();
237
+ const timeoutMs = options?.timeoutMs ?? TYPEAHEAD_TIMEOUT_MS;
238
+ const char = event.key;
239
+ const prevSearch = state.search;
240
+ const repeatSingleChar = prevSearch.length === 1 && prevSearch === char;
241
+ if (repeatSingleChar) {
242
+ state.search = prevSearch;
243
+ } else {
244
+ state.search = prevSearch + char;
245
+ }
246
+ if (state.timer) clearTimeout(state.timer);
247
+ state.timer = setTimeout(() => {
248
+ state.search = "";
249
+ state.timer = null;
250
+ }, timeoutMs);
251
+ const labels = items.map((item) => normalizeSearch(getItemLabel(item)));
252
+ let needle = normalizeSearch(state.search);
253
+ let matches = items.map((item, index) => ({ item, index, label: labels[index] })).filter(({ label }) => label.startsWith(needle));
254
+ if (matches.length === 0 && state.search.length > 1) {
255
+ state.search = char;
256
+ needle = normalizeSearch(char);
257
+ matches = items.map((item, index) => ({ item, index, label: labels[index] })).filter(({ label }) => label.startsWith(needle));
258
+ }
259
+ if (matches.length === 0) return true;
260
+ const focused = document.activeElement;
261
+ const focusedIndex = focused ? items.indexOf(focused) : -1;
262
+ if (repeatSingleChar && focusedIndex !== -1) {
263
+ const currentMatch = matches.findIndex(
264
+ ({ index }) => index === focusedIndex
265
+ );
266
+ if (currentMatch !== -1) {
267
+ const next = matches[(currentMatch + 1) % matches.length];
268
+ next?.item.focus();
269
+ return true;
270
+ }
271
+ }
272
+ if (focusedIndex !== -1) {
273
+ const nextAfterFocus = matches.find(({ index }) => index > focusedIndex);
274
+ if (nextAfterFocus) {
275
+ nextAfterFocus.item.focus();
276
+ return true;
277
+ }
278
+ }
279
+ matches[0]?.item.focus();
280
+ return true;
281
+ }
203
282
  var DROPDOWN_PANEL_OPEN_EASING = "cubic-bezier(0,0.55,0.45,1)";
204
283
  var DROPDOWN_PANEL_CLOSE_EASING = "cubic-bezier(0.55,0,1,0.45)";
205
284
  var DROPDOWN_MENU_MIN_WIDTH_PX = 192;
@@ -491,7 +570,7 @@ function DropdownMobileBottomSheetPortal({
491
570
  "div",
492
571
  {
493
572
  className: cn(
494
- "fixed inset-0 bg-black/40 dark:bg-black/60",
573
+ "fixed inset-0 bg-black/40 dark:bg-primary/4",
495
574
  isAnimating ? "opacity-100" : "opacity-0"
496
575
  ),
497
576
  style: {
@@ -573,6 +652,7 @@ var DropdownContent = ({
573
652
  closeOnEscape = true,
574
653
  minWidth = "trigger",
575
654
  loop = true,
655
+ typeahead = true,
576
656
  mobileOptions,
577
657
  slideEntrance = true,
578
658
  slideEntranceOffsetPx: slideEntranceOffsetPxProp,
@@ -587,6 +667,7 @@ var DropdownContent = ({
587
667
  const [pos, setPos] = useState({ top: -9999, left: -9999, side });
588
668
  const [triggerW, setTriggerW] = useState(0);
589
669
  const menuRef = useRef(null);
670
+ const typeaheadStateRef = useRef(createTypeaheadState());
590
671
  const resolvedMobile = resolveDropdownMobileSheet(mobileOptions);
591
672
  const isMobileSheet = isMobile && resolvedMobile.sheet;
592
673
  const slideOffsetPx = slideEntranceOffsetPxProp ?? DROPDOWN_MOBILE_SHEET_SLIDE_ENTRANCE_OFFSET_DEFAULT_PX;
@@ -661,6 +742,9 @@ var DropdownContent = ({
661
742
  menuRef.current.focus();
662
743
  }
663
744
  }, [isAnimating]);
745
+ useEffect(() => {
746
+ if (!open) resetTypeahead(typeaheadStateRef.current);
747
+ }, [open]);
664
748
  useEffect(() => {
665
749
  if (!open) return;
666
750
  const handler = (e) => {
@@ -715,11 +799,16 @@ var DropdownContent = ({
715
799
  case "Tab":
716
800
  setOpen(false);
717
801
  break;
802
+ default:
803
+ handleTypeaheadKeyDown(e, items, typeaheadStateRef.current, {
804
+ enabled: typeahead
805
+ });
806
+ break;
718
807
  }
719
808
  };
720
809
  window.addEventListener("keydown", handler);
721
810
  return () => window.removeEventListener("keydown", handler);
722
- }, [open, closeOnEscape, loop, setOpen, triggerRef]);
811
+ }, [open, closeOnEscape, loop, typeahead, setOpen, triggerRef]);
723
812
  useEffect(() => {
724
813
  if (!open || !isMobileSheet) return;
725
814
  document.body.style.overflow = "hidden";
@@ -843,7 +932,7 @@ var DropdownItem = ({
843
932
  ),
844
933
  children: [
845
934
  icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: icon }),
846
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children }),
935
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", "data-menu-label": true, children }),
847
936
  shortcut ? /* @__PURE__ */ jsx(
848
937
  "span",
849
938
  {