@astralui/core 0.1.12 → 0.1.13
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.cjs +140 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +141 -37
- package/dist/index.js.map +1 -1
- package/dist/styles.css +5 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -367,11 +367,16 @@ function AstralPinInput({ length = 6, value, onChange, autoFocus }) {
|
|
|
367
367
|
i
|
|
368
368
|
)) });
|
|
369
369
|
}
|
|
370
|
-
function AstralSelect({ value, onChange, options, placeholder, disabled, searchable, searchPlaceholder, noResults, clearable, creatable }) {
|
|
370
|
+
function AstralSelect({ value, onChange, options, placeholder, disabled, searchable, searchPlaceholder, noResults, clearable, creatable, id, ariaLabel, ariaLabelledBy, clearLabel = "Clear selection" }) {
|
|
371
371
|
const [open, setOpen] = react.useState(false);
|
|
372
372
|
const [q, setQ] = react.useState("");
|
|
373
373
|
const triggerRef = react.useRef(null);
|
|
374
374
|
const menuRef = react.useRef(null);
|
|
375
|
+
const [activeIndex, setActiveIndex] = react.useState(-1);
|
|
376
|
+
const reactId = react.useId();
|
|
377
|
+
const baseId = id ?? `au-select-${reactId}`;
|
|
378
|
+
const listboxId = `${baseId}-listbox`;
|
|
379
|
+
const optionId = (i) => `${baseId}-opt-${i}`;
|
|
375
380
|
const [pos, setPos] = react.useState(null);
|
|
376
381
|
const reposition = react.useCallback(() => {
|
|
377
382
|
const el = triggerRef.current;
|
|
@@ -387,7 +392,10 @@ function AstralSelect({ value, onChange, options, placeholder, disabled, searcha
|
|
|
387
392
|
});
|
|
388
393
|
}, []);
|
|
389
394
|
react.useEffect(() => {
|
|
390
|
-
if (!open)
|
|
395
|
+
if (!open) {
|
|
396
|
+
setActiveIndex(-1);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
391
399
|
reposition();
|
|
392
400
|
const onScroll = () => reposition();
|
|
393
401
|
const onResize = () => setOpen(false);
|
|
@@ -410,28 +418,116 @@ function AstralSelect({ value, onChange, options, placeholder, disabled, searcha
|
|
|
410
418
|
const display = selected ? selected.label : value ?? "";
|
|
411
419
|
const filtered = searchable && q.trim() ? options.filter((o) => o.label.toLowerCase().includes(q.trim().toLowerCase())) : options;
|
|
412
420
|
const showCreate = !!creatable && !!q.trim() && !options.some((o) => o.label.toLowerCase() === q.trim().toLowerCase());
|
|
421
|
+
const rows = [...filtered.map((o) => o.value), ...showCreate ? [q.trim()] : []];
|
|
422
|
+
react.useEffect(() => {
|
|
423
|
+
if (!open || activeIndex < 0) return;
|
|
424
|
+
const el = menuRef.current?.querySelector(`#${CSS.escape(optionId(activeIndex))}`);
|
|
425
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
426
|
+
}, [open, activeIndex, baseId]);
|
|
427
|
+
const commit = (index) => {
|
|
428
|
+
const picked = rows[index];
|
|
429
|
+
if (picked == null) return;
|
|
430
|
+
onChange(picked);
|
|
431
|
+
setOpen(false);
|
|
432
|
+
setQ("");
|
|
433
|
+
triggerRef.current?.focus();
|
|
434
|
+
};
|
|
435
|
+
const onKeyDown = (e) => {
|
|
436
|
+
if (disabled) return;
|
|
437
|
+
const key = e.key;
|
|
438
|
+
if (!open) {
|
|
439
|
+
if (key === "ArrowDown" || key === "ArrowUp" || key === "Enter" || key === " ") {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
setOpen(true);
|
|
442
|
+
setActiveIndex(0);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (key === "Escape") {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
e.stopPropagation();
|
|
449
|
+
setOpen(false);
|
|
450
|
+
setQ("");
|
|
451
|
+
triggerRef.current?.focus();
|
|
452
|
+
} else if (key === "ArrowDown") {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
setActiveIndex((i) => rows.length ? (i + 1) % rows.length : -1);
|
|
455
|
+
} else if (key === "ArrowUp") {
|
|
456
|
+
e.preventDefault();
|
|
457
|
+
setActiveIndex((i) => rows.length ? i <= 0 ? rows.length - 1 : i - 1 : -1);
|
|
458
|
+
} else if (key === "Home") {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
setActiveIndex(rows.length ? 0 : -1);
|
|
461
|
+
} else if (key === "End") {
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
setActiveIndex(rows.length - 1);
|
|
464
|
+
} else if (key === "Enter") {
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
commit(activeIndex >= 0 ? activeIndex : 0);
|
|
467
|
+
} else if (key === "Tab") {
|
|
468
|
+
setOpen(false);
|
|
469
|
+
setQ("");
|
|
470
|
+
}
|
|
471
|
+
};
|
|
413
472
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-select", children: [
|
|
414
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
474
|
+
"button",
|
|
475
|
+
{
|
|
476
|
+
ref: triggerRef,
|
|
477
|
+
id: baseId,
|
|
478
|
+
type: "button",
|
|
479
|
+
className: "au-input au-select-trigger",
|
|
480
|
+
disabled,
|
|
481
|
+
role: "combobox",
|
|
482
|
+
"aria-expanded": open,
|
|
483
|
+
"aria-haspopup": "listbox",
|
|
484
|
+
"aria-controls": open ? listboxId : void 0,
|
|
485
|
+
"aria-activedescendant": open && !searchable && activeIndex >= 0 ? optionId(activeIndex) : void 0,
|
|
486
|
+
"aria-label": ariaLabel,
|
|
487
|
+
"aria-labelledby": ariaLabelledBy,
|
|
488
|
+
onClick: () => setOpen((o) => !o),
|
|
489
|
+
onKeyDown,
|
|
490
|
+
children: [
|
|
491
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: hasValue ? "" : "ph", children: hasValue ? display : placeholder }),
|
|
492
|
+
clearable && hasValue ? (
|
|
493
|
+
// Was a <span role="button" tabIndex={-1}>: announced as a button and
|
|
494
|
+
// reachable by nobody, so a keyboard user could not clear the field.
|
|
495
|
+
// It is a real button now, and it stops the click bubbling to the
|
|
496
|
+
// trigger so clearing does not also open the menu.
|
|
497
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
498
|
+
"span",
|
|
499
|
+
{
|
|
500
|
+
className: "au-select-caret au-select-clear",
|
|
501
|
+
role: "button",
|
|
502
|
+
tabIndex: 0,
|
|
503
|
+
"aria-label": clearLabel,
|
|
504
|
+
onClick: (e) => {
|
|
505
|
+
e.stopPropagation();
|
|
506
|
+
onChange("");
|
|
507
|
+
},
|
|
508
|
+
onKeyDown: (e) => {
|
|
509
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
e.stopPropagation();
|
|
512
|
+
onChange("");
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, { size: 14 })
|
|
516
|
+
}
|
|
517
|
+
)
|
|
518
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconChevronDown, { size: 15, className: "au-select-caret" })
|
|
519
|
+
]
|
|
520
|
+
}
|
|
521
|
+
),
|
|
430
522
|
open && !disabled && pos && reactDom.createPortal(
|
|
431
523
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
432
524
|
"div",
|
|
433
525
|
{
|
|
434
526
|
ref: menuRef,
|
|
527
|
+
id: listboxId,
|
|
528
|
+
role: "listbox",
|
|
529
|
+
"aria-label": ariaLabel,
|
|
530
|
+
"aria-labelledby": ariaLabel ? void 0 : ariaLabelledBy,
|
|
435
531
|
className: "au-select-menu",
|
|
436
532
|
style: { left: pos.left, top: pos.top, bottom: pos.bottom, width: pos.width, maxHeight: pos.maxHeight },
|
|
437
533
|
children: [
|
|
@@ -441,35 +537,43 @@ function AstralSelect({ value, onChange, options, placeholder, disabled, searcha
|
|
|
441
537
|
autoFocus: true,
|
|
442
538
|
className: "au-select-search",
|
|
443
539
|
placeholder: searchPlaceholder,
|
|
540
|
+
"aria-label": searchPlaceholder,
|
|
541
|
+
"aria-controls": listboxId,
|
|
542
|
+
role: "combobox",
|
|
543
|
+
"aria-expanded": true,
|
|
544
|
+
"aria-autocomplete": "list",
|
|
545
|
+
"aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : void 0,
|
|
444
546
|
value: q,
|
|
445
|
-
onChange: (e) =>
|
|
547
|
+
onChange: (e) => {
|
|
548
|
+
setQ(e.currentTarget.value);
|
|
549
|
+
setActiveIndex(0);
|
|
550
|
+
},
|
|
551
|
+
onKeyDown
|
|
446
552
|
}
|
|
447
553
|
),
|
|
448
554
|
filtered.length === 0 && !showCreate && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-select-empty", children: noResults }),
|
|
449
|
-
filtered.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
450
|
-
"
|
|
555
|
+
filtered.map((o, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
556
|
+
"div",
|
|
451
557
|
{
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
},
|
|
558
|
+
id: optionId(i),
|
|
559
|
+
role: "option",
|
|
560
|
+
"aria-selected": o.value === value,
|
|
561
|
+
className: `au-select-opt${o.value === value ? " on" : ""}${i === activeIndex ? " active" : ""}`,
|
|
562
|
+
onMouseEnter: () => setActiveIndex(i),
|
|
563
|
+
onClick: () => commit(i),
|
|
459
564
|
children: o.label
|
|
460
565
|
},
|
|
461
566
|
o.value
|
|
462
567
|
)),
|
|
463
568
|
showCreate && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
464
|
-
"
|
|
569
|
+
"div",
|
|
465
570
|
{
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
},
|
|
571
|
+
id: optionId(filtered.length),
|
|
572
|
+
role: "option",
|
|
573
|
+
"aria-selected": false,
|
|
574
|
+
className: `au-select-opt au-select-create${filtered.length === activeIndex ? " active" : ""}`,
|
|
575
|
+
onMouseEnter: () => setActiveIndex(filtered.length),
|
|
576
|
+
onClick: () => commit(filtered.length),
|
|
473
577
|
children: [
|
|
474
578
|
"+ ",
|
|
475
579
|
q.trim()
|