@bigtablet/design-system 1.15.0 → 1.16.1

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.css CHANGED
@@ -683,6 +683,17 @@
683
683
  overflow-x: hidden;
684
684
  padding: 0.25rem 0;
685
685
  }
686
+ .select_list_up {
687
+ top: auto;
688
+ bottom: 100%;
689
+ margin-top: 0;
690
+ margin-bottom: 0.25rem;
691
+ }
692
+ .select_list_portal {
693
+ position: fixed;
694
+ margin-top: 0;
695
+ margin-bottom: 0;
696
+ }
686
697
  .select_option {
687
698
  width: 100%;
688
699
  box-sizing: border-box;
package/dist/index.d.ts CHANGED
@@ -109,7 +109,7 @@ interface SelectProps {
109
109
  className?: string;
110
110
  textAlign?: "left" | "center";
111
111
  }
112
- declare const Select: ({ id, label, placeholder, options, value, onChange, defaultValue, disabled, size, variant, fullWidth, className, textAlign }: SelectProps) => react_jsx_runtime.JSX.Element;
112
+ declare const Select: ({ id, label, placeholder, options, value, onChange, defaultValue, disabled, size, variant, fullWidth, className, textAlign, }: SelectProps) => react_jsx_runtime.JSX.Element;
113
113
 
114
114
  interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
115
115
  checked?: boolean;
package/dist/index.js CHANGED
@@ -315,7 +315,11 @@ var Select = ({
315
315
  const currentValue = isControlled ? value ?? null : internalValue;
316
316
  const [isOpen, setIsOpen] = React3.useState(false);
317
317
  const [activeIndex, setActiveIndex] = React3.useState(-1);
318
+ const [dropUp, setDropUp] = React3.useState(false);
319
+ const [listPosition, setListPosition] = React3.useState({ top: 0, left: 0, width: 0 });
318
320
  const wrapperRef = React3.useRef(null);
321
+ const controlRef = React3.useRef(null);
322
+ const listRef = React3.useRef(null);
319
323
  const currentOption = React3.useMemo(
320
324
  () => options.find((o) => o.value === currentValue) ?? null,
321
325
  [options, currentValue]
@@ -330,10 +334,10 @@ var Select = ({
330
334
  );
331
335
  React3.useEffect(() => {
332
336
  const onDocClick = (e) => {
333
- if (!wrapperRef.current) return;
334
- if (!wrapperRef.current.contains(e.target)) {
335
- setIsOpen(false);
336
- }
337
+ const target = e.target;
338
+ if (wrapperRef.current?.contains(target)) return;
339
+ if (listRef.current?.contains(target)) return;
340
+ setIsOpen(false);
337
341
  };
338
342
  document.addEventListener("mousedown", onDocClick);
339
343
  return () => document.removeEventListener("mousedown", onDocClick);
@@ -381,9 +385,7 @@ var Select = ({
381
385
  case "Home":
382
386
  e.preventDefault();
383
387
  setIsOpen(true);
384
- setActiveIndex(
385
- options.findIndex((o) => !o.disabled)
386
- );
388
+ setActiveIndex(options.findIndex((o) => !o.disabled));
387
389
  break;
388
390
  case "End":
389
391
  e.preventDefault();
@@ -403,16 +405,36 @@ var Select = ({
403
405
  };
404
406
  React3.useEffect(() => {
405
407
  if (!isOpen) return;
406
- const idx = options.findIndex(
407
- (o) => o.value === currentValue && !o.disabled
408
- );
409
- setActiveIndex(
410
- idx >= 0 ? idx : Math.max(
411
- 0,
412
- options.findIndex((o) => !o.disabled)
413
- )
414
- );
408
+ const idx = options.findIndex((o) => o.value === currentValue && !o.disabled);
409
+ setActiveIndex(idx >= 0 ? idx : Math.max(0, options.findIndex((o) => !o.disabled)));
415
410
  }, [isOpen, options, currentValue]);
411
+ const updatePosition = React3.useCallback(() => {
412
+ if (!controlRef.current) return;
413
+ const rect = controlRef.current.getBoundingClientRect();
414
+ const listHeight = Math.min(options.length * 40, 288);
415
+ const spaceBelow = window.innerHeight - rect.bottom;
416
+ const spaceAbove = rect.top;
417
+ const shouldDropUp = spaceBelow < listHeight && spaceAbove > spaceBelow;
418
+ setDropUp(shouldDropUp);
419
+ setListPosition({
420
+ top: shouldDropUp ? rect.top - listHeight - 4 : rect.bottom + 4,
421
+ left: rect.left,
422
+ width: rect.width
423
+ });
424
+ }, [options.length]);
425
+ React3.useLayoutEffect(() => {
426
+ if (!isOpen) return;
427
+ updatePosition();
428
+ }, [isOpen, updatePosition]);
429
+ React3.useEffect(() => {
430
+ if (!isOpen) return;
431
+ window.addEventListener("scroll", updatePosition, true);
432
+ window.addEventListener("resize", updatePosition);
433
+ return () => {
434
+ window.removeEventListener("scroll", updatePosition, true);
435
+ window.removeEventListener("resize", updatePosition);
436
+ };
437
+ }, [isOpen, updatePosition]);
416
438
  const rootClassName = ["select", className ?? ""].filter(Boolean).join(" ");
417
439
  const controlClassName = [
418
440
  "select_control",
@@ -421,92 +443,80 @@ var Select = ({
421
443
  isOpen && "is_open",
422
444
  disabled && "is_disabled"
423
445
  ].filter(Boolean).join(" ");
424
- return /* @__PURE__ */ jsxs(
425
- "div",
446
+ const renderList = () => /* @__PURE__ */ jsx(
447
+ "ul",
426
448
  {
427
- ref: wrapperRef,
428
- className: rootClassName,
429
- style: fullWidth ? { width: "100%" } : void 0,
430
- children: [
431
- label && /* @__PURE__ */ jsx(
432
- "label",
433
- {
434
- htmlFor: selectId,
435
- className: "select_label",
436
- children: label
437
- }
438
- ),
439
- /* @__PURE__ */ jsxs(
440
- "button",
449
+ ref: listRef,
450
+ id: `${selectId}_listbox`,
451
+ role: "listbox",
452
+ className: `select_list select_list_portal${dropUp ? " select_list_up" : ""}`,
453
+ style: {
454
+ position: "fixed",
455
+ top: listPosition.top,
456
+ left: listPosition.left,
457
+ width: listPosition.width
458
+ },
459
+ children: options.map((opt, i) => {
460
+ const selected = currentValue === opt.value;
461
+ const active = i === activeIndex;
462
+ const optionClassName = [
463
+ "select_option",
464
+ selected && "is_selected",
465
+ active && "is_active",
466
+ opt.disabled && "is_disabled"
467
+ ].filter(Boolean).join(" ");
468
+ return /* @__PURE__ */ jsxs(
469
+ "li",
441
470
  {
442
- id: selectId,
443
- type: "button",
444
- className: controlClassName,
445
- "aria-haspopup": "listbox",
446
- "aria-expanded": isOpen,
447
- "aria-controls": `${selectId}_listbox`,
448
- onClick: () => !disabled && setIsOpen((o) => !o),
449
- onKeyDown,
450
- disabled,
471
+ role: "option",
472
+ "aria-selected": selected,
473
+ className: optionClassName,
474
+ onMouseEnter: () => !opt.disabled && setActiveIndex(i),
475
+ onClick: () => {
476
+ if (opt.disabled) return;
477
+ setValue(opt.value);
478
+ setIsOpen(false);
479
+ },
451
480
  children: [
452
- /* @__PURE__ */ jsx(
453
- "span",
454
- {
455
- className: currentOption ? "select_value" : "select_placeholder",
456
- style: textAlign === "left" ? { textAlign: "start" } : void 0,
457
- children: currentOption ? currentOption.label : placeholder
458
- }
459
- ),
460
- /* @__PURE__ */ jsx("span", { className: "select_icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDown, { size: 16 }) })
481
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
482
+ selected && /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": "true" })
461
483
  ]
462
- }
463
- ),
464
- isOpen && /* @__PURE__ */ jsx(
465
- "ul",
466
- {
467
- id: `${selectId}_listbox`,
468
- role: "listbox",
469
- className: "select_list",
470
- children: options.map((opt, i) => {
471
- const selected = currentValue === opt.value;
472
- const active = i === activeIndex;
473
- const optionClassName = [
474
- "select_option",
475
- selected && "is_selected",
476
- active && "is_active",
477
- opt.disabled && "is_disabled"
478
- ].filter(Boolean).join(" ");
479
- return /* @__PURE__ */ jsxs(
480
- "li",
481
- {
482
- role: "option",
483
- "aria-selected": selected,
484
- className: optionClassName,
485
- onMouseEnter: () => !opt.disabled && setActiveIndex(i),
486
- onClick: () => {
487
- if (opt.disabled) return;
488
- setValue(opt.value);
489
- setIsOpen(false);
490
- },
491
- children: [
492
- /* @__PURE__ */ jsx("span", { children: opt.label }),
493
- selected && /* @__PURE__ */ jsx(
494
- Check,
495
- {
496
- size: 16,
497
- "aria-hidden": "true"
498
- }
499
- )
500
- ]
501
- },
502
- opt.value
503
- );
504
- })
505
- }
506
- )
507
- ]
484
+ },
485
+ opt.value
486
+ );
487
+ })
508
488
  }
509
489
  );
490
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: rootClassName, style: fullWidth ? { width: "100%" } : void 0, children: [
491
+ label && /* @__PURE__ */ jsx("label", { htmlFor: selectId, className: "select_label", children: label }),
492
+ /* @__PURE__ */ jsxs(
493
+ "button",
494
+ {
495
+ ref: controlRef,
496
+ id: selectId,
497
+ type: "button",
498
+ className: controlClassName,
499
+ "aria-haspopup": "listbox",
500
+ "aria-expanded": isOpen,
501
+ "aria-controls": `${selectId}_listbox`,
502
+ onClick: () => !disabled && setIsOpen((o) => !o),
503
+ onKeyDown,
504
+ disabled,
505
+ children: [
506
+ /* @__PURE__ */ jsx(
507
+ "span",
508
+ {
509
+ className: currentOption ? "select_value" : "select_placeholder",
510
+ style: textAlign === "left" ? { textAlign: "start" } : void 0,
511
+ children: currentOption ? currentOption.label : placeholder
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx("span", { className: "select_icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDown, { size: 16 }) })
515
+ ]
516
+ }
517
+ ),
518
+ isOpen && typeof document !== "undefined" && createPortal(renderList(), document.body)
519
+ ] });
510
520
  };
511
521
  var Switch = ({
512
522
  checked,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigtablet/design-system",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "Bigtablet Design System UI Components",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",