@vesture/react 0.2.8 → 0.2.9

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/dist/index.js CHANGED
@@ -3246,14 +3246,17 @@ import {
3246
3246
  var chip = "Combobox_chip__26nhic2";
3247
3247
  var chipRemove = "Combobox_chipRemove__26nhic3";
3248
3248
  var chipsWrapper = "Combobox_chipsWrapper__26nhic1";
3249
- var emptyState2 = "Combobox_emptyState__26nhic7";
3249
+ var emptyState2 = "Combobox_emptyState__26nhic8";
3250
3250
  var inputEl = "Combobox_inputEl__26nhic4";
3251
3251
  var listbox = "Combobox_listbox__26nhic5";
3252
3252
  var option = "Combobox_option__26nhic6";
3253
+ var virtualSpacer = "Combobox_virtualSpacer__26nhic7";
3253
3254
  var wrapper9 = "Combobox_wrapper__26nhic0";
3254
3255
 
3255
3256
  // src/components/Combobox/Combobox.tsx
3256
3257
  import { jsx as jsx40, jsxs as jsxs23 } from "react/jsx-runtime";
3258
+ var DEFAULT_OPTION_HEIGHT = 37;
3259
+ var OVERSCAN = 5;
3257
3260
  function defaultFilter(options, inputText) {
3258
3261
  const query = inputText.trim().toLowerCase();
3259
3262
  if (!query) return options;
@@ -3276,6 +3279,8 @@ function Combobox({
3276
3279
  disabled = false,
3277
3280
  invalid,
3278
3281
  loading = false,
3282
+ virtualizationThreshold = 50,
3283
+ optionHeight = DEFAULT_OPTION_HEIGHT,
3279
3284
  id,
3280
3285
  className,
3281
3286
  ...rest
@@ -3302,8 +3307,37 @@ function Combobox({
3302
3307
  }
3303
3308
  }, [singleValue, multiple]);
3304
3309
  const inputRef = useRef9(null);
3310
+ const listboxRef = useRef9(null);
3305
3311
  const listboxId = useId2();
3312
+ const [scrollTop, setScrollTop] = useState14(0);
3313
+ const [viewportHeight, setViewportHeight] = useState14(280);
3306
3314
  const filteredOptions = filterOptions(options, multiple ? inputText : open ? inputText : "");
3315
+ const isVirtualized = filteredOptions.length >= virtualizationThreshold;
3316
+ useEffect7(() => {
3317
+ setScrollTop(0);
3318
+ setActiveIndex(null);
3319
+ if (listboxRef.current) listboxRef.current.scrollTop = 0;
3320
+ }, [options]);
3321
+ useEffect7(() => {
3322
+ if (!open) return;
3323
+ const node = listboxRef.current;
3324
+ if (!node) return;
3325
+ if (node.clientHeight > 0) setViewportHeight(node.clientHeight);
3326
+ if (typeof ResizeObserver === "undefined") return;
3327
+ const observer = new ResizeObserver((entries) => {
3328
+ const entry = entries[0];
3329
+ if (entry) setViewportHeight(entry.contentRect.height);
3330
+ });
3331
+ observer.observe(node);
3332
+ return () => observer.disconnect();
3333
+ }, [open]);
3334
+ const totalHeight = filteredOptions.length * optionHeight;
3335
+ const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
3336
+ const effectiveScrollTop = Math.min(scrollTop, maxScrollTop);
3337
+ const visibleCount = Math.ceil(viewportHeight / optionHeight) + OVERSCAN * 2;
3338
+ const startIndex = isVirtualized ? Math.max(0, Math.floor(effectiveScrollTop / optionHeight) - OVERSCAN) : 0;
3339
+ const endIndex = isVirtualized ? Math.min(filteredOptions.length, startIndex + visibleCount) : filteredOptions.length;
3340
+ const visibleOptions = filteredOptions.slice(startIndex, endIndex);
3307
3341
  const { refs, floatingStyles, context } = useFloating7({
3308
3342
  open,
3309
3343
  onOpenChange: setOpen,
@@ -3348,6 +3382,21 @@ function Combobox({
3348
3382
  setOpen(true);
3349
3383
  setActiveIndex(null);
3350
3384
  };
3385
+ const scrollIndexIntoView = (index) => {
3386
+ if (!isVirtualized) return;
3387
+ const rowTop = index * optionHeight;
3388
+ const rowBottom = rowTop + optionHeight;
3389
+ let next = scrollTop;
3390
+ if (rowTop < scrollTop) {
3391
+ next = rowTop;
3392
+ } else if (rowBottom > scrollTop + viewportHeight) {
3393
+ next = rowBottom - viewportHeight;
3394
+ } else {
3395
+ return;
3396
+ }
3397
+ setScrollTop(next);
3398
+ if (listboxRef.current) listboxRef.current.scrollTop = next;
3399
+ };
3351
3400
  const moveActiveIndex = (direction2) => {
3352
3401
  if (filteredOptions.length === 0) return;
3353
3402
  let next = activeIndex ?? (direction2 === 1 ? -1 : filteredOptions.length);
@@ -3355,6 +3404,7 @@ function Combobox({
3355
3404
  next = (next + direction2 + filteredOptions.length) % filteredOptions.length;
3356
3405
  if (!filteredOptions[next]?.disabled) {
3357
3406
  setActiveIndex(next);
3407
+ scrollIndexIntoView(next);
3358
3408
  return;
3359
3409
  }
3360
3410
  }
@@ -3448,14 +3498,40 @@ function Combobox({
3448
3498
  open ? /* @__PURE__ */ jsx40(FloatingPortal7, { children: /* @__PURE__ */ jsx40(
3449
3499
  "div",
3450
3500
  {
3451
- ref: refs.setFloating,
3501
+ ref: (node) => {
3502
+ refs.setFloating(node);
3503
+ listboxRef.current = node;
3504
+ },
3452
3505
  id: listboxId,
3453
3506
  role: "listbox",
3454
3507
  "aria-multiselectable": multiple || void 0,
3455
3508
  className: listbox,
3456
3509
  style: floatingStyles,
3510
+ onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
3457
3511
  ...getFloatingProps(),
3458
- children: loading ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: "Loading\u2026" }) : filteredOptions.length === 0 ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: noOptionsMessage }) : filteredOptions.map((opt, index) => {
3512
+ children: loading ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: "Loading\u2026" }) : filteredOptions.length === 0 ? /* @__PURE__ */ jsx40("div", { className: emptyState2, children: noOptionsMessage }) : isVirtualized ? /* @__PURE__ */ jsx40("div", { className: virtualSpacer, style: { height: totalHeight }, children: visibleOptions.map((opt, i) => {
3513
+ const index = startIndex + i;
3514
+ const isSelected = multiple ? selectedValues.includes(opt.value) : opt.value === singleValue;
3515
+ return /* @__PURE__ */ jsx40(
3516
+ "div",
3517
+ {
3518
+ id: `${listboxId}-option-${index}`,
3519
+ role: "option",
3520
+ "aria-selected": isSelected,
3521
+ "aria-disabled": opt.disabled || void 0,
3522
+ "data-active": activeIndex === index || void 0,
3523
+ className: option,
3524
+ style: { position: "absolute", top: index * optionHeight, left: 0, right: 0, height: optionHeight },
3525
+ onMouseDown: (event) => {
3526
+ event.preventDefault();
3527
+ selectOption(opt);
3528
+ },
3529
+ onMouseEnter: () => setActiveIndex(index),
3530
+ children: opt.label
3531
+ },
3532
+ opt.value
3533
+ );
3534
+ }) }) : filteredOptions.map((opt, index) => {
3459
3535
  const isSelected = multiple ? selectedValues.includes(opt.value) : opt.value === singleValue;
3460
3536
  return /* @__PURE__ */ jsx40(
3461
3537
  "div",