@allxsmith/bestax-bulma 5.51.0 → 5.52.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.
- package/dist/bestax.css +1 -1
- package/dist/bestax.css.map +1 -1
- package/dist/esm/components/Command.js +637 -0
- package/dist/esm/components/Command.js.map +1 -0
- package/dist/esm/form/Autocomplete.js +2 -2
- package/dist/esm/form/CheckboxCard.js +118 -0
- package/dist/esm/form/CheckboxCard.js.map +1 -0
- package/dist/esm/form/Checkboxes.js +14 -3
- package/dist/esm/form/Checkboxes.js.map +1 -1
- package/dist/esm/form/RadioCard.js +85 -0
- package/dist/esm/form/RadioCard.js.map +1 -0
- package/dist/esm/form/Radios.js +14 -3
- package/dist/esm/form/Radios.js.map +1 -1
- package/dist/esm/form/groupFieldset.js +38 -0
- package/dist/esm/form/groupFieldset.js.map +1 -0
- package/dist/esm/form/useAutoLabelId.js +1 -1
- package/dist/esm/helpers/useComboboxEngine.js +3 -3
- package/dist/esm/helpers/useFocusTrap.js +2 -2
- package/dist/esm/helpers/useFocusTrap.js.map +1 -1
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +2429 -1566
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +3 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/Command.d.ts +318 -0
- package/dist/types/form/CheckboxCard.d.ts +94 -0
- package/dist/types/form/Checkboxes.d.ts +13 -0
- package/dist/types/form/RadioCard.d.ts +81 -0
- package/dist/types/form/Radios.d.ts +13 -0
- package/dist/types/form/groupFieldset.d.ts +32 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types-cjs/components/Command.d.ts +318 -0
- package/dist/types-cjs/form/CheckboxCard.d.ts +94 -0
- package/dist/types-cjs/form/Checkboxes.d.ts +13 -0
- package/dist/types-cjs/form/RadioCard.d.ts +81 -0
- package/dist/types-cjs/form/Radios.d.ts +13 -0
- package/dist/types-cjs/form/groupFieldset.d.ts +32 -0
- package/dist/types-cjs/index.d.ts +3 -0
- package/dist/versions/bestax-no-dark-mode.css +1 -1
- package/dist/versions/bestax-no-dark-mode.css.map +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
- package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
- package/dist/versions/bestax-no-helpers.css +1 -1
- package/dist/versions/bestax-no-helpers.css.map +1 -1
- package/dist/versions/bestax-prefixed.css +1 -1
- package/dist/versions/bestax-prefixed.css.map +1 -1
- package/package.json +1 -1
- package/src/scss/components/_command.scss +259 -0
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/form/_checkbox.scss +44 -0
- package/src/scss/form/_checkboxcard.scss +355 -0
- package/src/scss/form/_index.scss +2 -0
- package/src/scss/form/_radio.scss +44 -0
- package/src/scss/form/_radiocard.scss +291 -0
package/dist/index.cjs
CHANGED
|
@@ -4156,7 +4156,7 @@ function flatChildren(el) {
|
|
|
4156
4156
|
}
|
|
4157
4157
|
return Array.from(el.children);
|
|
4158
4158
|
}
|
|
4159
|
-
const isShown = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
|
|
4159
|
+
const isShown$1 = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
|
|
4160
4160
|
const isVisible = (el) => {
|
|
4161
4161
|
const { visibility } = getComputedStyle(el);
|
|
4162
4162
|
return visibility !== 'hidden' && visibility !== 'collapse';
|
|
@@ -4170,7 +4170,7 @@ const isVisible = (el) => {
|
|
|
4170
4170
|
function isShownUpTo(el, top) {
|
|
4171
4171
|
let below = null;
|
|
4172
4172
|
for (let node = el; node && node !== top; node = flatParent(node)) {
|
|
4173
|
-
if (!isShown(node))
|
|
4173
|
+
if (!isShown$1(node))
|
|
4174
4174
|
return false;
|
|
4175
4175
|
if (below &&
|
|
4176
4176
|
node.localName === 'details' &&
|
|
@@ -11425,6 +11425,1482 @@ const ScrollArea = React.forwardRef(({ className, style, textColor, bgColor, hei
|
|
|
11425
11425
|
});
|
|
11426
11426
|
ScrollArea.displayName = 'ScrollArea';
|
|
11427
11427
|
|
|
11428
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
11429
|
+
/** An IME is composing, so the key belongs to it (`keyCode` 229 is Safari's word for it). */
|
|
11430
|
+
const isComposing$2 = (event) => event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
|
11431
|
+
const sameValues = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
|
|
11432
|
+
/**
|
|
11433
|
+
* The keys, the open state, the highlight and the ARIA of an editable
|
|
11434
|
+
* combobox whose list the app renders and filters (#1083). DOM focus stays
|
|
11435
|
+
* in the field the whole time: the highlight is `aria-activedescendant`, as
|
|
11436
|
+
* the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
|
|
11437
|
+
* real focus, as a menu should.
|
|
11438
|
+
*
|
|
11439
|
+
* The options are read from the listbox in document order whenever they're
|
|
11440
|
+
* needed, so options in groups and options that come and go are found, and
|
|
11441
|
+
* the highlight is remembered by value, so it survives a re-render. The text
|
|
11442
|
+
* in the field and any value are the app's: a pick only calls
|
|
11443
|
+
* `onSelect`.
|
|
11444
|
+
*
|
|
11445
|
+
* @function useComboboxEngine
|
|
11446
|
+
* @param options - The open state, the pick callback and the shape.
|
|
11447
|
+
* @returns The state and handlers the parts wire up.
|
|
11448
|
+
*/
|
|
11449
|
+
function useComboboxEngine({ open: controlledOpen, defaultOpen, onOpenChange, onSelect, onSettle, keepFirst, multiple, inline, deferred, }) {
|
|
11450
|
+
const baseId = React.useId();
|
|
11451
|
+
const listboxId = `${baseId}-listbox`;
|
|
11452
|
+
const itemPrefix = `${baseId}-item-`;
|
|
11453
|
+
// The value goes into the id encoded, so a value with a space in it still
|
|
11454
|
+
// makes one id, not two.
|
|
11455
|
+
const itemId = React.useCallback((value) => itemPrefix + encodeURIComponent(value), [itemPrefix]);
|
|
11456
|
+
const rootRef = React.useRef(null);
|
|
11457
|
+
const inputRef = React.useRef(null);
|
|
11458
|
+
const listboxRef = React.useRef(null);
|
|
11459
|
+
const popupRef = React.useRef(null);
|
|
11460
|
+
const insidePressRef = React.useRef(null);
|
|
11461
|
+
const optionsRef = React.useRef(new Map());
|
|
11462
|
+
const landingRef = React.useRef(null);
|
|
11463
|
+
const wasOpenRef = React.useRef(false);
|
|
11464
|
+
const shownValuesRef = React.useRef([]);
|
|
11465
|
+
// Whether the next highlight change is one to scroll to: a key's or a
|
|
11466
|
+
// landing's. The pointer's isn't, since the pointer is already on that
|
|
11467
|
+
// option, and scrolling would move the list out from under it.
|
|
11468
|
+
const scrollToHighlightRef = React.useRef(false);
|
|
11469
|
+
const isControlled = controlledOpen !== undefined;
|
|
11470
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
11471
|
+
const open = inline || (controlledOpen ?? internalOpen);
|
|
11472
|
+
const [highlighted, setHighlighted] = React.useState(null);
|
|
11473
|
+
const [typed, setTyped] = React.useState(false);
|
|
11474
|
+
// Whether a listbox with an option in it is on the page, which only the
|
|
11475
|
+
// page can say, so it's read once the open list has rendered.
|
|
11476
|
+
const [listShown, setListShown] = React.useState(null);
|
|
11477
|
+
// Options register from their own effects, which needn't render the
|
|
11478
|
+
// component that holds this hook, so they ask for a render to be read.
|
|
11479
|
+
const [, refresh] = React.useReducer((n) => n + 1, 0);
|
|
11480
|
+
// Closing forgets the highlight, the typing and what was on the page,
|
|
11481
|
+
// during render, React's pattern for adjusting state when other state
|
|
11482
|
+
// changes.
|
|
11483
|
+
if (!open) {
|
|
11484
|
+
if (highlighted !== null)
|
|
11485
|
+
setHighlighted(null);
|
|
11486
|
+
if (typed)
|
|
11487
|
+
setTyped(false);
|
|
11488
|
+
if (listShown !== null)
|
|
11489
|
+
setListShown(null);
|
|
11490
|
+
}
|
|
11491
|
+
const expanded = open && (listShown ?? !deferred);
|
|
11492
|
+
/** The options on the page, in document order, groups included. */
|
|
11493
|
+
const shownOptions = React.useCallback(() => {
|
|
11494
|
+
const listbox = listboxRef.current;
|
|
11495
|
+
if (!listbox)
|
|
11496
|
+
return [];
|
|
11497
|
+
const shown = [];
|
|
11498
|
+
listbox
|
|
11499
|
+
.querySelectorAll('[role="option"]')
|
|
11500
|
+
.forEach(element => {
|
|
11501
|
+
// Only the Combobox's own options: an element given the role by
|
|
11502
|
+
// hand isn't one.
|
|
11503
|
+
const state = optionsRef.current.get(element);
|
|
11504
|
+
if (state)
|
|
11505
|
+
shown.push({ element, ...state });
|
|
11506
|
+
});
|
|
11507
|
+
return shown;
|
|
11508
|
+
}, []);
|
|
11509
|
+
const requestOpen = React.useCallback((next, landing = 'open') => {
|
|
11510
|
+
if (inline || next === open)
|
|
11511
|
+
return;
|
|
11512
|
+
landingRef.current = next ? landing : null;
|
|
11513
|
+
onOpenChange?.(next);
|
|
11514
|
+
if (!isControlled)
|
|
11515
|
+
setInternalOpen(next);
|
|
11516
|
+
}, [inline, open, onOpenChange, isControlled]);
|
|
11517
|
+
const close = React.useCallback(() => requestOpen(false), [requestOpen]);
|
|
11518
|
+
// After every render, read the list: whether it has options, and where
|
|
11519
|
+
// the highlight goes now. A highlighted option that has left takes the
|
|
11520
|
+
// highlight with it, or hands it to the first enabled one under
|
|
11521
|
+
// `keepFirst`, which also moves it there whenever the options change.
|
|
11522
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
|
|
11523
|
+
useClientLayoutEffect(() => {
|
|
11524
|
+
const opened = open && !wasOpenRef.current;
|
|
11525
|
+
wasOpenRef.current = open;
|
|
11526
|
+
if (!open) {
|
|
11527
|
+
landingRef.current = null;
|
|
11528
|
+
shownValuesRef.current = [];
|
|
11529
|
+
return;
|
|
11530
|
+
}
|
|
11531
|
+
const options = shownOptions();
|
|
11532
|
+
const values = options.map(option => option.value);
|
|
11533
|
+
const changed = !sameValues(values, shownValuesRef.current);
|
|
11534
|
+
shownValuesRef.current = values;
|
|
11535
|
+
setListShown(values.length > 0);
|
|
11536
|
+
const landing = landingRef.current ?? (opened ? 'open' : null);
|
|
11537
|
+
landingRef.current = null;
|
|
11538
|
+
const enabled = options.filter(option => !option.disabled);
|
|
11539
|
+
const checked = enabled.find(option => option.checked)?.value;
|
|
11540
|
+
const first = enabled.length > 0 ? enabled[0].value : null;
|
|
11541
|
+
const last = enabled.length > 0 ? enabled[enabled.length - 1].value : null;
|
|
11542
|
+
let next = highlighted;
|
|
11543
|
+
if (landing === 'first')
|
|
11544
|
+
next = checked ?? first;
|
|
11545
|
+
else if (landing === 'last')
|
|
11546
|
+
next = checked ?? last;
|
|
11547
|
+
else if (landing === 'open')
|
|
11548
|
+
next = checked ?? (keepFirst ? first : null);
|
|
11549
|
+
else if (landing === 'typed')
|
|
11550
|
+
next = keepFirst ? first : null;
|
|
11551
|
+
else if (landing === 'none')
|
|
11552
|
+
next = null;
|
|
11553
|
+
else if (keepFirst && changed)
|
|
11554
|
+
next = first;
|
|
11555
|
+
// Without `keepFirst`, which moved it above, a highlighted option
|
|
11556
|
+
// that left takes the highlight with it.
|
|
11557
|
+
else if (next !== null && !values.includes(next))
|
|
11558
|
+
next = null;
|
|
11559
|
+
// Only a change is set. StrictMode runs this twice on mount, and the
|
|
11560
|
+
// second run, with the landing used up, works out the rendered highlight
|
|
11561
|
+
// again, so it leaves the first run's landing in place.
|
|
11562
|
+
if (next !== highlighted) {
|
|
11563
|
+
scrollToHighlightRef.current = true;
|
|
11564
|
+
setHighlighted(next);
|
|
11565
|
+
}
|
|
11566
|
+
});
|
|
11567
|
+
// A browser scrolls to what has focus, not to an active descendant, so
|
|
11568
|
+
// the highlighted option is brought into view here, when a key or a
|
|
11569
|
+
// landing moved it there.
|
|
11570
|
+
useClientLayoutEffect(() => {
|
|
11571
|
+
if (highlighted === null || !scrollToHighlightRef.current)
|
|
11572
|
+
return;
|
|
11573
|
+
scrollToHighlightRef.current = false;
|
|
11574
|
+
const option = shownOptions().find(shown => shown.value === highlighted);
|
|
11575
|
+
option?.element.scrollIntoView?.({ block: 'nearest' });
|
|
11576
|
+
}, [highlighted, shownOptions]);
|
|
11577
|
+
// A press outside the field and the list closes it. The path is checked
|
|
11578
|
+
// rather than the target, so it works in a shadow root.
|
|
11579
|
+
React.useEffect(() => {
|
|
11580
|
+
const root = rootRef.current;
|
|
11581
|
+
if (!open || inline || !root)
|
|
11582
|
+
return undefined;
|
|
11583
|
+
const onPress = (event) => {
|
|
11584
|
+
if (event === insidePressRef.current ||
|
|
11585
|
+
isEventInside(event, rootRef.current) ||
|
|
11586
|
+
isEventInside(event, popupRef.current)) {
|
|
11587
|
+
return;
|
|
11588
|
+
}
|
|
11589
|
+
close();
|
|
11590
|
+
};
|
|
11591
|
+
// Its document, an iframe's when the tree renders into one.
|
|
11592
|
+
const doc = root.ownerDocument;
|
|
11593
|
+
doc.addEventListener('pointerdown', onPress);
|
|
11594
|
+
return () => doc.removeEventListener('pointerdown', onPress);
|
|
11595
|
+
}, [open, inline, close]);
|
|
11596
|
+
const submit = React.useCallback((value) => {
|
|
11597
|
+
onSelect?.(value);
|
|
11598
|
+
if (!multiple)
|
|
11599
|
+
close();
|
|
11600
|
+
}, [onSelect, multiple, close]);
|
|
11601
|
+
const onKeyDown = (event) => {
|
|
11602
|
+
// The caller's handler ran first and can keep a key, and an IME that's
|
|
11603
|
+
// composing owns every key.
|
|
11604
|
+
if (event.defaultPrevented || isComposing$2(event))
|
|
11605
|
+
return;
|
|
11606
|
+
const { key, altKey } = event;
|
|
11607
|
+
if (!open) {
|
|
11608
|
+
if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
|
|
11609
|
+
event.preventDefault();
|
|
11610
|
+
requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
|
|
11611
|
+
}
|
|
11612
|
+
return;
|
|
11613
|
+
}
|
|
11614
|
+
switch (key) {
|
|
11615
|
+
case 'ArrowDown':
|
|
11616
|
+
case 'ArrowUp': {
|
|
11617
|
+
if (altKey) {
|
|
11618
|
+
if (key === 'ArrowUp' && !inline) {
|
|
11619
|
+
event.preventDefault();
|
|
11620
|
+
close();
|
|
11621
|
+
}
|
|
11622
|
+
return;
|
|
11623
|
+
}
|
|
11624
|
+
event.preventDefault();
|
|
11625
|
+
// Disabled options too, so a screen reader user finds them.
|
|
11626
|
+
const options = shownOptions();
|
|
11627
|
+
if (options.length === 0)
|
|
11628
|
+
return;
|
|
11629
|
+
const at = options.findIndex(option => option.value === highlighted);
|
|
11630
|
+
const step = key === 'ArrowDown' ? 1 : -1;
|
|
11631
|
+
const index = at === -1
|
|
11632
|
+
? step === 1
|
|
11633
|
+
? 0
|
|
11634
|
+
: options.length - 1
|
|
11635
|
+
: (at + step + options.length) % options.length;
|
|
11636
|
+
scrollToHighlightRef.current = true;
|
|
11637
|
+
setHighlighted(options[index].value);
|
|
11638
|
+
return;
|
|
11639
|
+
}
|
|
11640
|
+
case 'Enter': {
|
|
11641
|
+
const option = shownOptions().find(shown => shown.value === highlighted);
|
|
11642
|
+
if (option) {
|
|
11643
|
+
event.preventDefault();
|
|
11644
|
+
if (!option.disabled)
|
|
11645
|
+
submit(option.value);
|
|
11646
|
+
return;
|
|
11647
|
+
}
|
|
11648
|
+
// Nothing to pick: the key goes on, so a form submits.
|
|
11649
|
+
onSettle?.();
|
|
11650
|
+
close();
|
|
11651
|
+
return;
|
|
11652
|
+
}
|
|
11653
|
+
case 'Escape':
|
|
11654
|
+
if (inline)
|
|
11655
|
+
return;
|
|
11656
|
+
// It stops here only when there was something to close, so a
|
|
11657
|
+
// Popover or Modal around it keeps its own Escape otherwise. Nothing
|
|
11658
|
+
// stops it going on: a handler further out reads `defaultPrevented`.
|
|
11659
|
+
if (popupRef.current)
|
|
11660
|
+
event.preventDefault();
|
|
11661
|
+
close();
|
|
11662
|
+
return;
|
|
11663
|
+
case 'Tab':
|
|
11664
|
+
close();
|
|
11665
|
+
return;
|
|
11666
|
+
case 'Home':
|
|
11667
|
+
case 'End':
|
|
11668
|
+
case 'ArrowLeft':
|
|
11669
|
+
case 'ArrowRight':
|
|
11670
|
+
// The field's own keys, which take the field back from the list.
|
|
11671
|
+
setHighlighted(null);
|
|
11672
|
+
return;
|
|
11673
|
+
}
|
|
11674
|
+
};
|
|
11675
|
+
const onChange = () => {
|
|
11676
|
+
setTyped(true);
|
|
11677
|
+
if (open) {
|
|
11678
|
+
landingRef.current = 'typed';
|
|
11679
|
+
setHighlighted(null);
|
|
11680
|
+
refresh();
|
|
11681
|
+
}
|
|
11682
|
+
else {
|
|
11683
|
+
requestOpen(true, 'typed');
|
|
11684
|
+
}
|
|
11685
|
+
};
|
|
11686
|
+
const onClick = () => {
|
|
11687
|
+
requestOpen(true, 'open');
|
|
11688
|
+
};
|
|
11689
|
+
const onBlur = (event) => {
|
|
11690
|
+
const next = event.relatedTarget;
|
|
11691
|
+
if (next &&
|
|
11692
|
+
(rootRef.current?.contains(next) || popupRef.current?.contains(next))) {
|
|
11693
|
+
return;
|
|
11694
|
+
}
|
|
11695
|
+
close();
|
|
11696
|
+
};
|
|
11697
|
+
const hover = (value) => {
|
|
11698
|
+
scrollToHighlightRef.current = false;
|
|
11699
|
+
setHighlighted(value);
|
|
11700
|
+
};
|
|
11701
|
+
const pick = (value) => {
|
|
11702
|
+
submit(value);
|
|
11703
|
+
// A press in the list keeps focus in the field, but a browser that
|
|
11704
|
+
// ignores that, or a tap, can leave it elsewhere.
|
|
11705
|
+
const input = inputRef.current;
|
|
11706
|
+
if (input && input.isConnected && getActiveElementInTree(input) !== input) {
|
|
11707
|
+
input.focus();
|
|
11708
|
+
}
|
|
11709
|
+
};
|
|
11710
|
+
const keepFocus = (event) => event.preventDefault();
|
|
11711
|
+
const markInside = (event) => {
|
|
11712
|
+
insidePressRef.current = event.nativeEvent;
|
|
11713
|
+
};
|
|
11714
|
+
const registerPopup = React.useCallback((element) => {
|
|
11715
|
+
popupRef.current = element;
|
|
11716
|
+
return () => {
|
|
11717
|
+
popupRef.current = null;
|
|
11718
|
+
};
|
|
11719
|
+
}, []);
|
|
11720
|
+
const registerItem = React.useCallback((element, state) => {
|
|
11721
|
+
const options = optionsRef.current;
|
|
11722
|
+
options.set(element, state);
|
|
11723
|
+
refresh();
|
|
11724
|
+
return () => {
|
|
11725
|
+
options.delete(element);
|
|
11726
|
+
refresh();
|
|
11727
|
+
};
|
|
11728
|
+
}, []);
|
|
11729
|
+
return {
|
|
11730
|
+
open,
|
|
11731
|
+
expanded,
|
|
11732
|
+
highlighted,
|
|
11733
|
+
typed,
|
|
11734
|
+
listboxId,
|
|
11735
|
+
itemId,
|
|
11736
|
+
rootRef,
|
|
11737
|
+
inputRef,
|
|
11738
|
+
listboxRef,
|
|
11739
|
+
registerPopup,
|
|
11740
|
+
registerItem,
|
|
11741
|
+
onKeyDown,
|
|
11742
|
+
onChange,
|
|
11743
|
+
onClick,
|
|
11744
|
+
onBlur,
|
|
11745
|
+
hover,
|
|
11746
|
+
pick,
|
|
11747
|
+
keepFocus,
|
|
11748
|
+
markInside,
|
|
11749
|
+
};
|
|
11750
|
+
}
|
|
11751
|
+
|
|
11752
|
+
/**
|
|
11753
|
+
* Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
|
|
11754
|
+
* `react-icons` component, …) in a slot that accepts either — `Control`'s
|
|
11755
|
+
* `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
|
|
11756
|
+
*
|
|
11757
|
+
* One copy, imported by both consumers: two hand-maintained copies, one per
|
|
11758
|
+
* consumer, is how the deprecated `icon` path came to crash both (#663).
|
|
11759
|
+
*
|
|
11760
|
+
* Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
|
|
11761
|
+
* that file, and everything published there is public forever. `polymorphic.ts`
|
|
11762
|
+
* makes the same call for `isCustomElement`: an internal discriminator is not
|
|
11763
|
+
* something to support for the life of the package.
|
|
11764
|
+
*
|
|
11765
|
+
* It tests each member's VALUE, not merely its key, and only after everything
|
|
11766
|
+
* React owns is out of the way. `'icon' in value` was enough
|
|
11767
|
+
* to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
|
|
11768
|
+
* path] }` — whose `icon` is a path array rather than a class string. That object
|
|
11769
|
+
* is not a renderable node either, so the honest outcome is the one this slot has
|
|
11770
|
+
* always had for it: the node branch, and React's own "Objects are not valid as a
|
|
11771
|
+
* React child". Claiming it here instead rendered a blank container that still
|
|
11772
|
+
* reserved layout.
|
|
11773
|
+
*
|
|
11774
|
+
* A member added to `IconProps` needs a line here, and a missed one is a crash
|
|
11775
|
+
* rather than a fallthrough. That is the cost of a slot taking either a props
|
|
11776
|
+
* object or a node; keeping the guard in one place is what keeps the cost to a
|
|
11777
|
+
* single line.
|
|
11778
|
+
*/
|
|
11779
|
+
function isIconProps(value) {
|
|
11780
|
+
if (typeof value !== 'object' || value === null)
|
|
11781
|
+
return false;
|
|
11782
|
+
// Anything React owns. `isValidElement` covers elements only; a PORTAL is a
|
|
11783
|
+
// valid `ReactNode` that fails it and carries a `children` key, so the member
|
|
11784
|
+
// test below claimed it and `Control` spread it into `<Icon>` as props —
|
|
11785
|
+
// rendering the portal's child locally instead of in its container. Every
|
|
11786
|
+
// React internal carries `$$typeof`, so testing for it covers lazy, context
|
|
11787
|
+
// and whatever React adds next, rather than portals alone.
|
|
11788
|
+
if (React.isValidElement(value) || '$$typeof' in value)
|
|
11789
|
+
return false;
|
|
11790
|
+
const candidate = value;
|
|
11791
|
+
return (typeof candidate.name === 'string' ||
|
|
11792
|
+
typeof candidate.icon === 'string' ||
|
|
11793
|
+
'children' in candidate);
|
|
11794
|
+
}
|
|
11795
|
+
|
|
11796
|
+
// Whether the nearest enclosing `Control` shows its `isLoading` spinner. The
|
|
11797
|
+
// picker bases read it so their right-side launcher gives way to that
|
|
11798
|
+
// spinner, which sits at the same edge, whether the `Control` is the one a
|
|
11799
|
+
// convenience input renders or one the caller wrapped around the input.
|
|
11800
|
+
// Autocomplete reads it for its clear button, which sits there too.
|
|
11801
|
+
// Kept apart from `FormContext`, whose contexts only report whether a `Field`
|
|
11802
|
+
// or `Control` is present. Internal; not part of the public API.
|
|
11803
|
+
const ControlLoadingContext = React.createContext(false);
|
|
11804
|
+
/** Provider for the loading state, used internally by `Control`. */
|
|
11805
|
+
const ControlLoadingProvider = ControlLoadingContext.Provider;
|
|
11806
|
+
/** Whether the nearest enclosing `Control` is loading; `false` outside one. */
|
|
11807
|
+
const useControlLoading = () => React.useContext(ControlLoadingContext);
|
|
11808
|
+
|
|
11809
|
+
const allowedColors = [...validColors, 'inherit', 'current'];
|
|
11810
|
+
/**
|
|
11811
|
+
* The `Control` component is a Bulma-styled wrapper for form controls (`Input`, `Select`, `TextArea`, etc.), supporting icons (left/right), loading state, expansion, size, and Bulma helper props for layout and color.
|
|
11812
|
+
*
|
|
11813
|
+
* @function
|
|
11814
|
+
* @param {ControlProps} props - Props for the Control component.
|
|
11815
|
+
* @returns {JSX.Element} The rendered control container.
|
|
11816
|
+
* @see {@link https://bulma.io/documentation/form/general/#control | Bulma Control documentation}
|
|
11817
|
+
*
|
|
11818
|
+
* @example
|
|
11819
|
+
* // Control with left icon
|
|
11820
|
+
* <Control iconLeftName="envelope" iconLeftSize="small">
|
|
11821
|
+
* <input className="input" type="email" placeholder="Email" />
|
|
11822
|
+
* </Control>
|
|
11823
|
+
*/
|
|
11824
|
+
const Control = React.forwardRef(({ as = 'div', hasIconsLeft, hasIconsRight, isLoading, isExpanded, size, textColor, bgColor, iconLeft, iconRight, iconLeftName, iconLeftSize, iconRightName, iconRightSize, className, children, ...props }, ref) => {
|
|
11825
|
+
const Component = (as === 'p' ? 'p' : 'div');
|
|
11826
|
+
const { classPrefix } = useConfig();
|
|
11827
|
+
// Remove textColor/bgColor from props before spreading
|
|
11828
|
+
const { textColor: _ignoredTextColor, bgColor: _ignoredBgColor, ...restProps } = props;
|
|
11829
|
+
const safeTextColor = allowedColors.includes(textColor)
|
|
11830
|
+
? textColor
|
|
11831
|
+
: undefined;
|
|
11832
|
+
const safeBgColor = allowedColors.includes(bgColor)
|
|
11833
|
+
? bgColor
|
|
11834
|
+
: undefined;
|
|
11835
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11836
|
+
color: safeTextColor,
|
|
11837
|
+
backgroundColor: safeBgColor,
|
|
11838
|
+
...restProps,
|
|
11839
|
+
});
|
|
11840
|
+
// Prepare icon props for the shortcut. A truthiness test (not `!== undefined`)
|
|
11841
|
+
// so a falsy node from the idiomatic `iconLeft={cond && <Node/>}` / `iconLeft={maybe ?? null}`
|
|
11842
|
+
// pattern counts as "no icon" — otherwise `false`/`null` would reserve the icon column and
|
|
11843
|
+
// mount an empty `.icon` span. A falsy `iconLeft` also correctly falls through to the
|
|
11844
|
+
// `iconLeftName` shortcut, matching the pre-node behavior.
|
|
11845
|
+
const leftIconValue = iconLeft ||
|
|
11846
|
+
(iconLeftName
|
|
11847
|
+
? {
|
|
11848
|
+
name: iconLeftName,
|
|
11849
|
+
size: iconLeftSize,
|
|
11850
|
+
}
|
|
11851
|
+
: undefined);
|
|
11852
|
+
const rightIconValue = iconRight ||
|
|
11853
|
+
(iconRightName
|
|
11854
|
+
? {
|
|
11855
|
+
name: iconRightName,
|
|
11856
|
+
size: iconRightSize,
|
|
11857
|
+
}
|
|
11858
|
+
: undefined);
|
|
11859
|
+
const mainClass = usePrefixedClassNames('control', {
|
|
11860
|
+
'has-icons-left': hasIconsLeft || !!leftIconValue,
|
|
11861
|
+
'has-icons-right': hasIconsRight || !!rightIconValue,
|
|
11862
|
+
'is-loading': isLoading,
|
|
11863
|
+
'is-expanded': isExpanded,
|
|
11864
|
+
[`is-${size}`]: !!size,
|
|
11865
|
+
});
|
|
11866
|
+
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
11867
|
+
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
11868
|
+
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
|
|
11869
|
+
(isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
11870
|
+
(isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
11871
|
+
});
|
|
11872
|
+
Control.displayName = 'Control';
|
|
11873
|
+
|
|
11874
|
+
/**
|
|
11875
|
+
* FieldLabel component for rendering a Bulma field label. It renders the
|
|
11876
|
+
* label column of a horizontal `Field` (a `div` with the `field-label` class),
|
|
11877
|
+
* not a `<label>`, so text placed straight in it names nothing. Put a `<label>`
|
|
11878
|
+
* with the `label` class inside it and point its `htmlFor` at the control's
|
|
11879
|
+
* `id`, or give the horizontal `Field` a `label` prop, which renders this
|
|
11880
|
+
* column and its `<label>` for you. A `<label>` you put here names the
|
|
11881
|
+
* control through its `htmlFor` alone: a range `Slider`'s thumbs and an
|
|
11882
|
+
* `Autocomplete`'s, a `Combobox`'s or a `Taginput`'s list point
|
|
11883
|
+
* `aria-labelledby` at a `Field`'s label only when its `label` prop renders it
|
|
11884
|
+
* and it names the control. Where it would not name the control on its own, as in an inner `Field`,
|
|
11885
|
+
* pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
|
|
11886
|
+
*
|
|
11887
|
+
* @function
|
|
11888
|
+
* @param {FieldLabelProps} props - Props for the FieldLabel component.
|
|
11889
|
+
* @returns {JSX.Element} The rendered field label.
|
|
11890
|
+
*
|
|
11891
|
+
* @example
|
|
11892
|
+
* <FieldLabel size="normal">
|
|
11893
|
+
* <label className="label" htmlFor="name">Name</label>
|
|
11894
|
+
* </FieldLabel>
|
|
11895
|
+
*/
|
|
11896
|
+
const FieldLabel = ({ size, textColor, bgColor, className, children, ...props }) => {
|
|
11897
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11898
|
+
color: textColor,
|
|
11899
|
+
backgroundColor: bgColor,
|
|
11900
|
+
...props,
|
|
11901
|
+
});
|
|
11902
|
+
const mainClass = usePrefixedClassNames('field-label', {
|
|
11903
|
+
[`is-${size}`]: !!size,
|
|
11904
|
+
});
|
|
11905
|
+
const fieldLabelClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
11906
|
+
// Spread ...props and ...rest so custom props like data-testid are included
|
|
11907
|
+
return (jsxRuntime.jsx("div", { className: fieldLabelClass, ...props, ...rest, children: children }));
|
|
11908
|
+
};
|
|
11909
|
+
/**
|
|
11910
|
+
* FieldBody component for rendering Bulma field body.
|
|
11911
|
+
*
|
|
11912
|
+
* @function
|
|
11913
|
+
* @param {FieldBodyProps} props - Props for the FieldBody component.
|
|
11914
|
+
* @returns {JSX.Element} The rendered field body.
|
|
11915
|
+
*
|
|
11916
|
+
* @example
|
|
11917
|
+
* <FieldBody><input className="input" /></FieldBody>
|
|
11918
|
+
*/
|
|
11919
|
+
const FieldBody = ({ textColor, bgColor, className, children, ...props }) => {
|
|
11920
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11921
|
+
color: textColor,
|
|
11922
|
+
backgroundColor: bgColor,
|
|
11923
|
+
...props,
|
|
11924
|
+
});
|
|
11925
|
+
const mainClass = usePrefixedClassNames('field-body');
|
|
11926
|
+
const fieldBodyClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
11927
|
+
// Spread ...props and ...rest so custom props like data-testid are included
|
|
11928
|
+
return (jsxRuntime.jsx("div", { className: fieldBodyClass, ...props, ...rest, children: children }));
|
|
11929
|
+
};
|
|
11930
|
+
/**
|
|
11931
|
+
* The `Field` component is a Bulma-styled form field container.
|
|
11932
|
+
*
|
|
11933
|
+
* @function
|
|
11934
|
+
* @param {FieldProps} props - Props for the Field component.
|
|
11935
|
+
* @returns {JSX.Element} The rendered field container.
|
|
11936
|
+
* @see {@link https://bulma.io/documentation/form/general/#field | Bulma Field documentation}
|
|
11937
|
+
*
|
|
11938
|
+
* @example
|
|
11939
|
+
* // Labelled field — the label auto-associates with the composed base
|
|
11940
|
+
* <Field label="Email">
|
|
11941
|
+
* <Control>
|
|
11942
|
+
* <InputBase type="email" />
|
|
11943
|
+
* </Control>
|
|
11944
|
+
* </Field>
|
|
11945
|
+
*
|
|
11946
|
+
* @example
|
|
11947
|
+
* // Horizontal field
|
|
11948
|
+
* <Field horizontal label="Name">
|
|
11949
|
+
* <Control>
|
|
11950
|
+
* <InputBase />
|
|
11951
|
+
* </Control>
|
|
11952
|
+
* </Field>
|
|
11953
|
+
*/
|
|
11954
|
+
const FieldComponent = ({ horizontal, grouped, hasAddons, narrow, label, labelSize, labelProps, textColor, color: _fieldColor, bgColor, className, children, ...props }) => {
|
|
11955
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
11956
|
+
color: textColor,
|
|
11957
|
+
backgroundColor: bgColor,
|
|
11958
|
+
...props,
|
|
11959
|
+
});
|
|
11960
|
+
const mainClass = usePrefixedClassNames('field', {
|
|
11961
|
+
'is-horizontal': horizontal,
|
|
11962
|
+
'has-addons': !!hasAddons,
|
|
11963
|
+
'has-addons-centered': hasAddons === 'centered',
|
|
11964
|
+
'has-addons-right': hasAddons === 'right',
|
|
11965
|
+
'is-narrow': narrow,
|
|
11966
|
+
'is-grouped': grouped === true ||
|
|
11967
|
+
grouped === 'centered' ||
|
|
11968
|
+
grouped === 'right' ||
|
|
11969
|
+
grouped === 'multiline',
|
|
11970
|
+
'is-grouped-centered': grouped === 'centered',
|
|
11971
|
+
'is-grouped-right': grouped === 'right',
|
|
11972
|
+
'is-grouped-multiline': grouped === 'multiline',
|
|
11973
|
+
});
|
|
11974
|
+
const fieldClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
11975
|
+
// Default labelSize to 'normal' when horizontal for proper baseline alignment
|
|
11976
|
+
const effectiveLabelSize = labelSize ?? (horizontal ? 'normal' : undefined);
|
|
11977
|
+
const labelClass = usePrefixedClassNames('label');
|
|
11978
|
+
// Auto-associate the label with a single composed base control (#495): the
|
|
11979
|
+
// label points at a generated id shared via context, which a composed base
|
|
11980
|
+
// adopts when the user supplied no id of their own.
|
|
11981
|
+
// Presence semantics on htmlFor — even an explicit `htmlFor: undefined`
|
|
11982
|
+
// means the caller owns the association. Grouped/addons fields hold several
|
|
11983
|
+
// controls, so no single association is generated for them.
|
|
11984
|
+
const generatedId = React.useId();
|
|
11985
|
+
const userWiredLabel = !!labelProps && 'htmlFor' in labelProps;
|
|
11986
|
+
const targetId = label && !userWiredLabel && !grouped && !hasAddons
|
|
11987
|
+
? generatedId
|
|
11988
|
+
: undefined;
|
|
11989
|
+
// A group control (Radios, Checkboxes, Rate, DateRangeInput and its base)
|
|
11990
|
+
// cannot take that `for`, so it points `aria-labelledby` at the label
|
|
11991
|
+
// itself (#939).
|
|
11992
|
+
// The label gets an id whenever the association is on, unless the caller
|
|
11993
|
+
// gave it one.
|
|
11994
|
+
const labelId = labelProps?.id ?? (targetId ? `${targetId}-label` : undefined);
|
|
11995
|
+
// What the rendered label's `for` points at, generated or the caller's, and
|
|
11996
|
+
// the id it renders with, so a control can tell when a label wired by hand
|
|
11997
|
+
// names it and point at that label. An unlabeled Field passes on its
|
|
11998
|
+
// parent's. Unlike `targetId`, a label wired by hand passes on its own in
|
|
11999
|
+
// `grouped`/`hasAddons` Fields too, on purpose: those only stop the Field
|
|
12000
|
+
// picking one control out of several, and the caller's `for` has picked
|
|
12001
|
+
// one, so a range Slider or Autocomplete it names points at that label in
|
|
12002
|
+
// a row as well. A group reads the label element id below instead, which
|
|
12003
|
+
// stays off for a label wired by hand, since no group takes its `for`.
|
|
12004
|
+
const inheritedLabelFor = useFieldLabelFor();
|
|
12005
|
+
const labelForTarget = userWiredLabel ? labelProps.htmlFor : targetId;
|
|
12006
|
+
const ownLabelFor = React.useMemo(() => ({ htmlFor: labelForTarget, id: labelId }), [labelForTarget, labelId]);
|
|
12007
|
+
const labelFor = label ? ownLabelFor : inheritedLabelFor;
|
|
12008
|
+
// Whether the label keeps its generated `for` (#1004). Content that takes
|
|
12009
|
+
// the Field's id says so from a layout effect, and this Field's own layout
|
|
12010
|
+
// effect runs after its content's in the same commit, so by then every
|
|
12011
|
+
// report has landed. Until it runs, on the server and in the first render
|
|
12012
|
+
// while hydrating, the label writes the `for` as it always has. From then
|
|
12013
|
+
// on it keeps the `for` only while some content holds the id, and content
|
|
12014
|
+
// that mounts or changes later reports when it does. Counted in a ref, and
|
|
12015
|
+
// state is set only once settled, so content that takes the id renders
|
|
12016
|
+
// the Field just once.
|
|
12017
|
+
const takersRef = React.useRef(0);
|
|
12018
|
+
const settledRef = React.useRef(false);
|
|
12019
|
+
const [forTaken, setForTaken] = React.useState(true);
|
|
12020
|
+
const settle = React.useCallback(() => {
|
|
12021
|
+
if (settledRef.current)
|
|
12022
|
+
setForTaken(takersRef.current > 0);
|
|
12023
|
+
}, []);
|
|
12024
|
+
const reportLabelFor = React.useCallback(() => {
|
|
12025
|
+
takersRef.current += 1;
|
|
12026
|
+
settle();
|
|
12027
|
+
return () => {
|
|
12028
|
+
takersRef.current -= 1;
|
|
12029
|
+
settle();
|
|
12030
|
+
};
|
|
12031
|
+
}, [settle]);
|
|
12032
|
+
useClientLayoutEffect(() => {
|
|
12033
|
+
settledRef.current = true;
|
|
12034
|
+
if (targetId)
|
|
12035
|
+
settle();
|
|
12036
|
+
}, [targetId, settle]);
|
|
12037
|
+
const renderedTargetId = forTaken ? targetId : undefined;
|
|
12038
|
+
let renderedLabel = null;
|
|
12039
|
+
if (label) {
|
|
12040
|
+
if (horizontal) {
|
|
12041
|
+
renderedLabel = (jsxRuntime.jsx(FieldLabel, { size: effectiveLabelSize, children: jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: labelProps?.style, children: label }) }));
|
|
12042
|
+
}
|
|
12043
|
+
else {
|
|
12044
|
+
renderedLabel = (jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: { display: 'block', ...(labelProps?.style || {}) }, children: label }));
|
|
12045
|
+
}
|
|
12046
|
+
}
|
|
12047
|
+
// If horizontal, wrap children in FieldBody (unless the user already provided
|
|
12048
|
+
// a FieldBody — either as the single child, or as one element among siblings
|
|
12049
|
+
// like <Field.Label/> + <Field.Body/>).
|
|
12050
|
+
let content = children;
|
|
12051
|
+
if (horizontal) {
|
|
12052
|
+
const isFieldBody = (c) => React.isValidElement(c) &&
|
|
12053
|
+
// @ts-expect-error displayName isn't on the public type
|
|
12054
|
+
(c.type === FieldBody || c.type?.displayName === 'FieldBody');
|
|
12055
|
+
const isFieldLabel = (c) => React.isValidElement(c) &&
|
|
12056
|
+
// @ts-expect-error displayName isn't on the public type
|
|
12057
|
+
(c.type === FieldLabel || c.type?.displayName === 'FieldLabel');
|
|
12058
|
+
const childArray = React.Children.toArray(children);
|
|
12059
|
+
const userProvidedStructure = childArray.some(c => isFieldBody(c) || isFieldLabel(c));
|
|
12060
|
+
if (userProvidedStructure) {
|
|
12061
|
+
content = children;
|
|
12062
|
+
}
|
|
12063
|
+
else {
|
|
12064
|
+
content = jsxRuntime.jsx(FieldBody, { children: children });
|
|
12065
|
+
}
|
|
12066
|
+
}
|
|
12067
|
+
return (jsxRuntime.jsx(FieldProvider, { value: true, children: jsxRuntime.jsx(FieldLabelIdProvider, { value: targetId, children: jsxRuntime.jsx(FieldLabelElementIdProvider, { value: targetId ? labelId : undefined, children: jsxRuntime.jsx(FieldLabelForProvider, { value: labelFor, children: jsxRuntime.jsx(FieldLabelForReportProvider, { value: targetId ? reportLabelFor : undefined, children: jsxRuntime.jsxs("div", { className: fieldClass, ...rest, children: [renderedLabel, content] }) }) }) }) }) }));
|
|
12068
|
+
};
|
|
12069
|
+
FieldLabel.displayName = 'FieldLabel';
|
|
12070
|
+
FieldBody.displayName = 'FieldBody';
|
|
12071
|
+
const Field = withSubComponents(FieldComponent, {
|
|
12072
|
+
Label: FieldLabel,
|
|
12073
|
+
Body: FieldBody,
|
|
12074
|
+
Control,
|
|
12075
|
+
}, 'Field');
|
|
12076
|
+
|
|
12077
|
+
/**
|
|
12078
|
+
* Bulma Input component with full Bulma helper class support.
|
|
12079
|
+
*
|
|
12080
|
+
* @function
|
|
12081
|
+
* @param {InputBaseProps} props - Props for the Input component.
|
|
12082
|
+
* @returns {JSX.Element} The rendered input element.
|
|
12083
|
+
* @see {@link https://bulma.io/documentation/form/input/ | Bulma Input documentation}
|
|
12084
|
+
*/
|
|
12085
|
+
const InputBase = React.forwardRef(({ color, size, isRounded, isStatic, isHovered, isFocused, isLoading, className, disabled, readOnly, ...props }, ref) => {
|
|
12086
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
12087
|
+
color,
|
|
12088
|
+
...props,
|
|
12089
|
+
});
|
|
12090
|
+
// A labeled Field's generated id when no user id arrived (rest.id may be
|
|
12091
|
+
// an undefined own key), reported to that Field (#1004).
|
|
12092
|
+
const id = useFieldLabelTarget(rest.id);
|
|
12093
|
+
const mainClass = usePrefixedClassNames('input', {
|
|
12094
|
+
[`is-${color}`]: !!color,
|
|
12095
|
+
[`is-${size}`]: !!size,
|
|
12096
|
+
'is-rounded': isRounded,
|
|
12097
|
+
'is-static': isStatic,
|
|
12098
|
+
'is-hovered': isHovered,
|
|
12099
|
+
'is-focused': isFocused,
|
|
12100
|
+
'is-loading': isLoading,
|
|
12101
|
+
});
|
|
12102
|
+
const inputClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
12103
|
+
return (jsxRuntime.jsx("input", { ref: ref, className: inputClass, disabled: disabled, readOnly: readOnly, ...rest,
|
|
12104
|
+
// After the spread, which may carry an undefined id key.
|
|
12105
|
+
id: id }));
|
|
12106
|
+
});
|
|
12107
|
+
InputBase.displayName = 'InputBase';
|
|
12108
|
+
|
|
12109
|
+
/**
|
|
12110
|
+
* Associates the convenience `label` prop with its control (#368): generates
|
|
12111
|
+
* an id for the control and returns labelProps carrying a matching `htmlFor`.
|
|
12112
|
+
* A user-supplied `id` is used as the target instead of the generated one, and
|
|
12113
|
+
* an explicit `htmlFor` key in labelProps — even set to `undefined` — disables
|
|
12114
|
+
* generation entirely: the user has taken over the association (#495 presence
|
|
12115
|
+
* semantics). When the control renders no label of its own, it adopts the id
|
|
12116
|
+
* a labeled Field around it offers, the way the bases do, so that Field's
|
|
12117
|
+
* label names it (#939); a user `id` still wins there. It tells that Field
|
|
12118
|
+
* when the control took its id, since the Field's label drops a `for`
|
|
12119
|
+
* nothing took (#1004). With `needsLabelId` it also returns the id of
|
|
12120
|
+
* whichever label names the control (#981).
|
|
12121
|
+
* Internal; not part of the public API.
|
|
12122
|
+
*/
|
|
12123
|
+
function useAutoLabelId({ label, id, labelProps, rendersLabel, hasInput = true, needsLabelId = false, }) {
|
|
12124
|
+
// Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
|
|
12125
|
+
const generatedId = React.useId();
|
|
12126
|
+
// Read outside the control's own Field, so it is set only when an outer
|
|
12127
|
+
// Field holds the control, which then renders no label of its own.
|
|
12128
|
+
const fieldLabelId = useFieldLabelId();
|
|
12129
|
+
// What an outer Field's label points at, even when wired by hand, and the
|
|
12130
|
+
// id that label renders with, read together so the id is that label's.
|
|
12131
|
+
// Set in `grouped`/`hasAddons` Fields too when the label is wired by hand,
|
|
12132
|
+
// since its `for` names this one control whatever the layout (see Field).
|
|
12133
|
+
const fieldLabelFor = useFieldLabelFor();
|
|
12134
|
+
// Truthiness mirrors Field's own `if (label)` render gate.
|
|
12135
|
+
const active = !!label && rendersLabel;
|
|
12136
|
+
// Presence, not truthiness: `htmlFor: undefined` is an explicit opt-out and
|
|
12137
|
+
// must not leave an orphan generated id on the control.
|
|
12138
|
+
const userWired = !!labelProps && 'htmlFor' in labelProps;
|
|
12139
|
+
const adopted = !active && hasInput ? fieldLabelId : undefined;
|
|
12140
|
+
const controlId = id ?? (active && !userWired ? generatedId : adopted);
|
|
12141
|
+
// Tells a labeled Field around the control when its label's `for` names
|
|
12142
|
+
// the control, since it drops a `for` nothing took (#1004).
|
|
12143
|
+
useReportFieldLabelFor(fieldLabelId !== undefined && controlId === fieldLabelId);
|
|
12144
|
+
const ownLabelProps = { htmlFor: controlId, ...labelProps };
|
|
12145
|
+
const labelTarget = active ? ownLabelProps.htmlFor : fieldLabelFor.htmlFor;
|
|
12146
|
+
const labelled = !!controlId && labelTarget === controlId;
|
|
12147
|
+
// Only a label that names the control gets an id to point at, with the
|
|
12148
|
+
// suffix Field gives its own, from an id that is already unique.
|
|
12149
|
+
const ownLabelId = needsLabelId && active && labelled
|
|
12150
|
+
? (labelProps?.id ?? `${generatedId}-label`)
|
|
12151
|
+
: undefined;
|
|
12152
|
+
// Inactive with a label still means an own Field may render it (pickers'
|
|
12153
|
+
// inline mode). The explicit `htmlFor: undefined` tells Field the
|
|
12154
|
+
// association is owned here, so it must not generate one that would
|
|
12155
|
+
// dangle (#495 presence semantics).
|
|
12156
|
+
const fieldLabelProps = active
|
|
12157
|
+
? ownLabelId
|
|
12158
|
+
? { ...ownLabelProps, id: ownLabelId }
|
|
12159
|
+
: ownLabelProps
|
|
12160
|
+
: label
|
|
12161
|
+
? { htmlFor: undefined, ...labelProps }
|
|
12162
|
+
: labelProps;
|
|
12163
|
+
const labelId = needsLabelId && labelled
|
|
12164
|
+
? active
|
|
12165
|
+
? ownLabelId
|
|
12166
|
+
: fieldLabelFor.id
|
|
12167
|
+
: undefined;
|
|
12168
|
+
return { controlId, fieldLabelProps, labelled, labelId };
|
|
12169
|
+
}
|
|
12170
|
+
/**
|
|
12171
|
+
* Group-input counterpart of {@link useAutoLabelId} (#494): a group of
|
|
12172
|
+
* controls cannot take a single `htmlFor`, so instead the rendered `<label>`
|
|
12173
|
+
* gets a generated id and the group container points at it with
|
|
12174
|
+
* `aria-labelledby`. A user-supplied `labelProps.id` is used as the target
|
|
12175
|
+
* instead of generating one. Any caller `htmlFor` is stripped — a group label
|
|
12176
|
+
* names the group, never a single control — so the merged labelProps always
|
|
12177
|
+
* carry an explicit `htmlFor: undefined`. When the group renders no label of
|
|
12178
|
+
* its own, it points at the label of a labeled Field around it instead
|
|
12179
|
+
* (#939). A caller who named the group with `aria-label` or
|
|
12180
|
+
* `aria-labelledby` gets no `aria-labelledby` from either label, so their
|
|
12181
|
+
* name wins wherever the label sits.
|
|
12182
|
+
* Internal; not part of the public API.
|
|
12183
|
+
*/
|
|
12184
|
+
function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
|
|
12185
|
+
// Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
|
|
12186
|
+
const generatedId = React.useId();
|
|
12187
|
+
// Set only when an outer Field holds the group (see useAutoLabelId).
|
|
12188
|
+
const fieldLabelElementId = useFieldLabelElementId();
|
|
12189
|
+
const active = !!label && rendersLabel;
|
|
12190
|
+
const labelId = labelProps?.id ?? (active ? generatedId : undefined);
|
|
12191
|
+
const fieldLabelProps = active
|
|
12192
|
+
? { ...labelProps, id: labelId, htmlFor: undefined }
|
|
12193
|
+
: labelProps;
|
|
12194
|
+
const aria = callerProps;
|
|
12195
|
+
// Presence for aria-labelledby: the caller's key replaces the attribute
|
|
12196
|
+
// through the spread whatever its value, and pointing it at a label would
|
|
12197
|
+
// only take a Rate's fallback name away. Checked before either label, so
|
|
12198
|
+
// the caller's name wins over the group's own `label` as it does over a
|
|
12199
|
+
// Field's, the way an aria-label beats a `<label for>` on a single input.
|
|
12200
|
+
const callerNamed = !!aria['aria-label'] || 'aria-labelledby' in aria;
|
|
12201
|
+
const ariaLabelledBy = callerNamed
|
|
12202
|
+
? undefined
|
|
12203
|
+
: active
|
|
12204
|
+
? labelId
|
|
12205
|
+
: fieldLabelElementId;
|
|
12206
|
+
return { ariaLabelledBy, fieldLabelProps };
|
|
12207
|
+
}
|
|
12208
|
+
|
|
12209
|
+
/**
|
|
12210
|
+
* The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
|
|
12211
|
+
*
|
|
12212
|
+
* @function
|
|
12213
|
+
* @param {InputProps} props - Props for Input.
|
|
12214
|
+
* @returns {JSX.Element} The composed field element.
|
|
12215
|
+
*
|
|
12216
|
+
* @example
|
|
12217
|
+
* <Input label="Username" placeholder="Enter username" iconLeftName="user" />
|
|
12218
|
+
*
|
|
12219
|
+
* @example
|
|
12220
|
+
* <Input
|
|
12221
|
+
* label="Email"
|
|
12222
|
+
* type="email"
|
|
12223
|
+
* message="Please enter a valid email"
|
|
12224
|
+
* messageColor="danger"
|
|
12225
|
+
* color="danger"
|
|
12226
|
+
* />
|
|
12227
|
+
*/
|
|
12228
|
+
const Input = React.forwardRef(({
|
|
12229
|
+
// Field props
|
|
12230
|
+
label, labelSize, labelProps, horizontal,
|
|
12231
|
+
// Control props
|
|
12232
|
+
iconLeft, iconRight, iconLeftName, iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize,
|
|
12233
|
+
// Message props
|
|
12234
|
+
message, messageColor,
|
|
12235
|
+
// Container class overrides
|
|
12236
|
+
fieldClassName, controlClassName,
|
|
12237
|
+
// Everything else goes to Input
|
|
12238
|
+
...inputProps }, ref) => {
|
|
12239
|
+
const insideField = useInsideField();
|
|
12240
|
+
const insideControl = useInsideControl();
|
|
12241
|
+
const ownField = rendersOwnField('Input', {
|
|
12242
|
+
insideField,
|
|
12243
|
+
insideControl,
|
|
12244
|
+
label,
|
|
12245
|
+
message,
|
|
12246
|
+
horizontal,
|
|
12247
|
+
fieldClassName,
|
|
12248
|
+
});
|
|
12249
|
+
const ownControl = rendersOwnControl('Input', {
|
|
12250
|
+
insideControl,
|
|
12251
|
+
isLoading,
|
|
12252
|
+
iconLeft,
|
|
12253
|
+
iconLeftName,
|
|
12254
|
+
iconLeftSize,
|
|
12255
|
+
iconRight,
|
|
12256
|
+
iconRightName,
|
|
12257
|
+
iconRightSize,
|
|
12258
|
+
hasIconsLeft,
|
|
12259
|
+
hasIconsRight,
|
|
12260
|
+
isExpanded,
|
|
12261
|
+
controlSize,
|
|
12262
|
+
controlClassName,
|
|
12263
|
+
});
|
|
12264
|
+
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
12265
|
+
label,
|
|
12266
|
+
id: inputProps.id,
|
|
12267
|
+
labelProps,
|
|
12268
|
+
rendersLabel: ownField,
|
|
12269
|
+
});
|
|
12270
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
12271
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
12272
|
+
});
|
|
12273
|
+
let content = jsxRuntime.jsx(InputBase, { ref: ref, ...inputProps, id: controlId });
|
|
12274
|
+
if (ownControl) {
|
|
12275
|
+
content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
12276
|
+
}
|
|
12277
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
12278
|
+
if (ownField) {
|
|
12279
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
12280
|
+
}
|
|
12281
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
12282
|
+
});
|
|
12283
|
+
Input.displayName = 'Input';
|
|
12284
|
+
|
|
12285
|
+
/** What names a Command that's given no `label`. */
|
|
12286
|
+
const defaultLabel = 'Commands';
|
|
12287
|
+
/** What a Command given `actions` shows when nothing matches, unless it's given `empty`. */
|
|
12288
|
+
const defaultEmpty = 'No results';
|
|
12289
|
+
// What the status region announces: the `Command.Empty` content, as the one
|
|
12290
|
+
// item it's handed while that content is on screen because of the user's
|
|
12291
|
+
// typing.
|
|
12292
|
+
const typedNoMatches$2 = [{ id: 'empty' }];
|
|
12293
|
+
const nothingToAnnounce$2 = [];
|
|
12294
|
+
/** Whether a slot was given something to show. */
|
|
12295
|
+
const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
|
|
12296
|
+
const noop$1 = () => { };
|
|
12297
|
+
const registerNothing$1 = () => noop$1;
|
|
12298
|
+
// What a part rendered outside a Command reads: nothing to report to, so it
|
|
12299
|
+
// renders its markup and does nothing.
|
|
12300
|
+
const standalone$1 = {
|
|
12301
|
+
engine: {
|
|
12302
|
+
open: false,
|
|
12303
|
+
expanded: false,
|
|
12304
|
+
highlighted: null,
|
|
12305
|
+
typed: false,
|
|
12306
|
+
listboxId: '',
|
|
12307
|
+
itemId: () => '',
|
|
12308
|
+
rootRef: { current: null },
|
|
12309
|
+
inputRef: { current: null },
|
|
12310
|
+
listboxRef: { current: null },
|
|
12311
|
+
registerPopup: registerNothing$1,
|
|
12312
|
+
registerItem: registerNothing$1,
|
|
12313
|
+
onKeyDown: noop$1,
|
|
12314
|
+
onChange: noop$1,
|
|
12315
|
+
onClick: noop$1,
|
|
12316
|
+
onBlur: noop$1,
|
|
12317
|
+
hover: noop$1,
|
|
12318
|
+
pick: noop$1,
|
|
12319
|
+
keepFocus: noop$1,
|
|
12320
|
+
markInside: noop$1,
|
|
12321
|
+
},
|
|
12322
|
+
label: defaultLabel,
|
|
12323
|
+
query: '',
|
|
12324
|
+
onFieldChange: noop$1,
|
|
12325
|
+
reportLoading: noop$1,
|
|
12326
|
+
reportEmpty: registerNothing$1,
|
|
12327
|
+
registerHandler: registerNothing$1,
|
|
12328
|
+
};
|
|
12329
|
+
const CommandContext = React.createContext(null);
|
|
12330
|
+
const useCommand = () => React.useContext(CommandContext) ?? standalone$1;
|
|
12331
|
+
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
12332
|
+
function setRefs$1(node, own, forwarded) {
|
|
12333
|
+
own.current = node;
|
|
12334
|
+
if (typeof forwarded === 'function')
|
|
12335
|
+
forwarded(node);
|
|
12336
|
+
else if (forwarded)
|
|
12337
|
+
forwarded.current = node;
|
|
12338
|
+
}
|
|
12339
|
+
/**
|
|
12340
|
+
* The default filter: the actions whose label, keywords or description hold
|
|
12341
|
+
* the search, ignoring case and the spaces around it, label matches first and
|
|
12342
|
+
* the order otherwise kept. An empty search keeps them all.
|
|
12343
|
+
*/
|
|
12344
|
+
const filterActions = (query, actions) => {
|
|
12345
|
+
const term = query.trim().toLowerCase();
|
|
12346
|
+
if (term === '')
|
|
12347
|
+
return actions;
|
|
12348
|
+
const byLabel = [];
|
|
12349
|
+
const byOther = [];
|
|
12350
|
+
for (const action of actions) {
|
|
12351
|
+
if (action.label.toLowerCase().includes(term)) {
|
|
12352
|
+
byLabel.push(action);
|
|
12353
|
+
}
|
|
12354
|
+
else if (action.description?.toLowerCase().includes(term) ||
|
|
12355
|
+
action.keywords?.some(keyword => keyword.toLowerCase().includes(term))) {
|
|
12356
|
+
byOther.push(action);
|
|
12357
|
+
}
|
|
12358
|
+
}
|
|
12359
|
+
return [...byLabel, ...byOther];
|
|
12360
|
+
};
|
|
12361
|
+
/** The search icon at the start of the field. */
|
|
12362
|
+
const SearchIcon = () => (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", width: "1em", height: "1em", focusable: "false", children: [jsxRuntime.jsx("circle", { cx: "7", cy: "7", r: "4.75" }), jsxRuntime.jsx("path", { d: "M10.5 10.5L14 14" })] }));
|
|
12363
|
+
/**
|
|
12364
|
+
* The Command built from parts: the field, the list, the keys and the run,
|
|
12365
|
+
* with the items and their filtering left to the app.
|
|
12366
|
+
*/
|
|
12367
|
+
const CommandParts = React.forwardRef(({ children, label, query: controlledQuery, defaultQuery = '', onQueryChange, onSelect, className, ...props }, ref) => {
|
|
12368
|
+
const [ownQuery, setOwnQuery] = React.useState(defaultQuery);
|
|
12369
|
+
const query = controlledQuery ?? ownQuery;
|
|
12370
|
+
// Each item's `onSelect`, by value, read when it runs.
|
|
12371
|
+
const handlersRef = React.useRef(new Map());
|
|
12372
|
+
const registerHandler = React.useCallback((value, handler) => {
|
|
12373
|
+
const handlers = handlersRef.current;
|
|
12374
|
+
handlers.set(value, handler);
|
|
12375
|
+
return () => {
|
|
12376
|
+
if (handlers.get(value) === handler)
|
|
12377
|
+
handlers.delete(value);
|
|
12378
|
+
};
|
|
12379
|
+
}, []);
|
|
12380
|
+
const run = (value) => {
|
|
12381
|
+
handlersRef.current.get(value)?.current?.();
|
|
12382
|
+
onSelect?.(value);
|
|
12383
|
+
};
|
|
12384
|
+
// Inline: the list is always on screen, nothing opens or closes it, and
|
|
12385
|
+
// Escape goes on to what's around it. `keepFirst` keeps the highlight on
|
|
12386
|
+
// the top match, so Enter runs it.
|
|
12387
|
+
const engine = useComboboxEngine({
|
|
12388
|
+
defaultOpen: true,
|
|
12389
|
+
onSelect: run,
|
|
12390
|
+
keepFirst: true,
|
|
12391
|
+
multiple: false,
|
|
12392
|
+
inline: true,
|
|
12393
|
+
deferred: false,
|
|
12394
|
+
});
|
|
12395
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
12396
|
+
const rootClass = usePrefixedClassNames('command');
|
|
12397
|
+
// A polite status region, in the page from the start so it's there
|
|
12398
|
+
// before anything is written into it, announces `Command.Empty` when the
|
|
12399
|
+
// user's typing leaves it on screen: once each time that happens, not
|
|
12400
|
+
// again on each keystroke while it stays. An empty search isn't a search,
|
|
12401
|
+
// so it stays quiet then, and while the field's `isLoading` says results
|
|
12402
|
+
// are on their way, it waits.
|
|
12403
|
+
const [inputLoading, setInputLoading] = React.useState(false);
|
|
12404
|
+
const emptyRef = React.useRef(null);
|
|
12405
|
+
const [emptyShown, setEmptyShown] = React.useState(false);
|
|
12406
|
+
const announced = engine.typed && query !== '' && emptyShown && !inputLoading
|
|
12407
|
+
? typedNoMatches$2
|
|
12408
|
+
: nothingToAnnounce$2;
|
|
12409
|
+
// The region reads the text on screen, whatever the message renders.
|
|
12410
|
+
const describeEmpty = React.useCallback(() => {
|
|
12411
|
+
const node = emptyRef.current;
|
|
12412
|
+
/* istanbul ignore if: it's only announced while it's on screen */
|
|
12413
|
+
if (!node)
|
|
12414
|
+
return null;
|
|
12415
|
+
return spokenText(node);
|
|
12416
|
+
}, []);
|
|
12417
|
+
const reportEmpty = React.useCallback((element) => {
|
|
12418
|
+
emptyRef.current = element;
|
|
12419
|
+
setEmptyShown(true);
|
|
12420
|
+
return () => {
|
|
12421
|
+
emptyRef.current = null;
|
|
12422
|
+
setEmptyShown(false);
|
|
12423
|
+
};
|
|
12424
|
+
}, []);
|
|
12425
|
+
const onFieldChange = (value) => {
|
|
12426
|
+
if (controlledQuery === undefined)
|
|
12427
|
+
setOwnQuery(value);
|
|
12428
|
+
onQueryChange?.(value);
|
|
12429
|
+
engine.onChange();
|
|
12430
|
+
};
|
|
12431
|
+
// A new value each render: the engine's state and handlers are, and the
|
|
12432
|
+
// parts read the highlight from it.
|
|
12433
|
+
const context = {
|
|
12434
|
+
engine,
|
|
12435
|
+
label: label || defaultLabel,
|
|
12436
|
+
query,
|
|
12437
|
+
onFieldChange,
|
|
12438
|
+
reportLoading: setInputLoading,
|
|
12439
|
+
reportEmpty,
|
|
12440
|
+
registerHandler,
|
|
12441
|
+
};
|
|
12442
|
+
const { rootRef } = engine;
|
|
12443
|
+
const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
|
|
12444
|
+
return (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(CommandContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
|
|
12445
|
+
});
|
|
12446
|
+
CommandParts.displayName = 'CommandParts';
|
|
12447
|
+
/**
|
|
12448
|
+
* The search field, with the combobox role and the keys, rendered as an `Input` with a search icon.
|
|
12449
|
+
*
|
|
12450
|
+
* @function
|
|
12451
|
+
* @param {CommandInputProps} props - Props for the Command.Input component.
|
|
12452
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
|
|
12453
|
+
* @returns {JSX.Element} The field in its Control.
|
|
12454
|
+
*/
|
|
12455
|
+
const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKeyDown, onChange, onClick, onBlur, 'aria-autocomplete': ariaAutocomplete, ...props }, ref) => {
|
|
12456
|
+
const context = useCommand();
|
|
12457
|
+
const { engine, reportLoading } = context;
|
|
12458
|
+
useClientLayoutEffect(() => {
|
|
12459
|
+
reportLoading(!!isLoading);
|
|
12460
|
+
return () => reportLoading(false);
|
|
12461
|
+
}, [reportLoading, isLoading]);
|
|
12462
|
+
const { inputRef } = engine;
|
|
12463
|
+
const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
|
|
12464
|
+
const { expanded, highlighted } = engine;
|
|
12465
|
+
// A name the caller gave the field wins over the Command's `label`.
|
|
12466
|
+
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
12467
|
+
return (jsxRuntime.jsx(Input, { autoCorrect: "off", spellCheck: false, ...props, ref: inputRefs, value: context.query, iconLeft: iconLeft === undefined && !iconLeftName ? jsxRuntime.jsx(SearchIcon, {}) : iconLeft, iconLeftName: iconLeftName, isLoading: isLoading, "aria-label": callerNamed ? props['aria-label'] : context.label, role: "combobox", "aria-autocomplete": ariaAutocomplete ?? 'list', "aria-haspopup": "listbox", "aria-expanded": expanded, "aria-controls": expanded ? engine.listboxId : undefined, "aria-activedescendant": expanded && highlighted !== null
|
|
12468
|
+
? engine.itemId(highlighted)
|
|
12469
|
+
: undefined, autoComplete: "off", onKeyDown: event => {
|
|
12470
|
+
onKeyDown?.(event);
|
|
12471
|
+
engine.onKeyDown(event);
|
|
12472
|
+
}, onChange: event => {
|
|
12473
|
+
onChange?.(event);
|
|
12474
|
+
context.onFieldChange(event.target.value);
|
|
12475
|
+
}, onClick: event => {
|
|
12476
|
+
onClick?.(event);
|
|
12477
|
+
if (!event.defaultPrevented)
|
|
12478
|
+
engine.onClick();
|
|
12479
|
+
}, onBlur: event => {
|
|
12480
|
+
onBlur?.(event);
|
|
12481
|
+
engine.onBlur(event);
|
|
12482
|
+
} }));
|
|
12483
|
+
});
|
|
12484
|
+
CommandInput.displayName = 'CommandInput';
|
|
12485
|
+
/**
|
|
12486
|
+
* The list of items, a `listbox` named by the Command's `label`.
|
|
12487
|
+
*
|
|
12488
|
+
* @function
|
|
12489
|
+
* @param {CommandListProps} props - Props for the Command.List component.
|
|
12490
|
+
* @returns {JSX.Element} The listbox.
|
|
12491
|
+
*/
|
|
12492
|
+
const CommandList = ({ children, className, onMouseDown, ...props }) => {
|
|
12493
|
+
const { engine, label } = useCommand();
|
|
12494
|
+
const { listboxRef, listboxId, keepFocus } = engine;
|
|
12495
|
+
const listClass = usePrefixedClassNames('command-list');
|
|
12496
|
+
// A name the caller gave the list wins over the Command's.
|
|
12497
|
+
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
12498
|
+
return (jsxRuntime.jsx("div", { className: classNames(listClass, className), ...props, ref: listboxRef, id: listboxId || undefined, role: "listbox", "aria-label": callerNamed ? props['aria-label'] : label, onMouseDown: event => {
|
|
12499
|
+
onMouseDown?.(event);
|
|
12500
|
+
keepFocus(event);
|
|
12501
|
+
}, children: children }));
|
|
12502
|
+
};
|
|
12503
|
+
CommandList.displayName = 'CommandList';
|
|
12504
|
+
/**
|
|
12505
|
+
* Items under a heading, a `group` named by it.
|
|
12506
|
+
*
|
|
12507
|
+
* @function
|
|
12508
|
+
* @param {CommandGroupProps} props - Props for the Command.Group component.
|
|
12509
|
+
* @returns {JSX.Element} The group and its heading.
|
|
12510
|
+
*/
|
|
12511
|
+
const CommandGroup = ({ label, children, className, ...props }) => {
|
|
12512
|
+
const labelId = React.useId();
|
|
12513
|
+
const groupClass = usePrefixedClassNames('command-group');
|
|
12514
|
+
const labelClass = usePrefixedClassNames('command-group-label');
|
|
12515
|
+
// A name the caller gave the group wins over its heading's, as on the list.
|
|
12516
|
+
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
12517
|
+
return (jsxRuntime.jsxs("div", { className: classNames(groupClass, className), ...props, role: "group", "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, children: [jsxRuntime.jsx("div", { className: labelClass, id: labelId, children: label }), children] }));
|
|
12518
|
+
};
|
|
12519
|
+
CommandGroup.displayName = 'CommandGroup';
|
|
12520
|
+
/**
|
|
12521
|
+
* One thing to do in the list, named by its label and described by its description and hint.
|
|
12522
|
+
*
|
|
12523
|
+
* @function
|
|
12524
|
+
* @param {CommandItemProps} props - Props for the Command.Item component.
|
|
12525
|
+
* @returns {JSX.Element} The item.
|
|
12526
|
+
*/
|
|
12527
|
+
const CommandItem = ({ value, disabled = false, onSelect, icon, description, hint, children, className, onClick, onPointerMove, ...props }) => {
|
|
12528
|
+
const { engine, registerHandler } = useCommand();
|
|
12529
|
+
const itemRef = React.useRef(null);
|
|
12530
|
+
const { registerItem } = engine;
|
|
12531
|
+
useClientLayoutEffect(() => registerItem(itemRef.current, {
|
|
12532
|
+
value,
|
|
12533
|
+
disabled,
|
|
12534
|
+
checked: false,
|
|
12535
|
+
}), [registerItem, value, disabled]);
|
|
12536
|
+
const onSelectRef = useLatestRef(onSelect);
|
|
12537
|
+
useClientLayoutEffect(() => registerHandler(value, onSelectRef), [registerHandler, value, onSelectRef]);
|
|
12538
|
+
const highlighted = engine.highlighted === value;
|
|
12539
|
+
const itemClass = usePrefixedClassNames('command-item', {
|
|
12540
|
+
'is-active': highlighted,
|
|
12541
|
+
'is-disabled': disabled,
|
|
12542
|
+
});
|
|
12543
|
+
const iconClass = usePrefixedClassNames('command-item-icon');
|
|
12544
|
+
const bodyClass = usePrefixedClassNames('command-item-body');
|
|
12545
|
+
const labelClass = usePrefixedClassNames('command-item-label');
|
|
12546
|
+
const descriptionClass = usePrefixedClassNames('command-item-description');
|
|
12547
|
+
const hintClass = usePrefixedClassNames('command-item-hint');
|
|
12548
|
+
// Named by its label alone, so a screen reader says what it does before
|
|
12549
|
+
// the rest, and described by the description and the hint.
|
|
12550
|
+
const baseId = React.useId();
|
|
12551
|
+
const labelId = `${baseId}label`;
|
|
12552
|
+
const descriptionId = isShown(description)
|
|
12553
|
+
? `${baseId}description`
|
|
12554
|
+
: undefined;
|
|
12555
|
+
const hintId = isShown(hint) ? `${baseId}hint` : undefined;
|
|
12556
|
+
const describedBy = [descriptionId, hintId].filter(Boolean).join(' ') || undefined;
|
|
12557
|
+
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
12558
|
+
return (jsxRuntime.jsxs("div", { className: classNames(itemClass, className), "aria-describedby": describedBy, ...props, ref: itemRef, id: engine.itemId(value) || undefined, role: "option", "aria-selected": disabled ? undefined : highlighted || undefined, "aria-disabled": disabled || undefined, "aria-labelledby": callerNamed ? props['aria-labelledby'] : labelId, onClick: event => {
|
|
12559
|
+
onClick?.(event);
|
|
12560
|
+
if (!disabled && !event.defaultPrevented)
|
|
12561
|
+
engine.pick(value);
|
|
12562
|
+
}, onPointerMove: event => {
|
|
12563
|
+
onPointerMove?.(event);
|
|
12564
|
+
if (!disabled && !highlighted)
|
|
12565
|
+
engine.hover(value);
|
|
12566
|
+
}, children: [isShown(icon) && (jsxRuntime.jsx("span", { className: iconClass, "aria-hidden": "true", children: icon })), jsxRuntime.jsxs("span", { className: bodyClass, children: [jsxRuntime.jsx("span", { className: labelClass, id: labelId, children: children }), descriptionId && (jsxRuntime.jsx("span", { className: descriptionClass, id: descriptionId, children: description }))] }), hintId && (jsxRuntime.jsx("span", { className: hintClass, id: hintId, children: hint }))] }));
|
|
12567
|
+
};
|
|
12568
|
+
CommandItem.displayName = 'CommandItem';
|
|
12569
|
+
/**
|
|
12570
|
+
* The message shown in place of the list when nothing matches, announced when typing brings it on screen.
|
|
12571
|
+
*
|
|
12572
|
+
* @function
|
|
12573
|
+
* @param {CommandEmptyProps} props - Props for the Command.Empty component.
|
|
12574
|
+
* @returns {JSX.Element} The message.
|
|
12575
|
+
*/
|
|
12576
|
+
const CommandEmpty = ({ children, className, onMouseDown, ...props }) => {
|
|
12577
|
+
const { engine, reportEmpty } = useCommand();
|
|
12578
|
+
const emptyRef = React.useRef(null);
|
|
12579
|
+
useClientLayoutEffect(() => reportEmpty(emptyRef.current), [reportEmpty]);
|
|
12580
|
+
const emptyClass = usePrefixedClassNames('command-empty');
|
|
12581
|
+
return (jsxRuntime.jsx("div", { className: classNames(emptyClass, className), ...props, ref: emptyRef, onMouseDown: event => {
|
|
12582
|
+
onMouseDown?.(event);
|
|
12583
|
+
engine.keepFocus(event);
|
|
12584
|
+
}, children: children }));
|
|
12585
|
+
};
|
|
12586
|
+
CommandEmpty.displayName = 'CommandEmpty';
|
|
12587
|
+
/**
|
|
12588
|
+
* The field, and the actions that match the search, grouped, or the empty
|
|
12589
|
+
* message. The search is the parts root's, read from its context.
|
|
12590
|
+
*/
|
|
12591
|
+
const CommandActionsContent = ({ actions, filter = filterActions, placeholder, empty, }) => {
|
|
12592
|
+
const { query } = useCommand();
|
|
12593
|
+
const shown = filter(query, actions);
|
|
12594
|
+
// Each group where the matches first name it, and the actions with none
|
|
12595
|
+
// together where the first of them is, so the best match stays on top.
|
|
12596
|
+
const sections = new Map();
|
|
12597
|
+
for (const action of shown) {
|
|
12598
|
+
const group = action.group ?? '';
|
|
12599
|
+
sections.set(group, [...(sections.get(group) ?? []), action]);
|
|
12600
|
+
}
|
|
12601
|
+
const renderItem = (action) => (jsxRuntime.jsx(CommandItem, { value: action.id, disabled: action.disabled, icon: action.icon, description: action.description, hint: action.hint, onSelect: action.onSelect, children: action.label }, action.id));
|
|
12602
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(CommandInput, { placeholder: placeholder }), shown.length > 0 ? (jsxRuntime.jsx(CommandList, { children: Array.from(sections, ([group, members]) => group ? (jsxRuntime.jsx(CommandGroup, { label: group, children: members.map(renderItem) }, group)) : (members.map(renderItem))) })) : isShown(empty) ? (jsxRuntime.jsx(CommandEmpty, { children: empty })) : null] }));
|
|
12603
|
+
};
|
|
12604
|
+
/**
|
|
12605
|
+
* The Command given `actions`. It renders the parts itself and filters the
|
|
12606
|
+
* actions as the user types, so a run goes through the same parts, keys and
|
|
12607
|
+
* ARIA as one built by hand.
|
|
12608
|
+
*/
|
|
12609
|
+
const CommandFromActions = React.forwardRef(({ actions, filter, placeholder, empty = defaultEmpty, children, ...rest }, ref) => {
|
|
12610
|
+
if (flattenChildren(children).length > 0) {
|
|
12611
|
+
warnOnce('Command:actions-children', '[bestax-bulma] <Command actions> renders its own field and items, ' +
|
|
12612
|
+
'so its children are ignored. Give it actions or build it from ' +
|
|
12613
|
+
'parts, not both.');
|
|
12614
|
+
}
|
|
12615
|
+
return (jsxRuntime.jsx(CommandParts, { ...rest, ref: ref, children: jsxRuntime.jsx(CommandActionsContent, { actions: actions, filter: filter, placeholder: placeholder, empty: empty }) }));
|
|
12616
|
+
});
|
|
12617
|
+
CommandFromActions.displayName = 'CommandFromActions';
|
|
12618
|
+
/**
|
|
12619
|
+
* The `Command` component is a search field over a list of things to do, which narrows as you type and runs the top match on Enter: inline by default, and a command palette in a modal as `Command.Dialog`.
|
|
12620
|
+
*
|
|
12621
|
+
* Given `actions`, it's one element. It renders the field, the list, the
|
|
12622
|
+
* groups and the empty message, and filters the actions by label, keywords
|
|
12623
|
+
* and description as the user types, with label matches first. `filter`
|
|
12624
|
+
* takes over the matching, for fuzzy search.
|
|
12625
|
+
*
|
|
12626
|
+
* Built from parts, the app owns the items: what's in the list and how it's
|
|
12627
|
+
* filtered, from the search `onQueryChange` reports. Nothing filters them,
|
|
12628
|
+
* so a server's answer shows as it came.
|
|
12629
|
+
*
|
|
12630
|
+
* Either way, focus stays in the field, and the highlighted item is its
|
|
12631
|
+
* `aria-activedescendant`. The first enabled item is highlighted whenever
|
|
12632
|
+
* the items change, so Enter runs the top match. Running an item calls its
|
|
12633
|
+
* own `onSelect`, then the Command's.
|
|
12634
|
+
*
|
|
12635
|
+
* When typing leaves the empty message on screen, its text is announced
|
|
12636
|
+
* through a polite status region, once, after any loading ends. Result
|
|
12637
|
+
* counts aren't announced: a screen reader reads the list's size from the
|
|
12638
|
+
* markup, and a count on every keystroke talks over the typing.
|
|
12639
|
+
*
|
|
12640
|
+
* @function
|
|
12641
|
+
* @param {CommandProps} props - Props for the Command component.
|
|
12642
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `div.command`.
|
|
12643
|
+
* @returns {JSX.Element} The field and its list.
|
|
12644
|
+
*
|
|
12645
|
+
* @example
|
|
12646
|
+
* <Command
|
|
12647
|
+
* label="Settings"
|
|
12648
|
+
* placeholder="Search settings"
|
|
12649
|
+
* actions={[
|
|
12650
|
+
* { id: 'profile', label: 'Edit profile', onSelect: openProfile },
|
|
12651
|
+
* { id: 'theme', label: 'Dark mode', keywords: ['theme'], onSelect: toggleTheme },
|
|
12652
|
+
* ]}
|
|
12653
|
+
* />
|
|
12654
|
+
*
|
|
12655
|
+
* @example
|
|
12656
|
+
* <Command label="People" onQueryChange={setQuery} onSelect={openProfile}>
|
|
12657
|
+
* <Command.Input placeholder="Find a person" />
|
|
12658
|
+
* {matches.length > 0 ? (
|
|
12659
|
+
* <Command.List>
|
|
12660
|
+
* {matches.map(person => (
|
|
12661
|
+
* <Command.Item key={person.id} value={person.id}>
|
|
12662
|
+
* {person.name}
|
|
12663
|
+
* </Command.Item>
|
|
12664
|
+
* ))}
|
|
12665
|
+
* </Command.List>
|
|
12666
|
+
* ) : (
|
|
12667
|
+
* <Command.Empty>No one matches</Command.Empty>
|
|
12668
|
+
* )}
|
|
12669
|
+
* </Command>
|
|
12670
|
+
*/
|
|
12671
|
+
const CommandRoot = React.forwardRef((props, ref) => props.actions === undefined ? (jsxRuntime.jsx(CommandParts, { ...props, ref: ref })) : (jsxRuntime.jsx(CommandFromActions, { ...props, ref: ref })));
|
|
12672
|
+
/** Other names a key goes by in a `shortcut`, for the ones `key` spells otherwise. */
|
|
12673
|
+
const keyAliases = {
|
|
12674
|
+
space: ' ',
|
|
12675
|
+
esc: 'escape',
|
|
12676
|
+
};
|
|
12677
|
+
/**
|
|
12678
|
+
* Reads a `shortcut` such as `"mod+k"` or `"ctrl + shift + p"`.
|
|
12679
|
+
*
|
|
12680
|
+
* @param shortcut - Modifiers and a key, joined by `+`.
|
|
12681
|
+
* @returns The combination, or `null` when it doesn't read as one.
|
|
12682
|
+
*/
|
|
12683
|
+
const parseShortcut = (shortcut) => {
|
|
12684
|
+
const parts = shortcut
|
|
12685
|
+
.toLowerCase()
|
|
12686
|
+
.split('+')
|
|
12687
|
+
.map(part => part.trim());
|
|
12688
|
+
const key = parts.pop();
|
|
12689
|
+
if (key === '')
|
|
12690
|
+
return null;
|
|
12691
|
+
const parsed = {
|
|
12692
|
+
key: keyAliases[key] ?? key,
|
|
12693
|
+
mod: false,
|
|
12694
|
+
ctrl: false,
|
|
12695
|
+
meta: false,
|
|
12696
|
+
alt: false,
|
|
12697
|
+
shift: false,
|
|
12698
|
+
};
|
|
12699
|
+
for (const part of parts) {
|
|
12700
|
+
if (part === 'mod')
|
|
12701
|
+
parsed.mod = true;
|
|
12702
|
+
else if (part === 'ctrl' || part === 'control')
|
|
12703
|
+
parsed.ctrl = true;
|
|
12704
|
+
else if (part === 'meta' || part === 'cmd' || part === 'command')
|
|
12705
|
+
parsed.meta = true;
|
|
12706
|
+
else if (part === 'alt' || part === 'option')
|
|
12707
|
+
parsed.alt = true;
|
|
12708
|
+
else if (part === 'shift')
|
|
12709
|
+
parsed.shift = true;
|
|
12710
|
+
else
|
|
12711
|
+
return null;
|
|
12712
|
+
}
|
|
12713
|
+
return parsed;
|
|
12714
|
+
};
|
|
12715
|
+
/**
|
|
12716
|
+
* Whether a keydown is the combination. `mod` takes Ctrl or ⌘, and any other
|
|
12717
|
+
* modifier has to be as the combination says, so `mod+k` isn't
|
|
12718
|
+
* `mod+shift+k`. A letter or digit is matched by its key, and by where it
|
|
12719
|
+
* sits on the keyboard only when the key isn't a Latin letter or digit at
|
|
12720
|
+
* all, as on a Cyrillic layout or with Option held on a Mac, so another
|
|
12721
|
+
* Latin layout's own letters keep their own shortcuts.
|
|
12722
|
+
*/
|
|
12723
|
+
const matchesShortcut = (event, shortcut) => {
|
|
12724
|
+
if (event.altKey !== shortcut.alt)
|
|
12725
|
+
return false;
|
|
12726
|
+
// A symbol such as `?` already says whether Shift was down, and which key
|
|
12727
|
+
// takes Shift to type it depends on the layout, so Shift is only checked
|
|
12728
|
+
// for a letter, a digit or a named key, unless the combination names it.
|
|
12729
|
+
const shiftMatters = shortcut.shift ||
|
|
12730
|
+
shortcut.key.length > 1 ||
|
|
12731
|
+
/^[a-z0-9]$/.test(shortcut.key);
|
|
12732
|
+
if (shiftMatters && event.shiftKey !== shortcut.shift)
|
|
12733
|
+
return false;
|
|
12734
|
+
if (shortcut.mod) {
|
|
12735
|
+
if (!event.ctrlKey && !event.metaKey)
|
|
12736
|
+
return false;
|
|
12737
|
+
}
|
|
12738
|
+
else if (event.ctrlKey !== shortcut.ctrl ||
|
|
12739
|
+
event.metaKey !== shortcut.meta) {
|
|
12740
|
+
return false;
|
|
12741
|
+
}
|
|
12742
|
+
const key = (event.key ?? '').toLowerCase();
|
|
12743
|
+
if (key === shortcut.key)
|
|
12744
|
+
return true;
|
|
12745
|
+
if (/^[a-z0-9]$/.test(key))
|
|
12746
|
+
return false;
|
|
12747
|
+
if (/^[a-z]$/.test(shortcut.key)) {
|
|
12748
|
+
return event.code === `Key${shortcut.key.toUpperCase()}`;
|
|
12749
|
+
}
|
|
12750
|
+
if (/^[0-9]$/.test(shortcut.key)) {
|
|
12751
|
+
return event.code === `Digit${shortcut.key}`;
|
|
12752
|
+
}
|
|
12753
|
+
return false;
|
|
12754
|
+
};
|
|
12755
|
+
/** Input types that take no typing, so a shortcut pressed in one still counts. */
|
|
12756
|
+
const untypedInputs = new Set([
|
|
12757
|
+
'button',
|
|
12758
|
+
'checkbox',
|
|
12759
|
+
'color',
|
|
12760
|
+
'file',
|
|
12761
|
+
'hidden',
|
|
12762
|
+
'image',
|
|
12763
|
+
'radio',
|
|
12764
|
+
'range',
|
|
12765
|
+
'reset',
|
|
12766
|
+
'submit',
|
|
12767
|
+
]);
|
|
12768
|
+
/**
|
|
12769
|
+
* Whether a keydown happened while the user was typing: in a text field, a
|
|
12770
|
+
* text area, a select, or editable content. The composed path's first entry
|
|
12771
|
+
* is the element itself, even inside a shadow root.
|
|
12772
|
+
*/
|
|
12773
|
+
const isTyping = (event) => {
|
|
12774
|
+
const target = event.composedPath()[0];
|
|
12775
|
+
if (typeof target?.closest !== 'function')
|
|
12776
|
+
return false;
|
|
12777
|
+
const element = target;
|
|
12778
|
+
const editable = element.closest('[contenteditable]');
|
|
12779
|
+
if (editable && editable.getAttribute('contenteditable') !== 'false') {
|
|
12780
|
+
return true;
|
|
12781
|
+
}
|
|
12782
|
+
const tag = element.tagName;
|
|
12783
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT')
|
|
12784
|
+
return true;
|
|
12785
|
+
return (tag === 'INPUT' &&
|
|
12786
|
+
!untypedInputs.has(element.type.toLowerCase()));
|
|
12787
|
+
};
|
|
12788
|
+
/**
|
|
12789
|
+
* The Command in a modal, a command palette, opened by its `shortcut` or by `open`, that closes once an item has run.
|
|
12790
|
+
*
|
|
12791
|
+
* It renders nothing while it's closed. Open, it's a `Modal` with a
|
|
12792
|
+
* background and the Command's box as the `dialog`, named by `label`:
|
|
12793
|
+
* focus goes to the search field, Tab stays inside, Escape and a click on
|
|
12794
|
+
* the background close it, the page behind it doesn't scroll, and focus goes
|
|
12795
|
+
* back to where it was when it closes.
|
|
12796
|
+
*
|
|
12797
|
+
* @function
|
|
12798
|
+
* @param {CommandDialogProps} props - Props for the Command.Dialog component.
|
|
12799
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `div.command` that is the dialog.
|
|
12800
|
+
* @returns {JSX.Element | null} The dialog, or nothing while it's closed.
|
|
12801
|
+
*/
|
|
12802
|
+
const CommandDialog = React.forwardRef(({ open: controlledOpen, defaultOpen = false, onOpenChange, shortcut, closeOnSelect = true, clearOnClose = true, portal, label, query: controlledQuery, defaultQuery = '', onQueryChange, onSelect, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }, ref) => {
|
|
12803
|
+
const [ownOpen, setOwnOpen] = React.useState(defaultOpen);
|
|
12804
|
+
const open = controlledOpen ?? ownOpen;
|
|
12805
|
+
// Only ever asked for the other state: the shortcut listens while it's
|
|
12806
|
+
// closed, and everything that closes it is there while it's open.
|
|
12807
|
+
const setOpen = (next) => {
|
|
12808
|
+
if (controlledOpen === undefined)
|
|
12809
|
+
setOwnOpen(next);
|
|
12810
|
+
onOpenChange?.(next);
|
|
12811
|
+
};
|
|
12812
|
+
// The search is held here rather than in the Command, which is only
|
|
12813
|
+
// mounted while the dialog is open, so it can outlive a close when
|
|
12814
|
+
// `clearOnClose` is off.
|
|
12815
|
+
const [ownQuery, setOwnQuery] = React.useState(defaultQuery);
|
|
12816
|
+
const query = controlledQuery ?? ownQuery;
|
|
12817
|
+
const setQuery = (next) => {
|
|
12818
|
+
if (controlledQuery === undefined)
|
|
12819
|
+
setOwnQuery(next);
|
|
12820
|
+
onQueryChange?.(next);
|
|
12821
|
+
};
|
|
12822
|
+
// Closing clears the search, however the dialog closed. Our own is
|
|
12823
|
+
// cleared during render, React's pattern for adjusting state when other
|
|
12824
|
+
// state changes, and a controlled one is asked for after the commit.
|
|
12825
|
+
const [shownOpen, setShownOpen] = React.useState(open);
|
|
12826
|
+
if (shownOpen !== open) {
|
|
12827
|
+
setShownOpen(open);
|
|
12828
|
+
if (!open && clearOnClose && controlledQuery === undefined) {
|
|
12829
|
+
setOwnQuery('');
|
|
12830
|
+
}
|
|
12831
|
+
}
|
|
12832
|
+
const wasOpenRef = React.useRef(open);
|
|
12833
|
+
const clearRef = useLatestRef(() => {
|
|
12834
|
+
if (clearOnClose && controlledQuery !== undefined && query !== '') {
|
|
12835
|
+
onQueryChange?.('');
|
|
12836
|
+
}
|
|
12837
|
+
});
|
|
12838
|
+
React.useEffect(() => {
|
|
12839
|
+
const wasOpen = wasOpenRef.current;
|
|
12840
|
+
wasOpenRef.current = open;
|
|
12841
|
+
if (wasOpen && !open)
|
|
12842
|
+
clearRef.current();
|
|
12843
|
+
}, [open, clearRef]);
|
|
12844
|
+
if (controlledOpen === undefined && !defaultOpen && !shortcut) {
|
|
12845
|
+
warnOnce('Command.Dialog:no-opener', '[bestax-bulma] <Command.Dialog> has no way to open. Give it a ' +
|
|
12846
|
+
'shortcut, or open and onOpenChange.');
|
|
12847
|
+
}
|
|
12848
|
+
if (shortcut && !parseShortcut(shortcut)) {
|
|
12849
|
+
warnOnce(`Command.Dialog:shortcut:${shortcut}`, `[bestax-bulma] <Command.Dialog> can't read the shortcut ` +
|
|
12850
|
+
`"${shortcut}". Write modifiers and a key joined by +, such as ` +
|
|
12851
|
+
`"mod+k", where the modifiers are mod, ctrl, meta, alt and shift.`);
|
|
12852
|
+
}
|
|
12853
|
+
const openFromShortcut = useLatestRef(() => setOpen(true));
|
|
12854
|
+
React.useEffect(() => {
|
|
12855
|
+
const combination = shortcut ? parseShortcut(shortcut) : null;
|
|
12856
|
+
if (!combination || open)
|
|
12857
|
+
return undefined;
|
|
12858
|
+
const onKeyDown = (event) => {
|
|
12859
|
+
if (event.defaultPrevented ||
|
|
12860
|
+
event.isComposing ||
|
|
12861
|
+
event.keyCode === 229 ||
|
|
12862
|
+
!matchesShortcut(event, combination) ||
|
|
12863
|
+
isTyping(event) ||
|
|
12864
|
+
// A Modal, a Dialog, a Sidebar with its overlay or a full-page
|
|
12865
|
+
// Loading has the page shut off, so the key isn't the page's. A
|
|
12866
|
+
// Sidebar without one leaves the page in use, so it still opens.
|
|
12867
|
+
isModalOverlayOpen()) {
|
|
12868
|
+
return;
|
|
12869
|
+
}
|
|
12870
|
+
// The browser's own use of the combination, such as Ctrl+K's search
|
|
12871
|
+
// in the address bar, doesn't happen as well.
|
|
12872
|
+
event.preventDefault();
|
|
12873
|
+
openFromShortcut.current();
|
|
12874
|
+
};
|
|
12875
|
+
// On `window`, where a keydown arrives last, so anything on the page
|
|
12876
|
+
// that used the key first has said so with `preventDefault()`.
|
|
12877
|
+
window.addEventListener('keydown', onKeyDown);
|
|
12878
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
12879
|
+
}, [shortcut, open, openFromShortcut]);
|
|
12880
|
+
const dialogClass = usePrefixedClassNames('command-dialog');
|
|
12881
|
+
if (!open)
|
|
12882
|
+
return null;
|
|
12883
|
+
const close = () => setOpen(false);
|
|
12884
|
+
const run = (value) => {
|
|
12885
|
+
onSelect?.(value);
|
|
12886
|
+
if (closeOnSelect)
|
|
12887
|
+
close();
|
|
12888
|
+
};
|
|
12889
|
+
return (jsxRuntime.jsxs(Modal, { isActive: true, onClose: close, role: "presentation", portal: portal, children: [jsxRuntime.jsx(Modal.Background, { onClick: close }), jsxRuntime.jsx(CommandRoot, { ...props, ref: ref, className: classNames(dialogClass, className), label: label, query: query, onQueryChange: setQuery, onSelect: run, role: "dialog", "aria-modal": "true", "aria-label": ariaLabelledBy === undefined
|
|
12890
|
+
? ariaLabel || label || defaultLabel
|
|
12891
|
+
: ariaLabel, "aria-labelledby": ariaLabelledBy })] }));
|
|
12892
|
+
});
|
|
12893
|
+
CommandDialog.displayName = 'CommandDialog';
|
|
12894
|
+
/** Command with Input, List, Group, Item, Empty and Dialog sub-components. */
|
|
12895
|
+
const Command = withSubComponents(CommandRoot, {
|
|
12896
|
+
Input: CommandInput,
|
|
12897
|
+
List: CommandList,
|
|
12898
|
+
Group: CommandGroup,
|
|
12899
|
+
Item: CommandItem,
|
|
12900
|
+
Empty: CommandEmpty,
|
|
12901
|
+
Dialog: CommandDialog,
|
|
12902
|
+
}, 'Command');
|
|
12903
|
+
|
|
11428
12904
|
/**
|
|
11429
12905
|
* The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
|
|
11430
12906
|
*
|
|
@@ -11739,64 +13215,20 @@ const FigureComponent = ({ className, textColor, bgColor, children, ...props })
|
|
|
11739
13215
|
});
|
|
11740
13216
|
const bulmaClasses = usePrefixedClassNames();
|
|
11741
13217
|
const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
11742
|
-
return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
|
|
11743
|
-
};
|
|
11744
|
-
/**
|
|
11745
|
-
* Figure component with Caption subcomponent.
|
|
11746
|
-
*
|
|
11747
|
-
* @example
|
|
11748
|
-
* <Figure>
|
|
11749
|
-
* <img src="image.jpg" alt="Description" />
|
|
11750
|
-
* <Figure.Caption>Image caption text</Figure.Caption>
|
|
11751
|
-
* </Figure>
|
|
11752
|
-
*/
|
|
11753
|
-
const Figure = withSubComponents(FigureComponent, {
|
|
11754
|
-
Caption: FigureCaption,
|
|
11755
|
-
}, 'Figure');
|
|
11756
|
-
|
|
11757
|
-
/**
|
|
11758
|
-
* Distinguishes an `IconProps` object from a plain custom node (an inline SVG, a
|
|
11759
|
-
* `react-icons` component, …) in a slot that accepts either — `Control`'s
|
|
11760
|
-
* `iconLeft`/`iconRight`, `IconText`'s `iconProps`.
|
|
11761
|
-
*
|
|
11762
|
-
* One copy, imported by both consumers: two hand-maintained copies, one per
|
|
11763
|
-
* consumer, is how the deprecated `icon` path came to crash both (#663).
|
|
11764
|
-
*
|
|
11765
|
-
* Its own module rather than `Icon.tsx` because `src/index.ts` wildcard-exports
|
|
11766
|
-
* that file, and everything published there is public forever. `polymorphic.ts`
|
|
11767
|
-
* makes the same call for `isCustomElement`: an internal discriminator is not
|
|
11768
|
-
* something to support for the life of the package.
|
|
11769
|
-
*
|
|
11770
|
-
* It tests each member's VALUE, not merely its key, and only after everything
|
|
11771
|
-
* React owns is out of the way. `'icon' in value` was enough
|
|
11772
|
-
* to claim a Font Awesome `IconDefinition` — `{ prefix, iconName, icon: [w, h, …,
|
|
11773
|
-
* path] }` — whose `icon` is a path array rather than a class string. That object
|
|
11774
|
-
* is not a renderable node either, so the honest outcome is the one this slot has
|
|
11775
|
-
* always had for it: the node branch, and React's own "Objects are not valid as a
|
|
11776
|
-
* React child". Claiming it here instead rendered a blank container that still
|
|
11777
|
-
* reserved layout.
|
|
11778
|
-
*
|
|
11779
|
-
* A member added to `IconProps` needs a line here, and a missed one is a crash
|
|
11780
|
-
* rather than a fallthrough. That is the cost of a slot taking either a props
|
|
11781
|
-
* object or a node; keeping the guard in one place is what keeps the cost to a
|
|
11782
|
-
* single line.
|
|
11783
|
-
*/
|
|
11784
|
-
function isIconProps(value) {
|
|
11785
|
-
if (typeof value !== 'object' || value === null)
|
|
11786
|
-
return false;
|
|
11787
|
-
// Anything React owns. `isValidElement` covers elements only; a PORTAL is a
|
|
11788
|
-
// valid `ReactNode` that fails it and carries a `children` key, so the member
|
|
11789
|
-
// test below claimed it and `Control` spread it into `<Icon>` as props —
|
|
11790
|
-
// rendering the portal's child locally instead of in its container. Every
|
|
11791
|
-
// React internal carries `$$typeof`, so testing for it covers lazy, context
|
|
11792
|
-
// and whatever React adds next, rather than portals alone.
|
|
11793
|
-
if (React.isValidElement(value) || '$$typeof' in value)
|
|
11794
|
-
return false;
|
|
11795
|
-
const candidate = value;
|
|
11796
|
-
return (typeof candidate.name === 'string' ||
|
|
11797
|
-
typeof candidate.icon === 'string' ||
|
|
11798
|
-
'children' in candidate);
|
|
11799
|
-
}
|
|
13218
|
+
return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
|
|
13219
|
+
};
|
|
13220
|
+
/**
|
|
13221
|
+
* Figure component with Caption subcomponent.
|
|
13222
|
+
*
|
|
13223
|
+
* @example
|
|
13224
|
+
* <Figure>
|
|
13225
|
+
* <img src="image.jpg" alt="Description" />
|
|
13226
|
+
* <Figure.Caption>Image caption text</Figure.Caption>
|
|
13227
|
+
* </Figure>
|
|
13228
|
+
*/
|
|
13229
|
+
const Figure = withSubComponents(FigureComponent, {
|
|
13230
|
+
Caption: FigureCaption,
|
|
13231
|
+
}, 'Figure');
|
|
11800
13232
|
|
|
11801
13233
|
/**
|
|
11802
13234
|
* The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
|
|
@@ -12881,392 +14313,150 @@ const UnorderedListComponent = ({ className, textColor, bgColor, children, ...pr
|
|
|
12881
14313
|
backgroundColor: bgColor,
|
|
12882
14314
|
...props,
|
|
12883
14315
|
});
|
|
12884
|
-
const bulmaClasses = usePrefixedClassNames();
|
|
12885
|
-
const listClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
12886
|
-
return (jsxRuntime.jsx("ul", { className: listClasses || undefined, ...rest, children: children }));
|
|
12887
|
-
};
|
|
12888
|
-
const UnorderedList = withSubComponents(UnorderedListComponent, { Item: ListItem }, 'UnorderedList');
|
|
12889
|
-
|
|
12890
|
-
//
|
|
12891
|
-
//
|
|
12892
|
-
//
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12897
|
-
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
*
|
|
12906
|
-
*
|
|
12907
|
-
*
|
|
12908
|
-
*
|
|
12909
|
-
* @
|
|
12910
|
-
* @
|
|
12911
|
-
*
|
|
12912
|
-
* @
|
|
12913
|
-
*
|
|
12914
|
-
*
|
|
12915
|
-
*
|
|
12916
|
-
*
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
12934
|
-
|
|
12935
|
-
//
|
|
12936
|
-
//
|
|
12937
|
-
//
|
|
12938
|
-
|
|
12939
|
-
const leftIconValue = iconLeft ||
|
|
12940
|
-
(iconLeftName
|
|
12941
|
-
? {
|
|
12942
|
-
name: iconLeftName,
|
|
12943
|
-
size: iconLeftSize,
|
|
12944
|
-
}
|
|
12945
|
-
: undefined);
|
|
12946
|
-
const rightIconValue = iconRight ||
|
|
12947
|
-
(iconRightName
|
|
12948
|
-
? {
|
|
12949
|
-
name: iconRightName,
|
|
12950
|
-
size: iconRightSize,
|
|
12951
|
-
}
|
|
12952
|
-
: undefined);
|
|
12953
|
-
const mainClass = usePrefixedClassNames('control', {
|
|
12954
|
-
'has-icons-left': hasIconsLeft || !!leftIconValue,
|
|
12955
|
-
'has-icons-right': hasIconsRight || !!rightIconValue,
|
|
12956
|
-
'is-loading': isLoading,
|
|
12957
|
-
'is-expanded': isExpanded,
|
|
12958
|
-
[`is-${size}`]: !!size,
|
|
12959
|
-
});
|
|
12960
|
-
const controlClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
12961
|
-
// --- FIX: Spread both restProps (for data-testid, etc) AND rest (from useBulmaClasses) ---
|
|
12962
|
-
return (jsxRuntime.jsx(ControlProvider, { value: true, children: jsxRuntime.jsxs(Component, { className: controlClass, ref: ref, ...restProps, ...rest, children: [jsxRuntime.jsx(ControlLoadingProvider, { value: !!isLoading, children: children }), leftIconValue &&
|
|
12963
|
-
(isIconProps(leftIconValue) ? (jsxRuntime.jsx(Icon, { ...leftIconValue, className: prefixedClassNames(classPrefix, 'is-left') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-left'), children: leftIconValue }))), rightIconValue &&
|
|
12964
|
-
(isIconProps(rightIconValue) ? (jsxRuntime.jsx(Icon, { ...rightIconValue, className: prefixedClassNames(classPrefix, 'is-right') })) : (jsxRuntime.jsx(Icon, { className: prefixedClassNames(classPrefix, 'is-right'), children: rightIconValue })))] }) }));
|
|
12965
|
-
});
|
|
12966
|
-
Control.displayName = 'Control';
|
|
12967
|
-
|
|
12968
|
-
/**
|
|
12969
|
-
* FieldLabel component for rendering a Bulma field label. It renders the
|
|
12970
|
-
* label column of a horizontal `Field` (a `div` with the `field-label` class),
|
|
12971
|
-
* not a `<label>`, so text placed straight in it names nothing. Put a `<label>`
|
|
12972
|
-
* with the `label` class inside it and point its `htmlFor` at the control's
|
|
12973
|
-
* `id`, or give the horizontal `Field` a `label` prop, which renders this
|
|
12974
|
-
* column and its `<label>` for you. A `<label>` you put here names the
|
|
12975
|
-
* control through its `htmlFor` alone: a range `Slider`'s thumbs and an
|
|
12976
|
-
* `Autocomplete`'s, a `Combobox`'s or a `Taginput`'s list point
|
|
12977
|
-
* `aria-labelledby` at a `Field`'s label only when its `label` prop renders it
|
|
12978
|
-
* and it names the control. Where it would not name the control on its own, as in an inner `Field`,
|
|
12979
|
-
* pass `labelProps={{ htmlFor, id }}` with a matching `id` on the control.
|
|
12980
|
-
*
|
|
12981
|
-
* @function
|
|
12982
|
-
* @param {FieldLabelProps} props - Props for the FieldLabel component.
|
|
12983
|
-
* @returns {JSX.Element} The rendered field label.
|
|
12984
|
-
*
|
|
12985
|
-
* @example
|
|
12986
|
-
* <FieldLabel size="normal">
|
|
12987
|
-
* <label className="label" htmlFor="name">Name</label>
|
|
12988
|
-
* </FieldLabel>
|
|
12989
|
-
*/
|
|
12990
|
-
const FieldLabel = ({ size, textColor, bgColor, className, children, ...props }) => {
|
|
12991
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
12992
|
-
color: textColor,
|
|
12993
|
-
backgroundColor: bgColor,
|
|
12994
|
-
...props,
|
|
12995
|
-
});
|
|
12996
|
-
const mainClass = usePrefixedClassNames('field-label', {
|
|
12997
|
-
[`is-${size}`]: !!size,
|
|
12998
|
-
});
|
|
12999
|
-
const fieldLabelClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
13000
|
-
// Spread ...props and ...rest so custom props like data-testid are included
|
|
13001
|
-
return (jsxRuntime.jsx("div", { className: fieldLabelClass, ...props, ...rest, children: children }));
|
|
13002
|
-
};
|
|
13003
|
-
/**
|
|
13004
|
-
* FieldBody component for rendering Bulma field body.
|
|
13005
|
-
*
|
|
13006
|
-
* @function
|
|
13007
|
-
* @param {FieldBodyProps} props - Props for the FieldBody component.
|
|
13008
|
-
* @returns {JSX.Element} The rendered field body.
|
|
13009
|
-
*
|
|
13010
|
-
* @example
|
|
13011
|
-
* <FieldBody><input className="input" /></FieldBody>
|
|
13012
|
-
*/
|
|
13013
|
-
const FieldBody = ({ textColor, bgColor, className, children, ...props }) => {
|
|
13014
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13015
|
-
color: textColor,
|
|
13016
|
-
backgroundColor: bgColor,
|
|
13017
|
-
...props,
|
|
13018
|
-
});
|
|
13019
|
-
const mainClass = usePrefixedClassNames('field-body');
|
|
13020
|
-
const fieldBodyClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
13021
|
-
// Spread ...props and ...rest so custom props like data-testid are included
|
|
13022
|
-
return (jsxRuntime.jsx("div", { className: fieldBodyClass, ...props, ...rest, children: children }));
|
|
13023
|
-
};
|
|
13024
|
-
/**
|
|
13025
|
-
* The `Field` component is a Bulma-styled form field container.
|
|
13026
|
-
*
|
|
13027
|
-
* @function
|
|
13028
|
-
* @param {FieldProps} props - Props for the Field component.
|
|
13029
|
-
* @returns {JSX.Element} The rendered field container.
|
|
13030
|
-
* @see {@link https://bulma.io/documentation/form/general/#field | Bulma Field documentation}
|
|
13031
|
-
*
|
|
13032
|
-
* @example
|
|
13033
|
-
* // Labelled field — the label auto-associates with the composed base
|
|
13034
|
-
* <Field label="Email">
|
|
13035
|
-
* <Control>
|
|
13036
|
-
* <InputBase type="email" />
|
|
13037
|
-
* </Control>
|
|
13038
|
-
* </Field>
|
|
13039
|
-
*
|
|
13040
|
-
* @example
|
|
13041
|
-
* // Horizontal field
|
|
13042
|
-
* <Field horizontal label="Name">
|
|
13043
|
-
* <Control>
|
|
13044
|
-
* <InputBase />
|
|
13045
|
-
* </Control>
|
|
13046
|
-
* </Field>
|
|
13047
|
-
*/
|
|
13048
|
-
const FieldComponent = ({ horizontal, grouped, hasAddons, narrow, label, labelSize, labelProps, textColor, color: _fieldColor, bgColor, className, children, ...props }) => {
|
|
13049
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13050
|
-
color: textColor,
|
|
13051
|
-
backgroundColor: bgColor,
|
|
13052
|
-
...props,
|
|
13053
|
-
});
|
|
13054
|
-
const mainClass = usePrefixedClassNames('field', {
|
|
13055
|
-
'is-horizontal': horizontal,
|
|
13056
|
-
'has-addons': !!hasAddons,
|
|
13057
|
-
'has-addons-centered': hasAddons === 'centered',
|
|
13058
|
-
'has-addons-right': hasAddons === 'right',
|
|
13059
|
-
'is-narrow': narrow,
|
|
13060
|
-
'is-grouped': grouped === true ||
|
|
13061
|
-
grouped === 'centered' ||
|
|
13062
|
-
grouped === 'right' ||
|
|
13063
|
-
grouped === 'multiline',
|
|
13064
|
-
'is-grouped-centered': grouped === 'centered',
|
|
13065
|
-
'is-grouped-right': grouped === 'right',
|
|
13066
|
-
'is-grouped-multiline': grouped === 'multiline',
|
|
13067
|
-
});
|
|
13068
|
-
const fieldClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
13069
|
-
// Default labelSize to 'normal' when horizontal for proper baseline alignment
|
|
13070
|
-
const effectiveLabelSize = labelSize ?? (horizontal ? 'normal' : undefined);
|
|
13071
|
-
const labelClass = usePrefixedClassNames('label');
|
|
13072
|
-
// Auto-associate the label with a single composed base control (#495): the
|
|
13073
|
-
// label points at a generated id shared via context, which a composed base
|
|
13074
|
-
// adopts when the user supplied no id of their own.
|
|
13075
|
-
// Presence semantics on htmlFor — even an explicit `htmlFor: undefined`
|
|
13076
|
-
// means the caller owns the association. Grouped/addons fields hold several
|
|
13077
|
-
// controls, so no single association is generated for them.
|
|
13078
|
-
const generatedId = React.useId();
|
|
13079
|
-
const userWiredLabel = !!labelProps && 'htmlFor' in labelProps;
|
|
13080
|
-
const targetId = label && !userWiredLabel && !grouped && !hasAddons
|
|
13081
|
-
? generatedId
|
|
13082
|
-
: undefined;
|
|
13083
|
-
// A group control (Radios, Checkboxes, Rate, DateRangeInput and its base)
|
|
13084
|
-
// cannot take that `for`, so it points `aria-labelledby` at the label
|
|
13085
|
-
// itself (#939).
|
|
13086
|
-
// The label gets an id whenever the association is on, unless the caller
|
|
13087
|
-
// gave it one.
|
|
13088
|
-
const labelId = labelProps?.id ?? (targetId ? `${targetId}-label` : undefined);
|
|
13089
|
-
// What the rendered label's `for` points at, generated or the caller's, and
|
|
13090
|
-
// the id it renders with, so a control can tell when a label wired by hand
|
|
13091
|
-
// names it and point at that label. An unlabeled Field passes on its
|
|
13092
|
-
// parent's. Unlike `targetId`, a label wired by hand passes on its own in
|
|
13093
|
-
// `grouped`/`hasAddons` Fields too, on purpose: those only stop the Field
|
|
13094
|
-
// picking one control out of several, and the caller's `for` has picked
|
|
13095
|
-
// one, so a range Slider or Autocomplete it names points at that label in
|
|
13096
|
-
// a row as well. A group reads the label element id below instead, which
|
|
13097
|
-
// stays off for a label wired by hand, since no group takes its `for`.
|
|
13098
|
-
const inheritedLabelFor = useFieldLabelFor();
|
|
13099
|
-
const labelForTarget = userWiredLabel ? labelProps.htmlFor : targetId;
|
|
13100
|
-
const ownLabelFor = React.useMemo(() => ({ htmlFor: labelForTarget, id: labelId }), [labelForTarget, labelId]);
|
|
13101
|
-
const labelFor = label ? ownLabelFor : inheritedLabelFor;
|
|
13102
|
-
// Whether the label keeps its generated `for` (#1004). Content that takes
|
|
13103
|
-
// the Field's id says so from a layout effect, and this Field's own layout
|
|
13104
|
-
// effect runs after its content's in the same commit, so by then every
|
|
13105
|
-
// report has landed. Until it runs, on the server and in the first render
|
|
13106
|
-
// while hydrating, the label writes the `for` as it always has. From then
|
|
13107
|
-
// on it keeps the `for` only while some content holds the id, and content
|
|
13108
|
-
// that mounts or changes later reports when it does. Counted in a ref, and
|
|
13109
|
-
// state is set only once settled, so content that takes the id renders
|
|
13110
|
-
// the Field just once.
|
|
13111
|
-
const takersRef = React.useRef(0);
|
|
13112
|
-
const settledRef = React.useRef(false);
|
|
13113
|
-
const [forTaken, setForTaken] = React.useState(true);
|
|
13114
|
-
const settle = React.useCallback(() => {
|
|
13115
|
-
if (settledRef.current)
|
|
13116
|
-
setForTaken(takersRef.current > 0);
|
|
13117
|
-
}, []);
|
|
13118
|
-
const reportLabelFor = React.useCallback(() => {
|
|
13119
|
-
takersRef.current += 1;
|
|
13120
|
-
settle();
|
|
13121
|
-
return () => {
|
|
13122
|
-
takersRef.current -= 1;
|
|
13123
|
-
settle();
|
|
13124
|
-
};
|
|
13125
|
-
}, [settle]);
|
|
14316
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
14317
|
+
const listClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14318
|
+
return (jsxRuntime.jsx("ul", { className: listClasses || undefined, ...rest, children: children }));
|
|
14319
|
+
};
|
|
14320
|
+
const UnorderedList = withSubComponents(UnorderedListComponent, { Item: ListItem }, 'UnorderedList');
|
|
14321
|
+
|
|
14322
|
+
// The colors `Checkbox` takes, as `checkboxColors` lists them. Declared here as
|
|
14323
|
+
// well, so the API page can read them off this module, and a type test holds
|
|
14324
|
+
// the two lists to each other.
|
|
14325
|
+
const checkboxCardColors = [
|
|
14326
|
+
'primary',
|
|
14327
|
+
'link',
|
|
14328
|
+
'info',
|
|
14329
|
+
'success',
|
|
14330
|
+
'warning',
|
|
14331
|
+
'danger',
|
|
14332
|
+
];
|
|
14333
|
+
/**
|
|
14334
|
+
* The `CheckboxCard` component is a checkbox drawn as a card, with a title and a description, that toggles wherever you click it.
|
|
14335
|
+
*
|
|
14336
|
+
* It's a `<label>` around a real checkbox, so it sends its `value` with a
|
|
14337
|
+
* form and Space toggles it. The title names the checkbox and the
|
|
14338
|
+
* description describes it. Alone it's one choice; inside `Checkboxes` it
|
|
14339
|
+
* takes the group's name and selection, as `Checkbox` does.
|
|
14340
|
+
*
|
|
14341
|
+
* @function
|
|
14342
|
+
* @param {CheckboxCardProps} props - Props for the CheckboxCard component.
|
|
14343
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
|
|
14344
|
+
* @returns {JSX.Element} The rendered card.
|
|
14345
|
+
*
|
|
14346
|
+
* @example
|
|
14347
|
+
* <Checkboxes label="Add-ons" name="extras" defaultValue={['backups']}>
|
|
14348
|
+
* <CheckboxCard value="backups" title="Daily backups" description="Kept for 30 days." />
|
|
14349
|
+
* <CheckboxCard value="domain" title="Custom domain" description="Bring your own." />
|
|
14350
|
+
* </Checkboxes>
|
|
14351
|
+
*/
|
|
14352
|
+
const CheckboxCard = React.forwardRef(({ title, description, children, hasIndicator = true, color, className, textColor, disabled, name, value, checked, defaultChecked, indeterminate, onChange, ...props }, ref) => {
|
|
14353
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14354
|
+
color: textColor,
|
|
14355
|
+
...props,
|
|
14356
|
+
});
|
|
14357
|
+
// The input's own ref, merged with the forwarded one, as Checkbox does,
|
|
14358
|
+
// so the mixed state can be set on it.
|
|
14359
|
+
const inputRef = React.useRef(null);
|
|
14360
|
+
const setRefs = React.useCallback((node) => {
|
|
14361
|
+
inputRef.current = node;
|
|
14362
|
+
if (typeof ref === 'function')
|
|
14363
|
+
return ref(node);
|
|
14364
|
+
if (ref)
|
|
14365
|
+
ref.current = node;
|
|
14366
|
+
}, [ref]);
|
|
14367
|
+
// `indeterminate` is a DOM property with no attribute, and a click clears
|
|
14368
|
+
// it, so it's set after every render while the prop is set, and cleared
|
|
14369
|
+
// once when the prop goes back to unset. Checkbox does the same.
|
|
14370
|
+
const ownsIndeterminateRef = React.useRef(false);
|
|
13126
14371
|
useClientLayoutEffect(() => {
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
}, [targetId, settle]);
|
|
13131
|
-
const renderedTargetId = forTaken ? targetId : undefined;
|
|
13132
|
-
let renderedLabel = null;
|
|
13133
|
-
if (label) {
|
|
13134
|
-
if (horizontal) {
|
|
13135
|
-
renderedLabel = (jsxRuntime.jsx(FieldLabel, { size: effectiveLabelSize, children: jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: labelProps?.style, children: label }) }));
|
|
13136
|
-
}
|
|
13137
|
-
else {
|
|
13138
|
-
renderedLabel = (jsxRuntime.jsx("label", { htmlFor: renderedTargetId, ...labelProps, id: labelId, className: classNames(labelClass, labelProps?.className), style: { display: 'block', ...(labelProps?.style || {}) }, children: label }));
|
|
14372
|
+
if (indeterminate !== undefined) {
|
|
14373
|
+
ownsIndeterminateRef.current = true;
|
|
14374
|
+
inputRef.current.indeterminate = indeterminate;
|
|
13139
14375
|
}
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
// like <Field.Label/> + <Field.Body/>).
|
|
13144
|
-
let content = children;
|
|
13145
|
-
if (horizontal) {
|
|
13146
|
-
const isFieldBody = (c) => React.isValidElement(c) &&
|
|
13147
|
-
// @ts-expect-error displayName isn't on the public type
|
|
13148
|
-
(c.type === FieldBody || c.type?.displayName === 'FieldBody');
|
|
13149
|
-
const isFieldLabel = (c) => React.isValidElement(c) &&
|
|
13150
|
-
// @ts-expect-error displayName isn't on the public type
|
|
13151
|
-
(c.type === FieldLabel || c.type?.displayName === 'FieldLabel');
|
|
13152
|
-
const childArray = React.Children.toArray(children);
|
|
13153
|
-
const userProvidedStructure = childArray.some(c => isFieldBody(c) || isFieldLabel(c));
|
|
13154
|
-
if (userProvidedStructure) {
|
|
13155
|
-
content = children;
|
|
14376
|
+
else if (ownsIndeterminateRef.current) {
|
|
14377
|
+
ownsIndeterminateRef.current = false;
|
|
14378
|
+
inputRef.current.indeterminate = false;
|
|
13156
14379
|
}
|
|
13157
|
-
|
|
13158
|
-
|
|
14380
|
+
});
|
|
14381
|
+
// A surrounding <Checkboxes> hands down its name and, when it manages
|
|
14382
|
+
// the selection, whether this card's value is in it. The card's own
|
|
14383
|
+
// props win, as they do on Checkbox.
|
|
14384
|
+
const group = useCheckboxesGroup();
|
|
14385
|
+
const effectiveName = name ?? group?.name;
|
|
14386
|
+
const groupManaged = group?.value !== undefined && value !== undefined;
|
|
14387
|
+
const effectiveChecked = checked !== undefined
|
|
14388
|
+
? checked
|
|
14389
|
+
: groupManaged
|
|
14390
|
+
? group.value.includes(String(value))
|
|
14391
|
+
: undefined;
|
|
14392
|
+
const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
|
|
14393
|
+
const handleChange = React.useCallback((e) => {
|
|
14394
|
+
if (indeterminate !== undefined)
|
|
14395
|
+
e.target.indeterminate = indeterminate;
|
|
14396
|
+
onChange?.(e);
|
|
14397
|
+
if (group?.onChange && value !== undefined) {
|
|
14398
|
+
const valStr = String(value);
|
|
14399
|
+
const current = group.value ?? [];
|
|
14400
|
+
group.onChange(e.target.checked
|
|
14401
|
+
? current.includes(valStr)
|
|
14402
|
+
? current
|
|
14403
|
+
: [...current, valStr]
|
|
14404
|
+
: current.filter(v => v !== valStr));
|
|
13159
14405
|
}
|
|
13160
|
-
}
|
|
13161
|
-
|
|
13162
|
-
|
|
13163
|
-
|
|
13164
|
-
|
|
13165
|
-
const
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
14406
|
+
}, [onChange, group, value, indeterminate]);
|
|
14407
|
+
// The title names the input and the description describes it. A name
|
|
14408
|
+
// passed in wins, and without one of either the label's own text names
|
|
14409
|
+
// the input, description included, so it isn't described again.
|
|
14410
|
+
const titleId = React.useId();
|
|
14411
|
+
const descriptionId = React.useId();
|
|
14412
|
+
const ariaLabel = rest['aria-label'];
|
|
14413
|
+
const ariaLabelledBy = rest['aria-labelledby'] || (!ariaLabel && title ? titleId : undefined);
|
|
14414
|
+
const ariaDescribedBy = rest['aria-describedby'] ||
|
|
14415
|
+
(description && (ariaLabel || ariaLabelledBy)
|
|
14416
|
+
? descriptionId
|
|
14417
|
+
: undefined);
|
|
14418
|
+
const cardClass = usePrefixedClassNames('checkbox-card', 'card', {
|
|
14419
|
+
[`is-${color}`]: color && checkboxCardColors.includes(color),
|
|
14420
|
+
});
|
|
14421
|
+
const contentClass = usePrefixedClassNames('card-content', 'checkbox-card-content');
|
|
14422
|
+
const checkClass = usePrefixedClassNames('checkbox-card-check');
|
|
14423
|
+
const bodyClass = usePrefixedClassNames('checkbox-card-body');
|
|
14424
|
+
const titleClass = usePrefixedClassNames('checkbox-card-title');
|
|
14425
|
+
const descriptionClass = usePrefixedClassNames('checkbox-card-description');
|
|
14426
|
+
return (jsxRuntime.jsxs("label", { className: classNames(cardClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("input", { ref: setRefs, type: "checkbox", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy }), jsxRuntime.jsxs("span", { className: contentClass, children: [hasIndicator && jsxRuntime.jsx("span", { className: checkClass }), jsxRuntime.jsxs("span", { className: bodyClass, children: [title ? (jsxRuntime.jsx("span", { id: titleId, className: titleClass, children: title })) : null, description ? (jsxRuntime.jsx("span", { id: descriptionId, className: descriptionClass, children: description })) : null, children] })] })] }));
|
|
14427
|
+
});
|
|
14428
|
+
CheckboxCard.displayName = 'CheckboxCard';
|
|
13170
14429
|
|
|
13171
14430
|
/**
|
|
13172
|
-
*
|
|
13173
|
-
*
|
|
13174
|
-
*
|
|
13175
|
-
*
|
|
13176
|
-
*
|
|
13177
|
-
*
|
|
13178
|
-
* a labeled Field around it offers, the way the bases do, so that Field's
|
|
13179
|
-
* label names it (#939); a user `id` still wins there. It tells that Field
|
|
13180
|
-
* when the control took its id, since the Field's label drops a `for`
|
|
13181
|
-
* nothing took (#1004). With `needsLabelId` it also returns the id of
|
|
13182
|
-
* whichever label names the control (#981).
|
|
13183
|
-
* Internal; not part of the public API.
|
|
13184
|
-
*/
|
|
13185
|
-
function useAutoLabelId({ label, id, labelProps, rendersLabel, hasInput = true, needsLabelId = false, }) {
|
|
13186
|
-
// Called unconditionally per the rules of hooks; SSR-safe on React 18 and 19.
|
|
13187
|
-
const generatedId = React.useId();
|
|
13188
|
-
// Read outside the control's own Field, so it is set only when an outer
|
|
13189
|
-
// Field holds the control, which then renders no label of its own.
|
|
13190
|
-
const fieldLabelId = useFieldLabelId();
|
|
13191
|
-
// What an outer Field's label points at, even when wired by hand, and the
|
|
13192
|
-
// id that label renders with, read together so the id is that label's.
|
|
13193
|
-
// Set in `grouped`/`hasAddons` Fields too when the label is wired by hand,
|
|
13194
|
-
// since its `for` names this one control whatever the layout (see Field).
|
|
13195
|
-
const fieldLabelFor = useFieldLabelFor();
|
|
13196
|
-
// Truthiness mirrors Field's own `if (label)` render gate.
|
|
13197
|
-
const active = !!label && rendersLabel;
|
|
13198
|
-
// Presence, not truthiness: `htmlFor: undefined` is an explicit opt-out and
|
|
13199
|
-
// must not leave an orphan generated id on the control.
|
|
13200
|
-
const userWired = !!labelProps && 'htmlFor' in labelProps;
|
|
13201
|
-
const adopted = !active && hasInput ? fieldLabelId : undefined;
|
|
13202
|
-
const controlId = id ?? (active && !userWired ? generatedId : adopted);
|
|
13203
|
-
// Tells a labeled Field around the control when its label's `for` names
|
|
13204
|
-
// the control, since it drops a `for` nothing took (#1004).
|
|
13205
|
-
useReportFieldLabelFor(fieldLabelId !== undefined && controlId === fieldLabelId);
|
|
13206
|
-
const ownLabelProps = { htmlFor: controlId, ...labelProps };
|
|
13207
|
-
const labelTarget = active ? ownLabelProps.htmlFor : fieldLabelFor.htmlFor;
|
|
13208
|
-
const labelled = !!controlId && labelTarget === controlId;
|
|
13209
|
-
// Only a label that names the control gets an id to point at, with the
|
|
13210
|
-
// suffix Field gives its own, from an id that is already unique.
|
|
13211
|
-
const ownLabelId = needsLabelId && active && labelled
|
|
13212
|
-
? (labelProps?.id ?? `${generatedId}-label`)
|
|
13213
|
-
: undefined;
|
|
13214
|
-
// Inactive with a label still means an own Field may render it (pickers'
|
|
13215
|
-
// inline mode). The explicit `htmlFor: undefined` tells Field the
|
|
13216
|
-
// association is owned here, so it must not generate one that would
|
|
13217
|
-
// dangle (#495 presence semantics).
|
|
13218
|
-
const fieldLabelProps = active
|
|
13219
|
-
? ownLabelId
|
|
13220
|
-
? { ...ownLabelProps, id: ownLabelId }
|
|
13221
|
-
: ownLabelProps
|
|
13222
|
-
: label
|
|
13223
|
-
? { htmlFor: undefined, ...labelProps }
|
|
13224
|
-
: labelProps;
|
|
13225
|
-
const labelId = needsLabelId && labelled
|
|
13226
|
-
? active
|
|
13227
|
-
? ownLabelId
|
|
13228
|
-
: fieldLabelFor.id
|
|
13229
|
-
: undefined;
|
|
13230
|
-
return { controlId, fieldLabelProps, labelled, labelId };
|
|
13231
|
-
}
|
|
13232
|
-
/**
|
|
13233
|
-
* Group-input counterpart of {@link useAutoLabelId} (#494): a group of
|
|
13234
|
-
* controls cannot take a single `htmlFor`, so instead the rendered `<label>`
|
|
13235
|
-
* gets a generated id and the group container points at it with
|
|
13236
|
-
* `aria-labelledby`. A user-supplied `labelProps.id` is used as the target
|
|
13237
|
-
* instead of generating one. Any caller `htmlFor` is stripped — a group label
|
|
13238
|
-
* names the group, never a single control — so the merged labelProps always
|
|
13239
|
-
* carry an explicit `htmlFor: undefined`. When the group renders no label of
|
|
13240
|
-
* its own, it points at the label of a labeled Field around it instead
|
|
13241
|
-
* (#939). A caller who named the group with `aria-label` or
|
|
13242
|
-
* `aria-labelledby` gets no `aria-labelledby` from either label, so their
|
|
13243
|
-
* name wins wherever the label sits.
|
|
13244
|
-
* Internal; not part of the public API.
|
|
14431
|
+
* The `Field` a `Checkboxes` or `Radios` with `as="fieldset"` renders of its
|
|
14432
|
+
* own, as a native `<fieldset>`: Bulma's field markup, with its label as the
|
|
14433
|
+
* `<legend>` (inside the `field-label` column when horizontal), so the legend
|
|
14434
|
+
* names the group and needs no id. It holds a `Field`'s place for what is
|
|
14435
|
+
* inside, so the choices read as inside a `Field`. Internal; not part of the
|
|
14436
|
+
* public API.
|
|
13245
14437
|
*/
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
const
|
|
13251
|
-
const
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
?
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
return { ariaLabelledBy, fieldLabelProps };
|
|
13269
|
-
}
|
|
14438
|
+
const GroupFieldset = ({ label, labelSize, labelProps, horizontal, fieldClassName, role, attributes, message, children, }) => {
|
|
14439
|
+
const fieldClass = usePrefixedClassNames('field', {
|
|
14440
|
+
'is-horizontal': !!horizontal,
|
|
14441
|
+
});
|
|
14442
|
+
const labelClass = usePrefixedClassNames('label');
|
|
14443
|
+
const fieldLabelClass = usePrefixedClassNames('field-label', {
|
|
14444
|
+
[`is-${labelSize || 'normal'}`]: true,
|
|
14445
|
+
});
|
|
14446
|
+
const fieldBodyClass = usePrefixedClassNames('field-body');
|
|
14447
|
+
// A legend names its fieldset with no `for`, so a `htmlFor` has nothing
|
|
14448
|
+
// to point at.
|
|
14449
|
+
const { htmlFor: _htmlFor, className: labelClassName, ...ownLabelProps } = labelProps ?? {};
|
|
14450
|
+
const shownLabel = {
|
|
14451
|
+
...ownLabelProps,
|
|
14452
|
+
className: classNames(labelClass, labelClassName),
|
|
14453
|
+
};
|
|
14454
|
+
let legend = null;
|
|
14455
|
+
if (label) {
|
|
14456
|
+
legend = horizontal ? (jsxRuntime.jsx("legend", { className: fieldLabelClass, children: jsxRuntime.jsx("span", { ...shownLabel, children: label }) })) : (jsxRuntime.jsx("legend", { ...shownLabel, children: label }));
|
|
14457
|
+
}
|
|
14458
|
+
return (jsxRuntime.jsx(FieldProvider, { value: true, children: jsxRuntime.jsxs("fieldset", { className: classNames(fieldClass, fieldClassName), role: role, ...attributes, children: [legend, horizontal ? (jsxRuntime.jsxs("div", { className: fieldBodyClass, children: [children, message] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [children, message] }))] }) }));
|
|
14459
|
+
};
|
|
13270
14460
|
|
|
13271
14461
|
/**
|
|
13272
14462
|
* The `Checkboxes` component wraps multiple `Checkbox` components in a Bulma-styled group.
|
|
@@ -13293,7 +14483,7 @@ function useAutoLabelledBy({ label, labelProps, rendersLabel, callerProps, }) {
|
|
|
13293
14483
|
* <Checkbox value="angular">Angular</Checkbox>
|
|
13294
14484
|
* </Checkboxes>
|
|
13295
14485
|
*/
|
|
13296
|
-
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
14486
|
+
const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, as, ...props }) => {
|
|
13297
14487
|
const insideField = useInsideField();
|
|
13298
14488
|
const insideControl = useInsideControl();
|
|
13299
14489
|
const ownField = rendersOwnField('Checkboxes', {
|
|
@@ -13304,10 +14494,14 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
13304
14494
|
horizontal,
|
|
13305
14495
|
fieldClassName,
|
|
13306
14496
|
});
|
|
14497
|
+
// As a fieldset with a Field of its own, the group's label is its legend,
|
|
14498
|
+
// which names it with no id, so nothing points `aria-labelledby` at it.
|
|
14499
|
+
const asFieldset = as === 'fieldset';
|
|
14500
|
+
const fieldsetField = asFieldset && ownField;
|
|
13307
14501
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
13308
14502
|
label,
|
|
13309
14503
|
labelProps,
|
|
13310
|
-
rendersLabel: ownField,
|
|
14504
|
+
rendersLabel: ownField && !fieldsetField,
|
|
13311
14505
|
callerProps: props,
|
|
13312
14506
|
});
|
|
13313
14507
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -13332,11 +14526,17 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
13332
14526
|
name,
|
|
13333
14527
|
...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
|
|
13334
14528
|
}), [name, groupActive, currentValue, handleChange]);
|
|
13335
|
-
const
|
|
14529
|
+
const choices = (jsxRuntime.jsx(CheckboxesProvider, { value: ctx, children: children }));
|
|
14530
|
+
// A fieldset is a group already, so it takes no role of its own.
|
|
14531
|
+
const Group = asFieldset ? 'fieldset' : 'div';
|
|
14532
|
+
const checkboxesElement = fieldsetField ? (jsxRuntime.jsx("div", { className: wrapperClass, children: choices })) : (jsxRuntime.jsx(Group, { className: wrapperClass, role: asFieldset ? undefined : 'group', "aria-labelledby": ariaLabelledBy, ...rest, children: choices }));
|
|
13336
14533
|
let content = checkboxesElement;
|
|
13337
14534
|
if (rendersOwnControl('Checkboxes', { insideControl })) {
|
|
13338
14535
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
13339
14536
|
}
|
|
14537
|
+
if (fieldsetField) {
|
|
14538
|
+
return (jsxRuntime.jsx(GroupFieldset, { label: label, labelSize: labelSize, labelProps: labelProps, horizontal: horizontal, fieldClassName: fieldClassName, attributes: rest, message: messageEl, children: content }));
|
|
14539
|
+
}
|
|
13340
14540
|
if (ownField) {
|
|
13341
14541
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13342
14542
|
}
|
|
@@ -13513,36 +14713,112 @@ const Radio = React.forwardRef(({ color, size, className, children, textColor, d
|
|
|
13513
14713
|
// Local props always win over the group (explicit > implicit).
|
|
13514
14714
|
const group = useRadiosGroup();
|
|
13515
14715
|
const effectiveName = name ?? group?.name;
|
|
13516
|
-
// Group-managed checked state — only when the group is in
|
|
13517
|
-
// controlled/uncontrolled mode (group.value is defined) AND this Radio
|
|
13518
|
-
// has a value to compare against. Local `checked` always wins.
|
|
14716
|
+
// Group-managed checked state — only when the group is in
|
|
14717
|
+
// controlled/uncontrolled mode (group.value is defined) AND this Radio
|
|
14718
|
+
// has a value to compare against. Local `checked` always wins.
|
|
14719
|
+
const groupManaged = group?.value !== undefined && value !== undefined;
|
|
14720
|
+
const effectiveChecked = checked !== undefined
|
|
14721
|
+
? checked
|
|
14722
|
+
: groupManaged
|
|
14723
|
+
? group.value === value
|
|
14724
|
+
: undefined;
|
|
14725
|
+
// When the group manages checked, suppress local defaultChecked to avoid
|
|
14726
|
+
// the controlled/uncontrolled React warning.
|
|
14727
|
+
const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
|
|
14728
|
+
const handleChange = React.useCallback((e) => {
|
|
14729
|
+
// Local onChange always fires.
|
|
14730
|
+
onChange?.(e);
|
|
14731
|
+
// Dispatch to the group additionally — keeps group state in sync.
|
|
14732
|
+
if (group?.onChange && value !== undefined) {
|
|
14733
|
+
group.onChange(String(value));
|
|
14734
|
+
}
|
|
14735
|
+
}, [onChange, group, value]);
|
|
14736
|
+
const mainClass = usePrefixedClassNames('styled-radio', 'radio', {
|
|
14737
|
+
[`is-${color}`]: color && radioColors.includes(color),
|
|
14738
|
+
[`is-${size}`]: size && radioSizes.includes(size),
|
|
14739
|
+
});
|
|
14740
|
+
const radioClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
14741
|
+
const checkClass = usePrefixedClassNames('check');
|
|
14742
|
+
const controlLabelClass = usePrefixedClassNames('control-label');
|
|
14743
|
+
return (jsxRuntime.jsxs("label", { className: radioClass, children: [jsxRuntime.jsx("input", { ref: ref, type: "radio", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest }), jsxRuntime.jsx("span", { className: checkClass }), children && jsxRuntime.jsx("span", { className: controlLabelClass, children: children })] }));
|
|
14744
|
+
});
|
|
14745
|
+
Radio.displayName = 'Radio';
|
|
14746
|
+
|
|
14747
|
+
// The colors `Radio` takes, as `radioColors` lists them. Declared here as
|
|
14748
|
+
// well, so the API page can read them off this module, and a type test holds
|
|
14749
|
+
// the two lists to each other.
|
|
14750
|
+
const radioCardColors = [
|
|
14751
|
+
'primary',
|
|
14752
|
+
'link',
|
|
14753
|
+
'info',
|
|
14754
|
+
'success',
|
|
14755
|
+
'warning',
|
|
14756
|
+
'danger',
|
|
14757
|
+
];
|
|
14758
|
+
/**
|
|
14759
|
+
* The `RadioCard` component is a radio button drawn as a card, with a title and a description, picked wherever you click it.
|
|
14760
|
+
*
|
|
14761
|
+
* It's a `<label>` around a real radio, so it sends its `value` with a form,
|
|
14762
|
+
* and the cards that share a `name` are one choice the arrow keys move
|
|
14763
|
+
* through. The title names the radio and the description describes it.
|
|
14764
|
+
* Inside `Radios` it takes the group's name and selection, as `Radio` does.
|
|
14765
|
+
*
|
|
14766
|
+
* @function
|
|
14767
|
+
* @param {RadioCardProps} props - Props for the RadioCard component.
|
|
14768
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the `<input>`.
|
|
14769
|
+
* @returns {JSX.Element} The rendered card.
|
|
14770
|
+
*
|
|
14771
|
+
* @example
|
|
14772
|
+
* <Radios label="Shipping" name="shipping" defaultValue="standard">
|
|
14773
|
+
* <RadioCard value="standard" title="Standard" description="Arrives in 4 to 6 days." />
|
|
14774
|
+
* <RadioCard value="express" title="Express" description="Arrives tomorrow." />
|
|
14775
|
+
* </Radios>
|
|
14776
|
+
*/
|
|
14777
|
+
const RadioCard = React.forwardRef(({ title, description, children, hasIndicator = true, color, className, textColor, disabled, name, value, checked, defaultChecked, onChange, ...props }, ref) => {
|
|
14778
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14779
|
+
color: textColor,
|
|
14780
|
+
...props,
|
|
14781
|
+
});
|
|
14782
|
+
// A surrounding <Radios> hands down its name and, when it manages the
|
|
14783
|
+
// selection, whether this card is the one picked. The card's own props
|
|
14784
|
+
// win, as they do on Radio.
|
|
14785
|
+
const group = useRadiosGroup();
|
|
14786
|
+
const effectiveName = name ?? group?.name;
|
|
13519
14787
|
const groupManaged = group?.value !== undefined && value !== undefined;
|
|
13520
14788
|
const effectiveChecked = checked !== undefined
|
|
13521
14789
|
? checked
|
|
13522
14790
|
: groupManaged
|
|
13523
14791
|
? group.value === value
|
|
13524
14792
|
: undefined;
|
|
13525
|
-
// When the group manages checked, suppress local defaultChecked to avoid
|
|
13526
|
-
// the controlled/uncontrolled React warning.
|
|
13527
14793
|
const effectiveDefaultChecked = groupManaged ? undefined : defaultChecked;
|
|
13528
14794
|
const handleChange = React.useCallback((e) => {
|
|
13529
|
-
// Local onChange always fires.
|
|
13530
14795
|
onChange?.(e);
|
|
13531
|
-
// Dispatch to the group additionally — keeps group state in sync.
|
|
13532
14796
|
if (group?.onChange && value !== undefined) {
|
|
13533
14797
|
group.onChange(String(value));
|
|
13534
14798
|
}
|
|
13535
14799
|
}, [onChange, group, value]);
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
13539
|
-
|
|
13540
|
-
const
|
|
13541
|
-
const
|
|
13542
|
-
const
|
|
13543
|
-
|
|
14800
|
+
// The title names the input and the description describes it. A name
|
|
14801
|
+
// passed in wins, and without one of either the label's own text names
|
|
14802
|
+
// the input, description included, so it isn't described again.
|
|
14803
|
+
const titleId = React.useId();
|
|
14804
|
+
const descriptionId = React.useId();
|
|
14805
|
+
const ariaLabel = rest['aria-label'];
|
|
14806
|
+
const ariaLabelledBy = rest['aria-labelledby'] || (!ariaLabel && title ? titleId : undefined);
|
|
14807
|
+
const ariaDescribedBy = rest['aria-describedby'] ||
|
|
14808
|
+
(description && (ariaLabel || ariaLabelledBy)
|
|
14809
|
+
? descriptionId
|
|
14810
|
+
: undefined);
|
|
14811
|
+
const cardClass = usePrefixedClassNames('radio-card', 'card', {
|
|
14812
|
+
[`is-${color}`]: color && radioCardColors.includes(color),
|
|
14813
|
+
});
|
|
14814
|
+
const contentClass = usePrefixedClassNames('card-content', 'radio-card-content');
|
|
14815
|
+
const checkClass = usePrefixedClassNames('radio-card-check');
|
|
14816
|
+
const bodyClass = usePrefixedClassNames('radio-card-body');
|
|
14817
|
+
const titleClass = usePrefixedClassNames('radio-card-title');
|
|
14818
|
+
const descriptionClass = usePrefixedClassNames('radio-card-description');
|
|
14819
|
+
return (jsxRuntime.jsxs("label", { className: classNames(cardClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("input", { ref: ref, type: "radio", disabled: disabled, name: effectiveName, value: value, checked: effectiveChecked, defaultChecked: effectiveDefaultChecked, onChange: handleChange, ...rest, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy }), jsxRuntime.jsxs("span", { className: contentClass, children: [hasIndicator && jsxRuntime.jsx("span", { className: checkClass }), jsxRuntime.jsxs("span", { className: bodyClass, children: [title ? (jsxRuntime.jsx("span", { id: titleId, className: titleClass, children: title })) : null, description ? (jsxRuntime.jsx("span", { id: descriptionId, className: descriptionClass, children: description })) : null, children] })] })] }));
|
|
13544
14820
|
});
|
|
13545
|
-
|
|
14821
|
+
RadioCard.displayName = 'RadioCard';
|
|
13546
14822
|
|
|
13547
14823
|
/**
|
|
13548
14824
|
* The `Radios` component wraps multiple `Radio` components in a Bulma-styled group.
|
|
@@ -13568,7 +14844,7 @@ Radio.displayName = 'Radio';
|
|
|
13568
14844
|
* <Radio value="green">Green</Radio>
|
|
13569
14845
|
* </Radios>
|
|
13570
14846
|
*/
|
|
13571
|
-
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, ...props }) => {
|
|
14847
|
+
const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, name, value, defaultValue, onChange, children, className, as, ...props }) => {
|
|
13572
14848
|
const insideField = useInsideField();
|
|
13573
14849
|
const insideControl = useInsideControl();
|
|
13574
14850
|
const ownField = rendersOwnField('Radios', {
|
|
@@ -13579,10 +14855,14 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
13579
14855
|
horizontal,
|
|
13580
14856
|
fieldClassName,
|
|
13581
14857
|
});
|
|
14858
|
+
// As a fieldset with a Field of its own, the group's label is its legend,
|
|
14859
|
+
// which names it with no id, so nothing points `aria-labelledby` at it.
|
|
14860
|
+
const asFieldset = as === 'fieldset';
|
|
14861
|
+
const fieldsetField = asFieldset && ownField;
|
|
13582
14862
|
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
13583
14863
|
label,
|
|
13584
14864
|
labelProps,
|
|
13585
|
-
rendersLabel: ownField,
|
|
14865
|
+
rendersLabel: ownField && !fieldsetField,
|
|
13586
14866
|
callerProps: props,
|
|
13587
14867
|
});
|
|
13588
14868
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
@@ -13610,11 +14890,17 @@ const RadiosComponent = ({ label, labelSize, labelProps, horizontal, message, me
|
|
|
13610
14890
|
name,
|
|
13611
14891
|
...(groupActive ? { value: currentValue, onChange: handleChange } : {}),
|
|
13612
14892
|
}), [name, groupActive, currentValue, handleChange]);
|
|
13613
|
-
const
|
|
14893
|
+
const choices = jsxRuntime.jsx(RadiosProvider, { value: ctx, children: children });
|
|
14894
|
+
// ARIA in HTML allows `radiogroup` on a fieldset, so it keeps the role.
|
|
14895
|
+
const Group = asFieldset ? 'fieldset' : 'div';
|
|
14896
|
+
const radiosElement = fieldsetField ? (jsxRuntime.jsx("div", { className: wrapperClass, children: choices })) : (jsxRuntime.jsx(Group, { className: wrapperClass, role: "radiogroup", "aria-labelledby": ariaLabelledBy, ...rest, children: choices }));
|
|
13614
14897
|
let content = radiosElement;
|
|
13615
14898
|
if (rendersOwnControl('Radios', { insideControl })) {
|
|
13616
14899
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
13617
14900
|
}
|
|
14901
|
+
if (fieldsetField) {
|
|
14902
|
+
return (jsxRuntime.jsx(GroupFieldset, { label: label, labelSize: labelSize, labelProps: labelProps, horizontal: horizontal, fieldClassName: fieldClassName, role: "radiogroup", attributes: rest, message: messageEl, children: content }));
|
|
14903
|
+
}
|
|
13618
14904
|
if (ownField) {
|
|
13619
14905
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
13620
14906
|
}
|
|
@@ -14451,1190 +15737,758 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
14451
15737
|
const keep = (text) => (toAsciiDigits(text).match(new RegExp(allowed, 'g')) ?? [])
|
|
14452
15738
|
.join('')
|
|
14453
15739
|
.slice(0, length);
|
|
14454
|
-
const commit = (next) => {
|
|
14455
|
-
if (next === code)
|
|
14456
|
-
return;
|
|
14457
|
-
if (controlledValue === undefined)
|
|
14458
|
-
setInternalValue(next);
|
|
14459
|
-
onChange?.(next);
|
|
14460
|
-
if (next.length === length)
|
|
14461
|
-
onComplete?.(next);
|
|
14462
|
-
};
|
|
14463
|
-
const inputRef = React.useRef(null);
|
|
14464
|
-
const setRefs = React.useCallback((node) => {
|
|
14465
|
-
inputRef.current = node;
|
|
14466
|
-
if (typeof ref === 'function')
|
|
14467
|
-
return ref(node);
|
|
14468
|
-
if (ref)
|
|
14469
|
-
ref.current = node;
|
|
14470
|
-
}, [ref]);
|
|
14471
|
-
// The input's selection while it has focus, which the boxes draw.
|
|
14472
|
-
const [selection, setSelection] = React.useState(null);
|
|
14473
|
-
const follow = React.useCallback(() => {
|
|
14474
|
-
const input = inputRef.current;
|
|
14475
|
-
const next = getActiveElementInTree(input) === input
|
|
14476
|
-
? [input.selectionStart, input.selectionEnd]
|
|
14477
|
-
: null;
|
|
14478
|
-
setSelection(prev => prev?.[0] === next?.[0] && prev?.[1] === next?.[1] ? prev : next);
|
|
14479
|
-
}, []);
|
|
14480
|
-
// A new code moves the caret, and so can a value set from outside.
|
|
14481
|
-
useClientLayoutEffect(follow, [code, follow]);
|
|
14482
|
-
// React's `onSelect` never fires inside a shadow root, so the boxes
|
|
14483
|
-
// follow the browser's own `selectionchange`. A caret that moves fires it
|
|
14484
|
-
// at the input, and it bubbles to the document, but in a shadow root some
|
|
14485
|
-
// browsers fire it at the document alone and others at the input alone,
|
|
14486
|
-
// so both are followed.
|
|
14487
|
-
React.useEffect(() => {
|
|
14488
|
-
const input = inputRef.current;
|
|
14489
|
-
const doc = input.ownerDocument;
|
|
14490
|
-
input.addEventListener('selectionchange', follow);
|
|
14491
|
-
doc.addEventListener('selectionchange', follow);
|
|
14492
|
-
return () => {
|
|
14493
|
-
input.removeEventListener('selectionchange', follow);
|
|
14494
|
-
doc.removeEventListener('selectionchange', follow);
|
|
14495
|
-
};
|
|
14496
|
-
}, [follow]);
|
|
14497
|
-
const handleChange = (e) => {
|
|
14498
|
-
const input = e.target;
|
|
14499
|
-
const typed = input.value;
|
|
14500
|
-
const next = keep(typed);
|
|
14501
|
-
if (next !== typed) {
|
|
14502
|
-
// Drop what the code doesn't take before React sees the value, so
|
|
14503
|
-
// the caret stays where it was rather than jumping to the end.
|
|
14504
|
-
const caret = keep(typed.slice(0, input.selectionStart)).length;
|
|
14505
|
-
input.value = next;
|
|
14506
|
-
input.setSelectionRange(caret, caret);
|
|
14507
|
-
}
|
|
14508
|
-
commit(next);
|
|
14509
|
-
};
|
|
14510
|
-
// The browser cuts a paste to `maxLength` before anything else sees it,
|
|
14511
|
-
// so a code pasted as "123 456" would lose its last digit. What the code
|
|
14512
|
-
// takes is kept first, and it replaces the whole code, since what you
|
|
14513
|
-
// paste into a code field is the code.
|
|
14514
|
-
const handlePaste = (e) => {
|
|
14515
|
-
onPaste?.(e);
|
|
14516
|
-
if (e.defaultPrevented)
|
|
14517
|
-
return;
|
|
14518
|
-
e.preventDefault();
|
|
14519
|
-
const pasted = keep(e.clipboardData.getData('text'));
|
|
14520
|
-
if (pasted && !e.currentTarget.readOnly)
|
|
14521
|
-
commit(pasted);
|
|
14522
|
-
};
|
|
14523
|
-
const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
|
|
14524
|
-
const fieldClass = usePrefixedClassNames('pin-input-field');
|
|
14525
|
-
const helpClass = usePrefixedClassNames('help', {
|
|
14526
|
-
[`is-${messageColor}`]: !!messageColor,
|
|
14527
|
-
});
|
|
14528
|
-
// The boxes the selection covers: the one at a caret, or every selected
|
|
14529
|
-
// one. A caret after the last character marks the last box.
|
|
14530
|
-
const [start, end] = selection ?? [-1, -1];
|
|
14531
|
-
const first = Math.min(start, length - 1);
|
|
14532
|
-
const last = Math.max(first + 1, Math.min(end, length));
|
|
14533
|
-
const boxes = Array.from({ length }, (_, i) => {
|
|
14534
|
-
const char = code[i];
|
|
14535
|
-
const active = i >= first && i < last;
|
|
14536
|
-
return (jsxRuntime.jsx("div", { "aria-hidden": "true", className: prefixedClassNames(classPrefix, 'input', 'pin-input-box', {
|
|
14537
|
-
[`is-${color}`]: !!color,
|
|
14538
|
-
[`is-${size}`]: !!size,
|
|
14539
|
-
'is-rounded': isRounded,
|
|
14540
|
-
'is-focused': active,
|
|
14541
|
-
'has-caret': active && start === end && !char,
|
|
14542
|
-
'is-disabled': disabled,
|
|
14543
|
-
}), children: char && (isMasked ? MASK : char) }, i));
|
|
14544
|
-
});
|
|
14545
|
-
// Left to right on a right-to-left page too: a code is written that way,
|
|
14546
|
-
// so its first character stays in the first box on the left, and the
|
|
14547
|
-
// arrow keys move the caret the way the boxes run.
|
|
14548
|
-
let content = (jsxRuntime.jsxs("div", { className: rootClass, dir: "ltr", children: [jsxRuntime.jsx("input", { ref: setRefs, className: fieldClass, type: isMasked ? 'password' : 'text', inputMode: type === 'alphanumeric' ? 'text' : 'numeric', autoComplete: "one-time-code", autoCorrect: "off", spellCheck: false,
|
|
14549
|
-
// A partial code fails constraint validation. `minLength` would
|
|
14550
|
-
// say so only after a keystroke, never after a paste or a filtered
|
|
14551
|
-
// character, which set the value from script.
|
|
14552
|
-
pattern: `${allowed}{${length}}`, ...rest, value: code, maxLength: length, disabled: disabled, onChange: handleChange, onPaste: handlePaste, onFocus: e => {
|
|
14553
|
-
follow();
|
|
14554
|
-
onFocus?.(e);
|
|
14555
|
-
}, onBlur: e => {
|
|
14556
|
-
setSelection(null);
|
|
14557
|
-
onBlur?.(e);
|
|
14558
|
-
},
|
|
14559
|
-
// After the spread: an undefined `id` key in rest would wipe it.
|
|
14560
|
-
id: controlId }), boxes] }));
|
|
14561
|
-
if (ownControl) {
|
|
14562
|
-
content = jsxRuntime.jsx(Control, { children: content });
|
|
14563
|
-
}
|
|
14564
|
-
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
14565
|
-
if (ownField) {
|
|
14566
|
-
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14567
|
-
}
|
|
14568
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
14569
|
-
});
|
|
14570
|
-
PinInput.displayName = 'PinInput';
|
|
14571
|
-
|
|
14572
|
-
/**
|
|
14573
|
-
* Default star icon component.
|
|
14574
|
-
*/
|
|
14575
|
-
const StarIcon = ({ filled }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: filled ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: filled ? 0 : 2, "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }) }));
|
|
14576
|
-
/** Snap a value to the nearest precision step. */
|
|
14577
|
-
function snapToPrecision(val, precision) {
|
|
14578
|
-
return Math.round(val / precision) * precision;
|
|
14579
|
-
}
|
|
14580
|
-
/** Get fill percentage for a given icon index based on current value. */
|
|
14581
|
-
function getFillPercent(iconIndex, value) {
|
|
14582
|
-
if (value >= iconIndex + 1)
|
|
14583
|
-
return 100;
|
|
14584
|
-
if (value <= iconIndex)
|
|
14585
|
-
return 0;
|
|
14586
|
-
return Math.round((value - iconIndex) * 100);
|
|
14587
|
-
}
|
|
14588
|
-
/**
|
|
14589
|
-
* The `Rate` component provides a star/icon-based rating system.
|
|
14590
|
-
*
|
|
14591
|
-
* @function
|
|
14592
|
-
* @param {RateProps} props - Props for the Rate component.
|
|
14593
|
-
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the container element.
|
|
14594
|
-
* @returns {JSX.Element} The rendered rating component.
|
|
14595
|
-
*
|
|
14596
|
-
* @example
|
|
14597
|
-
* // Basic 5-star rating
|
|
14598
|
-
* <Rate defaultValue={3} />
|
|
14599
|
-
*
|
|
14600
|
-
* @example
|
|
14601
|
-
* // With color and precision
|
|
14602
|
-
* <Rate defaultValue={3.5} color="warning" precision={0.5} showScore />
|
|
14603
|
-
*
|
|
14604
|
-
* @example
|
|
14605
|
-
* // With Font Awesome icons
|
|
14606
|
-
* <Rate iconName="star" iconLibrary="fa" iconVariant="solid" />
|
|
14607
|
-
*/
|
|
14608
|
-
const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
|
|
14609
|
-
const insideField = useInsideField();
|
|
14610
|
-
const insideControl = useInsideControl();
|
|
14611
|
-
const ownField = rendersOwnField('Rate', {
|
|
14612
|
-
insideField,
|
|
14613
|
-
insideControl,
|
|
14614
|
-
label,
|
|
14615
|
-
message,
|
|
14616
|
-
horizontal,
|
|
14617
|
-
fieldClassName,
|
|
14618
|
-
});
|
|
14619
|
-
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
14620
|
-
label,
|
|
14621
|
-
labelProps,
|
|
14622
|
-
rendersLabel: ownField,
|
|
14623
|
-
callerProps: props,
|
|
14624
|
-
});
|
|
14625
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
14626
|
-
const starBaseId = React.useId();
|
|
14627
|
-
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
14628
|
-
const [hoverValue, setHoverValue] = React.useState(null);
|
|
14629
|
-
const defaultIconLibrary = useIconLibrary();
|
|
14630
|
-
const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
|
|
14631
|
-
// Determine if controlled
|
|
14632
|
-
const isControlled = controlledValue !== undefined;
|
|
14633
|
-
const currentValue = isControlled ? controlledValue : internalValue;
|
|
14634
|
-
const displayValue = hoverValue !== null ? hoverValue : currentValue;
|
|
14635
|
-
// Update value
|
|
14636
|
-
const updateValue = React.useCallback((newValue) => {
|
|
14637
|
-
if (disabled)
|
|
14638
|
-
return;
|
|
14639
|
-
// Allow clicking same value to deselect (set to 0)
|
|
14640
|
-
const finalValue = newValue === currentValue ? 0 : newValue;
|
|
14641
|
-
if (!isControlled) {
|
|
14642
|
-
setInternalValue(finalValue);
|
|
14643
|
-
}
|
|
14644
|
-
onChange?.(finalValue);
|
|
14645
|
-
}, [isControlled, currentValue, disabled, onChange]);
|
|
14646
|
-
// Handle click with precision support
|
|
14647
|
-
const handleClick = React.useCallback((iconIndex, e) => {
|
|
14648
|
-
if (disabled)
|
|
14649
|
-
return;
|
|
14650
|
-
if (precision < 1) {
|
|
14651
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
14652
|
-
let relativeX = (e.clientX - rect.left) / rect.width;
|
|
14653
|
-
if (rtl)
|
|
14654
|
-
relativeX = 1 - relativeX;
|
|
14655
|
-
const rawValue = iconIndex + relativeX;
|
|
14656
|
-
const snapped = snapToPrecision(rawValue, precision);
|
|
14657
|
-
const clamped = Math.max(precision, Math.min(snapped, max));
|
|
14658
|
-
updateValue(clamped);
|
|
14659
|
-
}
|
|
14660
|
-
else {
|
|
14661
|
-
updateValue(iconIndex + 1);
|
|
14662
|
-
}
|
|
14663
|
-
}, [disabled, precision, rtl, max, updateValue]);
|
|
14664
|
-
// Handle mouse enter on icon
|
|
14665
|
-
const handleMouseEnter = React.useCallback((iconIndex) => {
|
|
14666
|
-
if (disabled)
|
|
14667
|
-
return;
|
|
14668
|
-
setHoverValue(iconIndex + 1);
|
|
14669
|
-
}, [disabled]);
|
|
14670
|
-
// Handle mouse move with precision support
|
|
14671
|
-
const handleMouseMove = React.useCallback((iconIndex, e) => {
|
|
14672
|
-
if (disabled || precision >= 1)
|
|
14673
|
-
return;
|
|
14674
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
14675
|
-
let relativeX = (e.clientX - rect.left) / rect.width;
|
|
14676
|
-
if (rtl)
|
|
14677
|
-
relativeX = 1 - relativeX;
|
|
14678
|
-
const rawValue = iconIndex + relativeX;
|
|
14679
|
-
const snapped = snapToPrecision(rawValue, precision);
|
|
14680
|
-
const clamped = Math.max(precision, Math.min(snapped, max));
|
|
14681
|
-
setHoverValue(clamped);
|
|
14682
|
-
}, [disabled, precision, rtl, max]);
|
|
14683
|
-
// Handle mouse leave
|
|
14684
|
-
const handleMouseLeave = React.useCallback(() => {
|
|
14685
|
-
setHoverValue(null);
|
|
14686
|
-
}, []);
|
|
14687
|
-
// Handle keyboard
|
|
14688
|
-
const handleKeyDown = React.useCallback((e) => {
|
|
14689
|
-
if (disabled)
|
|
15740
|
+
const commit = (next) => {
|
|
15741
|
+
if (next === code)
|
|
14690
15742
|
return;
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
}
|
|
14697
|
-
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
14698
|
-
e.preventDefault();
|
|
14699
|
-
newValue = Math.max(currentValue - step, 0);
|
|
14700
|
-
}
|
|
14701
|
-
else if (e.key === 'Home') {
|
|
14702
|
-
e.preventDefault();
|
|
14703
|
-
newValue = 0;
|
|
14704
|
-
}
|
|
14705
|
-
else if (e.key === 'End') {
|
|
14706
|
-
e.preventDefault();
|
|
14707
|
-
newValue = max;
|
|
14708
|
-
}
|
|
14709
|
-
// Round to avoid floating point issues
|
|
14710
|
-
newValue = Math.round(newValue * 1000) / 1000;
|
|
14711
|
-
if (newValue !== currentValue) {
|
|
14712
|
-
if (!isControlled) {
|
|
14713
|
-
setInternalValue(newValue);
|
|
14714
|
-
}
|
|
14715
|
-
onChange?.(newValue);
|
|
14716
|
-
}
|
|
14717
|
-
}, [currentValue, max, precision, disabled, isControlled, onChange]);
|
|
14718
|
-
// Get text for current value
|
|
14719
|
-
const getText = () => {
|
|
14720
|
-
if (!showText || !texts)
|
|
14721
|
-
return null;
|
|
14722
|
-
const wholeIndex = Math.ceil(displayValue) - 1;
|
|
14723
|
-
const baseText = texts[wholeIndex] || null;
|
|
14724
|
-
if (!baseText)
|
|
14725
|
-
return null;
|
|
14726
|
-
// For fractional hover values with precision < 1, append "+"
|
|
14727
|
-
if (precision < 1 && hoverValue !== null && hoverValue % 1 !== 0) {
|
|
14728
|
-
return `${baseText}+`;
|
|
14729
|
-
}
|
|
14730
|
-
return baseText;
|
|
14731
|
-
};
|
|
14732
|
-
// Format score display
|
|
14733
|
-
const getScoreDisplay = () => {
|
|
14734
|
-
if (precision < 1 && currentValue % 1 !== 0) {
|
|
14735
|
-
return currentValue.toFixed(1);
|
|
14736
|
-
}
|
|
14737
|
-
return currentValue;
|
|
15743
|
+
if (controlledValue === undefined)
|
|
15744
|
+
setInternalValue(next);
|
|
15745
|
+
onChange?.(next);
|
|
15746
|
+
if (next.length === length)
|
|
15747
|
+
onComplete?.(next);
|
|
14738
15748
|
};
|
|
14739
|
-
|
|
14740
|
-
const
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
const
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
const
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
15749
|
+
const inputRef = React.useRef(null);
|
|
15750
|
+
const setRefs = React.useCallback((node) => {
|
|
15751
|
+
inputRef.current = node;
|
|
15752
|
+
if (typeof ref === 'function')
|
|
15753
|
+
return ref(node);
|
|
15754
|
+
if (ref)
|
|
15755
|
+
ref.current = node;
|
|
15756
|
+
}, [ref]);
|
|
15757
|
+
// The input's selection while it has focus, which the boxes draw.
|
|
15758
|
+
const [selection, setSelection] = React.useState(null);
|
|
15759
|
+
const follow = React.useCallback(() => {
|
|
15760
|
+
const input = inputRef.current;
|
|
15761
|
+
const next = getActiveElementInTree(input) === input
|
|
15762
|
+
? [input.selectionStart, input.selectionEnd]
|
|
15763
|
+
: null;
|
|
15764
|
+
setSelection(prev => prev?.[0] === next?.[0] && prev?.[1] === next?.[1] ? prev : next);
|
|
15765
|
+
}, []);
|
|
15766
|
+
// A new code moves the caret, and so can a value set from outside.
|
|
15767
|
+
useClientLayoutEffect(follow, [code, follow]);
|
|
15768
|
+
// React's `onSelect` never fires inside a shadow root, so the boxes
|
|
15769
|
+
// follow the browser's own `selectionchange`. A caret that moves fires it
|
|
15770
|
+
// at the input, and it bubbles to the document, but in a shadow root some
|
|
15771
|
+
// browsers fire it at the document alone and others at the input alone,
|
|
15772
|
+
// so both are followed.
|
|
15773
|
+
React.useEffect(() => {
|
|
15774
|
+
const input = inputRef.current;
|
|
15775
|
+
const doc = input.ownerDocument;
|
|
15776
|
+
input.addEventListener('selectionchange', follow);
|
|
15777
|
+
doc.addEventListener('selectionchange', follow);
|
|
15778
|
+
return () => {
|
|
15779
|
+
input.removeEventListener('selectionchange', follow);
|
|
15780
|
+
doc.removeEventListener('selectionchange', follow);
|
|
14757
15781
|
};
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
// For custom icons, pass fillPercent and let consumer handle it
|
|
14770
|
-
const iconProps = {
|
|
14771
|
-
index: iconIndex,
|
|
14772
|
-
isActive: fillPercent > 0,
|
|
14773
|
-
isHovered: hoverValue !== null && iconIndex < (hoverValue ?? 0),
|
|
14774
|
-
value: displayValue,
|
|
14775
|
-
fillPercent,
|
|
14776
|
-
};
|
|
14777
|
-
return customIcon(iconProps);
|
|
14778
|
-
}
|
|
14779
|
-
if (iconName) {
|
|
14780
|
-
return (jsxRuntime.jsxs("span", { className: prefixedClassNames(classPrefix, 'rate-icon-partial'), style: {
|
|
14781
|
-
'--rate-fill-percent': `${fillPercent}%`,
|
|
14782
|
-
}, children: [jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-bg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) }), jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-fg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) })] }));
|
|
15782
|
+
}, [follow]);
|
|
15783
|
+
const handleChange = (e) => {
|
|
15784
|
+
const input = e.target;
|
|
15785
|
+
const typed = input.value;
|
|
15786
|
+
const next = keep(typed);
|
|
15787
|
+
if (next !== typed) {
|
|
15788
|
+
// Drop what the code doesn't take before React sees the value, so
|
|
15789
|
+
// the caret stays where it was rather than jumping to the end.
|
|
15790
|
+
const caret = keep(typed.slice(0, input.selectionStart)).length;
|
|
15791
|
+
input.value = next;
|
|
15792
|
+
input.setSelectionRange(caret, caret);
|
|
14783
15793
|
}
|
|
14784
|
-
|
|
14785
|
-
const clipId = `rate-clip-${iconIndex}`;
|
|
14786
|
-
return (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsxRuntime.jsx("defs", { children: jsxRuntime.jsx("clipPath", { id: clipId, children: jsxRuntime.jsx("rect", { x: "0", y: "0", width: `${(fillPercent / 100) * 24}`, height: "24" }) }) }), jsxRuntime.jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }), jsxRuntime.jsx("path", { clipPath: `url(#${clipId})`, fill: "currentColor", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" })] }));
|
|
15794
|
+
commit(next);
|
|
14787
15795
|
};
|
|
14788
|
-
// The
|
|
14789
|
-
//
|
|
14790
|
-
//
|
|
14791
|
-
//
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
const iconIndex = rtl ? max - 1 - i : i;
|
|
14801
|
-
const fillPercent = getFillPercent(iconIndex, displayValue);
|
|
14802
|
-
const isActive = fillPercent === 100;
|
|
14803
|
-
const isHovered = hoverValue !== null && iconIndex < hoverValue;
|
|
14804
|
-
const isPartial = fillPercent > 0 && fillPercent < 100;
|
|
14805
|
-
const icon = isPartial
|
|
14806
|
-
? renderPartialIcon(iconIndex, fillPercent)
|
|
14807
|
-
: renderIcon(iconIndex, isActive, fillPercent);
|
|
14808
|
-
icons.push(jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-item', {
|
|
14809
|
-
'is-active': isActive,
|
|
14810
|
-
'is-hovered': isHovered,
|
|
14811
|
-
}), onClick: e => handleClick(iconIndex, e), onMouseEnter: () => handleMouseEnter(iconIndex), onMouseMove: e => handleMouseMove(iconIndex, e), onMouseLeave: handleMouseLeave, id: `${starBaseId}-${iconIndex}`, role: "radio", "aria-checked": iconIndex === checkedIndex, "aria-label": iconIndex === checkedIndex && shownValue % 1 !== 0
|
|
14812
|
-
? `${shownValue} stars`
|
|
14813
|
-
: `${iconIndex + 1} star${iconIndex === 0 ? '' : 's'}`, tabIndex: -1, children: icon }, iconIndex));
|
|
14814
|
-
}
|
|
14815
|
-
return icons;
|
|
15796
|
+
// The browser cuts a paste to `maxLength` before anything else sees it,
|
|
15797
|
+
// so a code pasted as "123 456" would lose its last digit. What the code
|
|
15798
|
+
// takes is kept first, and it replaces the whole code, since what you
|
|
15799
|
+
// paste into a code field is the code.
|
|
15800
|
+
const handlePaste = (e) => {
|
|
15801
|
+
onPaste?.(e);
|
|
15802
|
+
if (e.defaultPrevented)
|
|
15803
|
+
return;
|
|
15804
|
+
e.preventDefault();
|
|
15805
|
+
const pasted = keep(e.clipboardData.getData('text'));
|
|
15806
|
+
if (pasted && !e.currentTarget.readOnly)
|
|
15807
|
+
commit(pasted);
|
|
14816
15808
|
};
|
|
14817
|
-
const
|
|
15809
|
+
const rootClass = classNames(usePrefixedClassNames('pin-input'), bulmaHelperClasses, className);
|
|
15810
|
+
const fieldClass = usePrefixedClassNames('pin-input-field');
|
|
14818
15811
|
const helpClass = usePrefixedClassNames('help', {
|
|
14819
15812
|
[`is-${messageColor}`]: !!messageColor,
|
|
14820
15813
|
});
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
const
|
|
14824
|
-
const
|
|
14825
|
-
const
|
|
14826
|
-
const
|
|
14827
|
-
|
|
14828
|
-
|
|
15814
|
+
// The boxes the selection covers: the one at a caret, or every selected
|
|
15815
|
+
// one. A caret after the last character marks the last box.
|
|
15816
|
+
const [start, end] = selection ?? [-1, -1];
|
|
15817
|
+
const first = Math.min(start, length - 1);
|
|
15818
|
+
const last = Math.max(first + 1, Math.min(end, length));
|
|
15819
|
+
const boxes = Array.from({ length }, (_, i) => {
|
|
15820
|
+
const char = code[i];
|
|
15821
|
+
const active = i >= first && i < last;
|
|
15822
|
+
return (jsxRuntime.jsx("div", { "aria-hidden": "true", className: prefixedClassNames(classPrefix, 'input', 'pin-input-box', {
|
|
15823
|
+
[`is-${color}`]: !!color,
|
|
15824
|
+
[`is-${size}`]: !!size,
|
|
15825
|
+
'is-rounded': isRounded,
|
|
15826
|
+
'is-focused': active,
|
|
15827
|
+
'has-caret': active && start === end && !char,
|
|
15828
|
+
'is-disabled': disabled,
|
|
15829
|
+
}), children: char && (isMasked ? MASK : char) }, i));
|
|
15830
|
+
});
|
|
15831
|
+
// Left to right on a right-to-left page too: a code is written that way,
|
|
15832
|
+
// so its first character stays in the first box on the left, and the
|
|
15833
|
+
// arrow keys move the caret the way the boxes run.
|
|
15834
|
+
let content = (jsxRuntime.jsxs("div", { className: rootClass, dir: "ltr", children: [jsxRuntime.jsx("input", { ref: setRefs, className: fieldClass, type: isMasked ? 'password' : 'text', inputMode: type === 'alphanumeric' ? 'text' : 'numeric', autoComplete: "one-time-code", autoCorrect: "off", spellCheck: false,
|
|
15835
|
+
// A partial code fails constraint validation. `minLength` would
|
|
15836
|
+
// say so only after a keystroke, never after a paste or a filtered
|
|
15837
|
+
// character, which set the value from script.
|
|
15838
|
+
pattern: `${allowed}{${length}}`, ...rest, value: code, maxLength: length, disabled: disabled, onChange: handleChange, onPaste: handlePaste, onFocus: e => {
|
|
15839
|
+
follow();
|
|
15840
|
+
onFocus?.(e);
|
|
15841
|
+
}, onBlur: e => {
|
|
15842
|
+
setSelection(null);
|
|
15843
|
+
onBlur?.(e);
|
|
15844
|
+
},
|
|
15845
|
+
// After the spread: an undefined `id` key in rest would wipe it.
|
|
15846
|
+
id: controlId }), boxes] }));
|
|
15847
|
+
if (ownControl) {
|
|
14829
15848
|
content = jsxRuntime.jsx(Control, { children: content });
|
|
14830
15849
|
}
|
|
15850
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
14831
15851
|
if (ownField) {
|
|
14832
15852
|
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
14833
15853
|
}
|
|
14834
15854
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
14835
15855
|
});
|
|
14836
|
-
|
|
15856
|
+
PinInput.displayName = 'PinInput';
|
|
14837
15857
|
|
|
14838
|
-
// What the status region announces: the `empty` content, as the one item
|
|
14839
|
-
// it's handed while that content is on screen because of the user's typing.
|
|
14840
|
-
const typedNoMatches$1 = [{ id: 'empty' }];
|
|
14841
|
-
const nothingToAnnounce$1 = [];
|
|
14842
|
-
// A press on an option would take focus from the field, since an option
|
|
14843
|
-
// can't hold it, and nothing would put it back. Cancelling the press keeps
|
|
14844
|
-
// it there, as the ARIA Authoring Practices' combobox does, and the click
|
|
14845
|
-
// still picks the option.
|
|
14846
|
-
const keepFocus = (e) => e.preventDefault();
|
|
14847
|
-
// An IME that's composing owns every key: Enter confirms what it composed,
|
|
14848
|
-
// and the arrows move through its candidates. Safari can say so with
|
|
14849
|
-
// `keyCode` 229 rather than `isComposing`.
|
|
14850
|
-
const isComposing$2 = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
|
14851
|
-
// Whether an option can be picked. The arrows still reach a disabled one, so
|
|
14852
|
-
// a screen reader user finds it.
|
|
14853
|
-
const isEnabled = (item) => typeof item === 'string' || !item.disabled;
|
|
14854
15858
|
/**
|
|
14855
|
-
*
|
|
15859
|
+
* Default star icon component.
|
|
15860
|
+
*/
|
|
15861
|
+
const StarIcon = ({ filled }) => (jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: filled ? 'currentColor' : 'none', stroke: "currentColor", strokeWidth: filled ? 0 : 2, "aria-hidden": "true", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }) }));
|
|
15862
|
+
/** Snap a value to the nearest precision step. */
|
|
15863
|
+
function snapToPrecision(val, precision) {
|
|
15864
|
+
return Math.round(val / precision) * precision;
|
|
15865
|
+
}
|
|
15866
|
+
/** Get fill percentage for a given icon index based on current value. */
|
|
15867
|
+
function getFillPercent(iconIndex, value) {
|
|
15868
|
+
if (value >= iconIndex + 1)
|
|
15869
|
+
return 100;
|
|
15870
|
+
if (value <= iconIndex)
|
|
15871
|
+
return 0;
|
|
15872
|
+
return Math.round((value - iconIndex) * 100);
|
|
15873
|
+
}
|
|
15874
|
+
/**
|
|
15875
|
+
* The `Rate` component provides a star/icon-based rating system.
|
|
14856
15876
|
*
|
|
14857
15877
|
* @function
|
|
14858
|
-
* @param {
|
|
14859
|
-
* @param {React.Ref<
|
|
14860
|
-
* @returns {JSX.Element} The rendered
|
|
15878
|
+
* @param {RateProps} props - Props for the Rate component.
|
|
15879
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the container element.
|
|
15880
|
+
* @returns {JSX.Element} The rendered rating component.
|
|
14861
15881
|
*
|
|
14862
15882
|
* @example
|
|
14863
|
-
* // Basic
|
|
14864
|
-
* <
|
|
14865
|
-
* data={['Apple', 'Banana', 'Cherry']}
|
|
14866
|
-
* placeholder="Search fruit..."
|
|
14867
|
-
* onSelect={(item) => console.log(item)}
|
|
14868
|
-
* />
|
|
15883
|
+
* // Basic 5-star rating
|
|
15884
|
+
* <Rate defaultValue={3} />
|
|
14869
15885
|
*
|
|
14870
15886
|
* @example
|
|
14871
|
-
* // With
|
|
14872
|
-
* <
|
|
14873
|
-
*
|
|
14874
|
-
*
|
|
14875
|
-
*
|
|
14876
|
-
* />
|
|
15887
|
+
* // With color and precision
|
|
15888
|
+
* <Rate defaultValue={3.5} color="warning" precision={0.5} showScore />
|
|
15889
|
+
*
|
|
15890
|
+
* @example
|
|
15891
|
+
* // With Font Awesome icons
|
|
15892
|
+
* <Rate iconName="star" iconLibrary="fa" iconVariant="solid" />
|
|
14877
15893
|
*/
|
|
14878
|
-
const
|
|
14879
|
-
// Deprecated no-ops, taken here so they stay off the wrapper.
|
|
14880
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
|
|
14881
|
-
selected,
|
|
14882
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
|
|
14883
|
-
dropdown,
|
|
14884
|
-
// Field props
|
|
14885
|
-
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, ...props }, ref) => {
|
|
15894
|
+
const Rate = React.forwardRef(({ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value: controlledValue, defaultValue = 0, max = 5, size, disabled = false, showScore = false, showText = false, texts, onChange, customIcon, spaced = false, rtl = false, iconName, iconLibrary: iconLibraryProp, iconVariant, iconFeatures, color, precision = 1, customText, name, form, className, ...props }, ref) => {
|
|
14886
15895
|
const insideField = useInsideField();
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
15896
|
+
const insideControl = useInsideControl();
|
|
15897
|
+
const ownField = rendersOwnField('Rate', {
|
|
15898
|
+
insideField,
|
|
15899
|
+
insideControl,
|
|
15900
|
+
label,
|
|
15901
|
+
message,
|
|
15902
|
+
horizontal,
|
|
15903
|
+
fieldClassName,
|
|
15904
|
+
});
|
|
15905
|
+
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
14890
15906
|
label,
|
|
14891
|
-
id,
|
|
14892
15907
|
labelProps,
|
|
14893
|
-
rendersLabel:
|
|
14894
|
-
|
|
15908
|
+
rendersLabel: ownField,
|
|
15909
|
+
callerProps: props,
|
|
14895
15910
|
});
|
|
14896
15911
|
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
14897
|
-
const
|
|
14898
|
-
const
|
|
14899
|
-
const
|
|
14900
|
-
const
|
|
14901
|
-
const
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
//
|
|
14907
|
-
const
|
|
14908
|
-
|
|
14909
|
-
setTyped(false);
|
|
14910
|
-
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
|
14911
|
-
const listboxId = `${React.useId()}-listbox`;
|
|
14912
|
-
// Use controlled or internal value
|
|
14913
|
-
const inputValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
14914
|
-
// Get display value from item
|
|
14915
|
-
const getDisplayValue = (item) => {
|
|
14916
|
-
if (typeof item === 'string')
|
|
14917
|
-
return item;
|
|
14918
|
-
const fieldValue = item[field];
|
|
14919
|
-
if (typeof fieldValue === 'string')
|
|
14920
|
-
return fieldValue;
|
|
14921
|
-
return item.value || '';
|
|
14922
|
-
};
|
|
14923
|
-
// Filter data based on input
|
|
14924
|
-
const filteredData = data.filter(item => {
|
|
14925
|
-
const displayValue = getDisplayValue(item);
|
|
14926
|
-
return displayValue.toLowerCase().includes(inputValue.toLowerCase());
|
|
14927
|
-
});
|
|
14928
|
-
// The dropdown shows the options or, when nothing matches, the `empty`
|
|
14929
|
-
// content. The listbox holds only options, so it's on screen only while
|
|
14930
|
-
// there are some.
|
|
14931
|
-
const menuOpen = isActive && (filteredData.length > 0 || !!empty);
|
|
14932
|
-
const listOpen = isActive && filteredData.length > 0;
|
|
14933
|
-
// Handle input change
|
|
14934
|
-
const handleInputChange = React.useCallback((e) => {
|
|
14935
|
-
const newValue = e.target.value;
|
|
14936
|
-
if (controlledValue === undefined) {
|
|
14937
|
-
setInternalValue(newValue);
|
|
14938
|
-
}
|
|
14939
|
-
onInput?.(newValue);
|
|
14940
|
-
setTyped(true);
|
|
14941
|
-
if (!isActive && newValue) {
|
|
14942
|
-
setIsActive(true);
|
|
14943
|
-
}
|
|
14944
|
-
}, [controlledValue, isActive, onInput]);
|
|
14945
|
-
// Handle item selection
|
|
14946
|
-
const handleSelect = React.useCallback((item) => {
|
|
14947
|
-
if (typeof item !== 'string' && item.disabled)
|
|
14948
|
-
return;
|
|
14949
|
-
const displayValue = getDisplayValue(item);
|
|
14950
|
-
if (controlledValue === undefined) {
|
|
14951
|
-
setInternalValue(displayValue);
|
|
14952
|
-
}
|
|
14953
|
-
onInput?.(displayValue);
|
|
14954
|
-
onSelect?.(item);
|
|
14955
|
-
if (!keepOpen) {
|
|
14956
|
-
setIsActive(false);
|
|
14957
|
-
}
|
|
14958
|
-
setHighlightedIndex(-1);
|
|
14959
|
-
}, [controlledValue, field, keepOpen, onInput, onSelect]);
|
|
14960
|
-
// Handle clear
|
|
14961
|
-
const handleClear = React.useCallback(() => {
|
|
14962
|
-
if (controlledValue === undefined) {
|
|
14963
|
-
setInternalValue('');
|
|
14964
|
-
}
|
|
14965
|
-
onInput?.('');
|
|
14966
|
-
onSelect?.(null);
|
|
14967
|
-
inputRef.current?.focus();
|
|
14968
|
-
}, [controlledValue, onInput, onSelect]);
|
|
14969
|
-
// Handle focus
|
|
14970
|
-
const handleFocus = React.useCallback(() => {
|
|
14971
|
-
if (openOnFocus && !disabled) {
|
|
14972
|
-
setIsActive(true);
|
|
14973
|
-
}
|
|
14974
|
-
}, [openOnFocus, disabled]);
|
|
14975
|
-
// Read when a click lands outside, so the listener stays put while the
|
|
14976
|
-
// list is open. What it selects changes as the user types and arrows
|
|
14977
|
-
// through suggestions, the filtered list is new on every render, and an
|
|
14978
|
-
// inline `onInput` or `onSelect` changes on every render of the parent.
|
|
14979
|
-
// Typing leaves the highlight where it was, so a list that narrowed can
|
|
14980
|
-
// leave it past the last suggestion, with nothing there to select.
|
|
14981
|
-
const selectHighlightedRef = useLatestRef(() => {
|
|
14982
|
-
if (selectOnClickOutside &&
|
|
14983
|
-
highlightedIndex >= 0 &&
|
|
14984
|
-
highlightedIndex < filteredData.length) {
|
|
14985
|
-
handleSelect(filteredData[highlightedIndex]);
|
|
14986
|
-
}
|
|
14987
|
-
});
|
|
14988
|
-
// Handle blur (click outside)
|
|
14989
|
-
React.useEffect(() => {
|
|
14990
|
-
if (!isActive)
|
|
14991
|
-
return undefined;
|
|
14992
|
-
const handleClickOutside = (e) => {
|
|
14993
|
-
if (!containerRef.current)
|
|
14994
|
-
return;
|
|
14995
|
-
// Use composedPath() to correctly detect clicks inside Shadow DOM
|
|
14996
|
-
const path = e.composedPath();
|
|
14997
|
-
const isInside = path.includes(containerRef.current);
|
|
14998
|
-
if (!isInside) {
|
|
14999
|
-
selectHighlightedRef.current();
|
|
15000
|
-
setIsActive(false);
|
|
15001
|
-
}
|
|
15002
|
-
};
|
|
15003
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
15004
|
-
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
15005
|
-
}, [isActive, selectHighlightedRef]);
|
|
15006
|
-
// Read when the dropdown opens or closes, so an inline `onActiveChange`,
|
|
15007
|
-
// new on every render of the parent, isn't told again about a state that
|
|
15008
|
-
// didn't change.
|
|
15009
|
-
const onActiveChangeRef = useLatestRef(onActiveChange);
|
|
15010
|
-
// Notify active state change
|
|
15011
|
-
React.useEffect(() => {
|
|
15012
|
-
onActiveChangeRef.current?.(isActive);
|
|
15013
|
-
}, [isActive, onActiveChangeRef]);
|
|
15014
|
-
// Keep first highlighted
|
|
15015
|
-
React.useEffect(() => {
|
|
15016
|
-
if (keepFirst && isActive && filteredData.length > 0) {
|
|
15017
|
-
setHighlightedIndex(0);
|
|
15018
|
-
}
|
|
15019
|
-
}, [keepFirst, isActive, filteredData.length, inputValue]);
|
|
15020
|
-
// Handle keyboard navigation
|
|
15021
|
-
const handleKeyDown = React.useCallback((e) => {
|
|
15022
|
-
if (disabled || isComposing$2(e))
|
|
15912
|
+
const starBaseId = React.useId();
|
|
15913
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
15914
|
+
const [hoverValue, setHoverValue] = React.useState(null);
|
|
15915
|
+
const defaultIconLibrary = useIconLibrary();
|
|
15916
|
+
const resolvedLibrary = iconLibraryProp || defaultIconLibrary || 'fa';
|
|
15917
|
+
// Determine if controlled
|
|
15918
|
+
const isControlled = controlledValue !== undefined;
|
|
15919
|
+
const currentValue = isControlled ? controlledValue : internalValue;
|
|
15920
|
+
const displayValue = hoverValue !== null ? hoverValue : currentValue;
|
|
15921
|
+
// Update value
|
|
15922
|
+
const updateValue = React.useCallback((newValue) => {
|
|
15923
|
+
if (disabled)
|
|
15023
15924
|
return;
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
// A closed list opens on its first enabled option, as the ARIA
|
|
15029
|
-
// Authoring Practices' combobox does. Alt+Down Arrow opens it
|
|
15030
|
-
// without a highlight.
|
|
15031
|
-
setIsActive(true);
|
|
15032
|
-
setHighlightedIndex(e.altKey ? -1 : filteredData.map(isEnabled).indexOf(true));
|
|
15033
|
-
}
|
|
15034
|
-
else {
|
|
15035
|
-
setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
|
|
15036
|
-
}
|
|
15037
|
-
break;
|
|
15038
|
-
case 'ArrowUp':
|
|
15039
|
-
e.preventDefault();
|
|
15040
|
-
if (!isActive) {
|
|
15041
|
-
// And Up Arrow opens it on its last enabled option.
|
|
15042
|
-
setIsActive(true);
|
|
15043
|
-
setHighlightedIndex(filteredData.map(isEnabled).lastIndexOf(true));
|
|
15044
|
-
}
|
|
15045
|
-
else {
|
|
15046
|
-
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
|
|
15047
|
-
}
|
|
15048
|
-
break;
|
|
15049
|
-
case 'Enter':
|
|
15050
|
-
// With the list open on a highlighted option it can pick, Enter
|
|
15051
|
-
// is that option's. Otherwise it closes the list and goes on, so
|
|
15052
|
-
// a form around the field submits, as it would from any text
|
|
15053
|
-
// field.
|
|
15054
|
-
if (isActive &&
|
|
15055
|
-
highlightedIndex >= 0 &&
|
|
15056
|
-
filteredData[highlightedIndex] &&
|
|
15057
|
-
isEnabled(filteredData[highlightedIndex])) {
|
|
15058
|
-
e.preventDefault();
|
|
15059
|
-
handleSelect(filteredData[highlightedIndex]);
|
|
15060
|
-
}
|
|
15061
|
-
else {
|
|
15062
|
-
setIsActive(false);
|
|
15063
|
-
}
|
|
15064
|
-
break;
|
|
15065
|
-
case 'Escape':
|
|
15066
|
-
// Only closing the dropdown claims the key. With it closed, the
|
|
15067
|
-
// key goes on to a Modal around the field.
|
|
15068
|
-
if (menuOpen)
|
|
15069
|
-
e.preventDefault();
|
|
15070
|
-
setIsActive(false);
|
|
15071
|
-
setHighlightedIndex(-1);
|
|
15072
|
-
break;
|
|
15073
|
-
case 'Tab':
|
|
15074
|
-
if (isActive &&
|
|
15075
|
-
highlightedIndex >= 0 &&
|
|
15076
|
-
filteredData[highlightedIndex]) {
|
|
15077
|
-
handleSelect(filteredData[highlightedIndex]);
|
|
15078
|
-
}
|
|
15079
|
-
setIsActive(false);
|
|
15080
|
-
break;
|
|
15925
|
+
// Allow clicking same value to deselect (set to 0)
|
|
15926
|
+
const finalValue = newValue === currentValue ? 0 : newValue;
|
|
15927
|
+
if (!isControlled) {
|
|
15928
|
+
setInternalValue(finalValue);
|
|
15081
15929
|
}
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
filteredData,
|
|
15088
|
-
handleSelect,
|
|
15089
|
-
]);
|
|
15090
|
-
// Handle dropdown scroll for infinite scroll
|
|
15091
|
-
const handleDropdownScroll = React.useCallback(() => {
|
|
15092
|
-
if (!checkInfiniteScroll || !dropdownRef.current)
|
|
15930
|
+
onChange?.(finalValue);
|
|
15931
|
+
}, [isControlled, currentValue, disabled, onChange]);
|
|
15932
|
+
// Handle click with precision support
|
|
15933
|
+
const handleClick = React.useCallback((iconIndex, e) => {
|
|
15934
|
+
if (disabled)
|
|
15093
15935
|
return;
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15936
|
+
if (precision < 1) {
|
|
15937
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
15938
|
+
let relativeX = (e.clientX - rect.left) / rect.width;
|
|
15939
|
+
if (rtl)
|
|
15940
|
+
relativeX = 1 - relativeX;
|
|
15941
|
+
const rawValue = iconIndex + relativeX;
|
|
15942
|
+
const snapped = snapToPrecision(rawValue, precision);
|
|
15943
|
+
const clamped = Math.max(precision, Math.min(snapped, max));
|
|
15944
|
+
updateValue(clamped);
|
|
15097
15945
|
}
|
|
15098
|
-
|
|
15099
|
-
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15946
|
+
else {
|
|
15947
|
+
updateValue(iconIndex + 1);
|
|
15948
|
+
}
|
|
15949
|
+
}, [disabled, precision, rtl, max, updateValue]);
|
|
15950
|
+
// Handle mouse enter on icon
|
|
15951
|
+
const handleMouseEnter = React.useCallback((iconIndex) => {
|
|
15952
|
+
if (disabled)
|
|
15953
|
+
return;
|
|
15954
|
+
setHoverValue(iconIndex + 1);
|
|
15955
|
+
}, [disabled]);
|
|
15956
|
+
// Handle mouse move with precision support
|
|
15957
|
+
const handleMouseMove = React.useCallback((iconIndex, e) => {
|
|
15958
|
+
if (disabled || precision >= 1)
|
|
15959
|
+
return;
|
|
15960
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
15961
|
+
let relativeX = (e.clientX - rect.left) / rect.width;
|
|
15962
|
+
if (rtl)
|
|
15963
|
+
relativeX = 1 - relativeX;
|
|
15964
|
+
const rawValue = iconIndex + relativeX;
|
|
15965
|
+
const snapped = snapToPrecision(rawValue, precision);
|
|
15966
|
+
const clamped = Math.max(precision, Math.min(snapped, max));
|
|
15967
|
+
setHoverValue(clamped);
|
|
15968
|
+
}, [disabled, precision, rtl, max]);
|
|
15969
|
+
// Handle mouse leave
|
|
15970
|
+
const handleMouseLeave = React.useCallback(() => {
|
|
15971
|
+
setHoverValue(null);
|
|
15972
|
+
}, []);
|
|
15973
|
+
// Handle keyboard
|
|
15974
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
15975
|
+
if (disabled)
|
|
15976
|
+
return;
|
|
15977
|
+
let newValue = currentValue;
|
|
15978
|
+
const step = precision < 1 ? precision : 1;
|
|
15979
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
|
|
15980
|
+
e.preventDefault();
|
|
15981
|
+
newValue = Math.min(currentValue + step, max);
|
|
15982
|
+
}
|
|
15983
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
|
|
15984
|
+
e.preventDefault();
|
|
15985
|
+
newValue = Math.max(currentValue - step, 0);
|
|
15986
|
+
}
|
|
15987
|
+
else if (e.key === 'Home') {
|
|
15988
|
+
e.preventDefault();
|
|
15989
|
+
newValue = 0;
|
|
15990
|
+
}
|
|
15991
|
+
else if (e.key === 'End') {
|
|
15992
|
+
e.preventDefault();
|
|
15993
|
+
newValue = max;
|
|
15994
|
+
}
|
|
15995
|
+
// Round to avoid floating point issues
|
|
15996
|
+
newValue = Math.round(newValue * 1000) / 1000;
|
|
15997
|
+
if (newValue !== currentValue) {
|
|
15998
|
+
if (!isControlled) {
|
|
15999
|
+
setInternalValue(newValue);
|
|
15106
16000
|
}
|
|
16001
|
+
onChange?.(newValue);
|
|
15107
16002
|
}
|
|
15108
|
-
}, [
|
|
15109
|
-
//
|
|
15110
|
-
const
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
16003
|
+
}, [currentValue, max, precision, disabled, isControlled, onChange]);
|
|
16004
|
+
// Get text for current value
|
|
16005
|
+
const getText = () => {
|
|
16006
|
+
if (!showText || !texts)
|
|
16007
|
+
return null;
|
|
16008
|
+
const wholeIndex = Math.ceil(displayValue) - 1;
|
|
16009
|
+
const baseText = texts[wholeIndex] || null;
|
|
16010
|
+
if (!baseText)
|
|
16011
|
+
return null;
|
|
16012
|
+
// For fractional hover values with precision < 1, append "+"
|
|
16013
|
+
if (precision < 1 && hoverValue !== null && hoverValue % 1 !== 0) {
|
|
16014
|
+
return `${baseText}+`;
|
|
15115
16015
|
}
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
16016
|
+
return baseText;
|
|
16017
|
+
};
|
|
16018
|
+
// Format score display
|
|
16019
|
+
const getScoreDisplay = () => {
|
|
16020
|
+
if (precision < 1 && currentValue % 1 !== 0) {
|
|
16021
|
+
return currentValue.toFixed(1);
|
|
15119
16022
|
}
|
|
15120
|
-
|
|
16023
|
+
return currentValue;
|
|
16024
|
+
};
|
|
15121
16025
|
// Generate classes
|
|
15122
|
-
const
|
|
15123
|
-
|
|
15124
|
-
[`is-${size}`]:
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
[`is-${
|
|
15129
|
-
'is-loading': loading,
|
|
15130
|
-
});
|
|
15131
|
-
const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
|
|
15132
|
-
const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
|
|
15133
|
-
'is-active': menuOpen,
|
|
15134
|
-
});
|
|
15135
|
-
const dropdownContentClass = usePrefixedClassNames('dropdown-content');
|
|
15136
|
-
const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
|
|
15137
|
-
const dropdownFooterClass = usePrefixedClassNames('dropdown-footer');
|
|
15138
|
-
const emptyItemClasses = usePrefixedClassNames('dropdown-item', 'has-text-grey');
|
|
15139
|
-
const iconRightClickableClass = usePrefixedClassNames('icon', 'is-right', 'is-clickable');
|
|
15140
|
-
const iconRightClass = usePrefixedClassNames('icon', 'is-right');
|
|
15141
|
-
const loaderClass = usePrefixedClassNames('loader', 'is-loading');
|
|
15142
|
-
const combinedClasses = classNames(autocompleteClasses, bulmaHelperClasses, className);
|
|
15143
|
-
const helpClass = usePrefixedClassNames('help', {
|
|
15144
|
-
[`is-${messageColor}`]: !!messageColor,
|
|
16026
|
+
const classPrefix = useClassPrefix();
|
|
16027
|
+
const rateClasses = usePrefixedClassNames('rate', {
|
|
16028
|
+
[`is-${size}`]: size,
|
|
16029
|
+
'is-disabled': disabled,
|
|
16030
|
+
'is-spaced': spaced,
|
|
16031
|
+
'is-rtl': rtl,
|
|
16032
|
+
[`is-${color}`]: color,
|
|
15145
16033
|
});
|
|
15146
|
-
const
|
|
15147
|
-
//
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
:
|
|
15163
|
-
|
|
15164
|
-
//
|
|
15165
|
-
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
|
|
15169
|
-
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
const
|
|
15184
|
-
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
16034
|
+
const combinedClasses = classNames(rateClasses, bulmaHelperClasses, className);
|
|
16035
|
+
// Render a single icon based on priority: customIcon > iconName > default SVG
|
|
16036
|
+
const renderIcon = (iconIndex, isActive, fillPercent) => {
|
|
16037
|
+
const iconProps = {
|
|
16038
|
+
index: iconIndex,
|
|
16039
|
+
isActive,
|
|
16040
|
+
isHovered: hoverValue !== null && iconIndex < hoverValue,
|
|
16041
|
+
value: displayValue,
|
|
16042
|
+
fillPercent,
|
|
16043
|
+
};
|
|
16044
|
+
if (customIcon) {
|
|
16045
|
+
return customIcon(iconProps);
|
|
16046
|
+
}
|
|
16047
|
+
if (iconName) {
|
|
16048
|
+
return (jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }));
|
|
16049
|
+
}
|
|
16050
|
+
return jsxRuntime.jsx(StarIcon, { filled: isActive });
|
|
16051
|
+
};
|
|
16052
|
+
// Render partial icon with layered approach
|
|
16053
|
+
const renderPartialIcon = (iconIndex, fillPercent) => {
|
|
16054
|
+
if (customIcon) {
|
|
16055
|
+
// For custom icons, pass fillPercent and let consumer handle it
|
|
16056
|
+
const iconProps = {
|
|
16057
|
+
index: iconIndex,
|
|
16058
|
+
isActive: fillPercent > 0,
|
|
16059
|
+
isHovered: hoverValue !== null && iconIndex < (hoverValue ?? 0),
|
|
16060
|
+
value: displayValue,
|
|
16061
|
+
fillPercent,
|
|
16062
|
+
};
|
|
16063
|
+
return customIcon(iconProps);
|
|
16064
|
+
}
|
|
16065
|
+
if (iconName) {
|
|
16066
|
+
return (jsxRuntime.jsxs("span", { className: prefixedClassNames(classPrefix, 'rate-icon-partial'), style: {
|
|
16067
|
+
'--rate-fill-percent': `${fillPercent}%`,
|
|
16068
|
+
}, children: [jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-bg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) }), jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-icon-fg'), children: jsxRuntime.jsx(Icon, { name: iconName, library: resolvedLibrary, variant: iconVariant, features: iconFeatures }) })] }));
|
|
16069
|
+
}
|
|
16070
|
+
// Default SVG with clipPath for partial fill
|
|
16071
|
+
const clipId = `rate-clip-${iconIndex}`;
|
|
16072
|
+
return (jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", "aria-hidden": "true", children: [jsxRuntime.jsx("defs", { children: jsxRuntime.jsx("clipPath", { id: clipId, children: jsxRuntime.jsx("rect", { x: "0", y: "0", width: `${(fillPercent / 100) * 24}`, height: "24" }) }) }), jsxRuntime.jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" }), jsxRuntime.jsx("path", { clipPath: `url(#${clipId})`, fill: "currentColor", d: "M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" })] }));
|
|
16073
|
+
};
|
|
16074
|
+
// The star the value falls in is the checked radio, and it carries the
|
|
16075
|
+
// value: a radiogroup takes no aria-value* of its own. A fractional value
|
|
16076
|
+
// names that star by the value itself ("3.5 stars"). Both go by the value
|
|
16077
|
+
// as that name writes it, to hundredths, so a value a hair over a whole
|
|
16078
|
+
// number checks that number's star under its own name, and no two stars
|
|
16079
|
+
// share a name.
|
|
16080
|
+
const shownValue = Number(currentValue.toFixed(2));
|
|
16081
|
+
const checkedIndex = shownValue > 0 ? Math.min(Math.ceil(shownValue), max) - 1 : -1;
|
|
16082
|
+
// Render icons
|
|
16083
|
+
const renderIcons = () => {
|
|
16084
|
+
const icons = [];
|
|
16085
|
+
for (let i = 0; i < max; i++) {
|
|
16086
|
+
const iconIndex = rtl ? max - 1 - i : i;
|
|
16087
|
+
const fillPercent = getFillPercent(iconIndex, displayValue);
|
|
16088
|
+
const isActive = fillPercent === 100;
|
|
16089
|
+
const isHovered = hoverValue !== null && iconIndex < hoverValue;
|
|
16090
|
+
const isPartial = fillPercent > 0 && fillPercent < 100;
|
|
16091
|
+
const icon = isPartial
|
|
16092
|
+
? renderPartialIcon(iconIndex, fillPercent)
|
|
16093
|
+
: renderIcon(iconIndex, isActive, fillPercent);
|
|
16094
|
+
icons.push(jsxRuntime.jsx("span", { className: prefixedClassNames(classPrefix, 'rate-item', {
|
|
16095
|
+
'is-active': isActive,
|
|
16096
|
+
'is-hovered': isHovered,
|
|
16097
|
+
}), onClick: e => handleClick(iconIndex, e), onMouseEnter: () => handleMouseEnter(iconIndex), onMouseMove: e => handleMouseMove(iconIndex, e), onMouseLeave: handleMouseLeave, id: `${starBaseId}-${iconIndex}`, role: "radio", "aria-checked": iconIndex === checkedIndex, "aria-label": iconIndex === checkedIndex && shownValue % 1 !== 0
|
|
16098
|
+
? `${shownValue} stars`
|
|
16099
|
+
: `${iconIndex + 1} star${iconIndex === 0 ? '' : 's'}`, tabIndex: -1, children: icon }, iconIndex));
|
|
16100
|
+
}
|
|
16101
|
+
return icons;
|
|
16102
|
+
};
|
|
16103
|
+
const text = getText();
|
|
16104
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
16105
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
16106
|
+
});
|
|
16107
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
16108
|
+
const rateItemsClass = usePrefixedClassNames('rate-items');
|
|
16109
|
+
const rateScoreClass = usePrefixedClassNames('rate-score');
|
|
16110
|
+
const rateTextClass = usePrefixedClassNames('rate-text');
|
|
16111
|
+
const rateCustomTextClass = usePrefixedClassNames('rate-custom-text');
|
|
16112
|
+
const rateElement = (jsxRuntime.jsxs("div", { ref: ref, className: combinedClasses, role: "radiogroup", "aria-label": ariaLabelledBy ? undefined : 'Rating', "aria-labelledby": ariaLabelledBy, "aria-activedescendant": checkedIndex >= 0 ? `${starBaseId}-${checkedIndex}` : undefined, tabIndex: disabled ? -1 : 0, onKeyDown: handleKeyDown, ...rest, children: [jsxRuntime.jsx("div", { className: rateItemsClass, children: renderIcons() }), showScore && (jsxRuntime.jsx("span", { className: rateScoreClass, children: getScoreDisplay() })), text && jsxRuntime.jsx("span", { className: rateTextClass, children: text }), customText && (jsxRuntime.jsx("span", { className: rateCustomTextClass, children: customText })), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, value: currentValue, form: form }))] }));
|
|
16113
|
+
let content = rateElement;
|
|
16114
|
+
if (rendersOwnControl('Rate', { insideControl })) {
|
|
16115
|
+
content = jsxRuntime.jsx(Control, { children: content });
|
|
15202
16116
|
}
|
|
15203
|
-
|
|
16117
|
+
if (ownField) {
|
|
16118
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
16119
|
+
}
|
|
16120
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
15204
16121
|
});
|
|
15205
|
-
|
|
16122
|
+
Rate.displayName = 'Rate';
|
|
15206
16123
|
|
|
15207
|
-
//
|
|
15208
|
-
|
|
15209
|
-
const
|
|
15210
|
-
const
|
|
16124
|
+
// What the status region announces: the `empty` content, as the one item
|
|
16125
|
+
// it's handed while that content is on screen because of the user's typing.
|
|
16126
|
+
const typedNoMatches$1 = [{ id: 'empty' }];
|
|
16127
|
+
const nothingToAnnounce$1 = [];
|
|
16128
|
+
// A press on an option would take focus from the field, since an option
|
|
16129
|
+
// can't hold it, and nothing would put it back. Cancelling the press keeps
|
|
16130
|
+
// it there, as the ARIA Authoring Practices' combobox does, and the click
|
|
16131
|
+
// still picks the option.
|
|
16132
|
+
const keepFocus = (e) => e.preventDefault();
|
|
16133
|
+
// An IME that's composing owns every key: Enter confirms what it composed,
|
|
16134
|
+
// and the arrows move through its candidates. Safari can say so with
|
|
16135
|
+
// `keyCode` 229 rather than `isComposing`.
|
|
16136
|
+
const isComposing$1 = (e) => e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
|
16137
|
+
// Whether an option can be picked. The arrows still reach a disabled one, so
|
|
16138
|
+
// a screen reader user finds it.
|
|
16139
|
+
const isEnabled = (item) => typeof item === 'string' || !item.disabled;
|
|
15211
16140
|
/**
|
|
15212
|
-
* The
|
|
15213
|
-
* combobox whose list the app renders and filters (#1083). DOM focus stays
|
|
15214
|
-
* in the field the whole time: the highlight is `aria-activedescendant`, as
|
|
15215
|
-
* the ARIA Authoring Practices ask of a combobox, where `useMenuKeys` moves
|
|
15216
|
-
* real focus, as a menu should.
|
|
16141
|
+
* The `Autocomplete` component provides an input field with dropdown suggestions that filter based on user input.
|
|
15217
16142
|
*
|
|
15218
|
-
*
|
|
15219
|
-
*
|
|
15220
|
-
*
|
|
15221
|
-
*
|
|
15222
|
-
* `onSelect`.
|
|
16143
|
+
* @function
|
|
16144
|
+
* @param {AutocompleteProps} props - Props for the Autocomplete component.
|
|
16145
|
+
* @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element.
|
|
16146
|
+
* @returns {JSX.Element} The rendered autocomplete component.
|
|
15223
16147
|
*
|
|
15224
|
-
* @
|
|
15225
|
-
*
|
|
15226
|
-
*
|
|
16148
|
+
* @example
|
|
16149
|
+
* // Basic usage with string array
|
|
16150
|
+
* <Autocomplete
|
|
16151
|
+
* data={['Apple', 'Banana', 'Cherry']}
|
|
16152
|
+
* placeholder="Search fruit..."
|
|
16153
|
+
* onSelect={(item) => console.log(item)}
|
|
16154
|
+
* />
|
|
16155
|
+
*
|
|
16156
|
+
* @example
|
|
16157
|
+
* // With object data
|
|
16158
|
+
* <Autocomplete
|
|
16159
|
+
* data={[{ value: '1', label: 'Option 1' }, { value: '2', label: 'Option 2' }]}
|
|
16160
|
+
* field="label"
|
|
16161
|
+
* onSelect={(item) => console.log(item)}
|
|
16162
|
+
* />
|
|
15227
16163
|
*/
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
16164
|
+
const Autocomplete = React.forwardRef(({ data = [], value: controlledValue, placeholder, field = 'label', clearable = false, openOnFocus = false, keepFirst = false, keepOpen = false, selectOnClickOutside = false, maxHeight = 200, loading = false, disabled = false, checkInfiniteScroll = false, infiniteScrollDistance = 50, color, size, onInput, onSelect, onActiveChange, onInfiniteScroll, itemTemplate, header, footer, empty, name, form, required, id,
|
|
16165
|
+
// Deprecated no-ops, taken here so they stay off the wrapper.
|
|
16166
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
|
|
16167
|
+
selected,
|
|
16168
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op
|
|
16169
|
+
dropdown,
|
|
16170
|
+
// Field props
|
|
16171
|
+
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, className, ...props }, ref) => {
|
|
16172
|
+
const insideField = useInsideField();
|
|
16173
|
+
// The suggestion list points `aria-labelledby` at the label that names
|
|
16174
|
+
// the input, so it needs that label's id.
|
|
16175
|
+
const { controlId, fieldLabelProps, labelId: listLabelId, } = useAutoLabelId({
|
|
16176
|
+
label,
|
|
16177
|
+
id,
|
|
16178
|
+
labelProps,
|
|
16179
|
+
rendersLabel: !insideField,
|
|
16180
|
+
needsLabelId: true,
|
|
16181
|
+
});
|
|
16182
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
16183
|
+
const { classPrefix } = useConfig();
|
|
16184
|
+
const containerRef = React.useRef(null);
|
|
15236
16185
|
const inputRef = React.useRef(null);
|
|
15237
|
-
const
|
|
15238
|
-
const
|
|
15239
|
-
const
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
// Whether the next highlight change is one to scroll to: a key's or a
|
|
15245
|
-
// landing's. The pointer's isn't, since the pointer is already on that
|
|
15246
|
-
// option, and scrolling would move the list out from under it.
|
|
15247
|
-
const scrollToHighlightRef = React.useRef(false);
|
|
15248
|
-
const isControlled = controlledOpen !== undefined;
|
|
15249
|
-
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
15250
|
-
const open = inline || (controlledOpen ?? internalOpen);
|
|
15251
|
-
const [highlighted, setHighlighted] = React.useState(null);
|
|
16186
|
+
const dropdownRef = React.useRef(null);
|
|
16187
|
+
const [internalValue, setInternalValue] = React.useState('');
|
|
16188
|
+
const [isActive, setIsActive] = React.useState(false);
|
|
16189
|
+
// Whether the user has typed since the list opened, since only typing
|
|
16190
|
+
// gets the `empty` content announced. Closing the list resets it (during
|
|
16191
|
+
// render, React's pattern for adjusting state when other state changes),
|
|
16192
|
+
// so opening the list again without typing stays quiet.
|
|
15252
16193
|
const [typed, setTyped] = React.useState(false);
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
const [
|
|
15256
|
-
|
|
15257
|
-
//
|
|
15258
|
-
const
|
|
15259
|
-
//
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
|
|
15281
|
-
|
|
15282
|
-
|
|
15283
|
-
|
|
15284
|
-
|
|
15285
|
-
|
|
15286
|
-
|
|
15287
|
-
|
|
15288
|
-
|
|
15289
|
-
|
|
15290
|
-
|
|
15291
|
-
|
|
15292
|
-
|
|
15293
|
-
if (!isControlled)
|
|
15294
|
-
setInternalOpen(next);
|
|
15295
|
-
}, [inline, open, onOpenChange, isControlled]);
|
|
15296
|
-
const close = React.useCallback(() => requestOpen(false), [requestOpen]);
|
|
15297
|
-
// After every render, read the list: whether it has options, and where
|
|
15298
|
-
// the highlight goes now. A highlighted option that has left takes the
|
|
15299
|
-
// highlight with it, or hands it to the first enabled one under
|
|
15300
|
-
// `keepFirst`, which also moves it there whenever the options change.
|
|
15301
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose
|
|
15302
|
-
useClientLayoutEffect(() => {
|
|
15303
|
-
const opened = open && !wasOpenRef.current;
|
|
15304
|
-
wasOpenRef.current = open;
|
|
15305
|
-
if (!open) {
|
|
15306
|
-
landingRef.current = null;
|
|
15307
|
-
shownValuesRef.current = [];
|
|
16194
|
+
if (typed && !isActive)
|
|
16195
|
+
setTyped(false);
|
|
16196
|
+
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
|
16197
|
+
const listboxId = `${React.useId()}-listbox`;
|
|
16198
|
+
// Use controlled or internal value
|
|
16199
|
+
const inputValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
16200
|
+
// Get display value from item
|
|
16201
|
+
const getDisplayValue = (item) => {
|
|
16202
|
+
if (typeof item === 'string')
|
|
16203
|
+
return item;
|
|
16204
|
+
const fieldValue = item[field];
|
|
16205
|
+
if (typeof fieldValue === 'string')
|
|
16206
|
+
return fieldValue;
|
|
16207
|
+
return item.value || '';
|
|
16208
|
+
};
|
|
16209
|
+
// Filter data based on input
|
|
16210
|
+
const filteredData = data.filter(item => {
|
|
16211
|
+
const displayValue = getDisplayValue(item);
|
|
16212
|
+
return displayValue.toLowerCase().includes(inputValue.toLowerCase());
|
|
16213
|
+
});
|
|
16214
|
+
// The dropdown shows the options or, when nothing matches, the `empty`
|
|
16215
|
+
// content. The listbox holds only options, so it's on screen only while
|
|
16216
|
+
// there are some.
|
|
16217
|
+
const menuOpen = isActive && (filteredData.length > 0 || !!empty);
|
|
16218
|
+
const listOpen = isActive && filteredData.length > 0;
|
|
16219
|
+
// Handle input change
|
|
16220
|
+
const handleInputChange = React.useCallback((e) => {
|
|
16221
|
+
const newValue = e.target.value;
|
|
16222
|
+
if (controlledValue === undefined) {
|
|
16223
|
+
setInternalValue(newValue);
|
|
16224
|
+
}
|
|
16225
|
+
onInput?.(newValue);
|
|
16226
|
+
setTyped(true);
|
|
16227
|
+
if (!isActive && newValue) {
|
|
16228
|
+
setIsActive(true);
|
|
16229
|
+
}
|
|
16230
|
+
}, [controlledValue, isActive, onInput]);
|
|
16231
|
+
// Handle item selection
|
|
16232
|
+
const handleSelect = React.useCallback((item) => {
|
|
16233
|
+
if (typeof item !== 'string' && item.disabled)
|
|
15308
16234
|
return;
|
|
16235
|
+
const displayValue = getDisplayValue(item);
|
|
16236
|
+
if (controlledValue === undefined) {
|
|
16237
|
+
setInternalValue(displayValue);
|
|
16238
|
+
}
|
|
16239
|
+
onInput?.(displayValue);
|
|
16240
|
+
onSelect?.(item);
|
|
16241
|
+
if (!keepOpen) {
|
|
16242
|
+
setIsActive(false);
|
|
16243
|
+
}
|
|
16244
|
+
setHighlightedIndex(-1);
|
|
16245
|
+
}, [controlledValue, field, keepOpen, onInput, onSelect]);
|
|
16246
|
+
// Handle clear
|
|
16247
|
+
const handleClear = React.useCallback(() => {
|
|
16248
|
+
if (controlledValue === undefined) {
|
|
16249
|
+
setInternalValue('');
|
|
15309
16250
|
}
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
next = null;
|
|
15332
|
-
else if (keepFirst && changed)
|
|
15333
|
-
next = first;
|
|
15334
|
-
// Without `keepFirst`, which moved it above, a highlighted option
|
|
15335
|
-
// that left takes the highlight with it.
|
|
15336
|
-
else if (next !== null && !values.includes(next))
|
|
15337
|
-
next = null;
|
|
15338
|
-
// Only a change is set. StrictMode runs this twice on mount, and the
|
|
15339
|
-
// second run, with the landing used up, works out the rendered highlight
|
|
15340
|
-
// again, so it leaves the first run's landing in place.
|
|
15341
|
-
if (next !== highlighted) {
|
|
15342
|
-
scrollToHighlightRef.current = true;
|
|
15343
|
-
setHighlighted(next);
|
|
16251
|
+
onInput?.('');
|
|
16252
|
+
onSelect?.(null);
|
|
16253
|
+
inputRef.current?.focus();
|
|
16254
|
+
}, [controlledValue, onInput, onSelect]);
|
|
16255
|
+
// Handle focus
|
|
16256
|
+
const handleFocus = React.useCallback(() => {
|
|
16257
|
+
if (openOnFocus && !disabled) {
|
|
16258
|
+
setIsActive(true);
|
|
16259
|
+
}
|
|
16260
|
+
}, [openOnFocus, disabled]);
|
|
16261
|
+
// Read when a click lands outside, so the listener stays put while the
|
|
16262
|
+
// list is open. What it selects changes as the user types and arrows
|
|
16263
|
+
// through suggestions, the filtered list is new on every render, and an
|
|
16264
|
+
// inline `onInput` or `onSelect` changes on every render of the parent.
|
|
16265
|
+
// Typing leaves the highlight where it was, so a list that narrowed can
|
|
16266
|
+
// leave it past the last suggestion, with nothing there to select.
|
|
16267
|
+
const selectHighlightedRef = useLatestRef(() => {
|
|
16268
|
+
if (selectOnClickOutside &&
|
|
16269
|
+
highlightedIndex >= 0 &&
|
|
16270
|
+
highlightedIndex < filteredData.length) {
|
|
16271
|
+
handleSelect(filteredData[highlightedIndex]);
|
|
15344
16272
|
}
|
|
15345
16273
|
});
|
|
15346
|
-
//
|
|
15347
|
-
// the highlighted option is brought into view here, when a key or a
|
|
15348
|
-
// landing moved it there.
|
|
15349
|
-
useClientLayoutEffect(() => {
|
|
15350
|
-
if (highlighted === null || !scrollToHighlightRef.current)
|
|
15351
|
-
return;
|
|
15352
|
-
scrollToHighlightRef.current = false;
|
|
15353
|
-
const option = shownOptions().find(shown => shown.value === highlighted);
|
|
15354
|
-
option?.element.scrollIntoView?.({ block: 'nearest' });
|
|
15355
|
-
}, [highlighted, shownOptions]);
|
|
15356
|
-
// A press outside the field and the list closes it. The path is checked
|
|
15357
|
-
// rather than the target, so it works in a shadow root.
|
|
16274
|
+
// Handle blur (click outside)
|
|
15358
16275
|
React.useEffect(() => {
|
|
15359
|
-
|
|
15360
|
-
if (!open || inline || !root)
|
|
16276
|
+
if (!isActive)
|
|
15361
16277
|
return undefined;
|
|
15362
|
-
const
|
|
15363
|
-
if (
|
|
15364
|
-
isEventInside(event, rootRef.current) ||
|
|
15365
|
-
isEventInside(event, popupRef.current)) {
|
|
16278
|
+
const handleClickOutside = (e) => {
|
|
16279
|
+
if (!containerRef.current)
|
|
15366
16280
|
return;
|
|
16281
|
+
// Use composedPath() to correctly detect clicks inside Shadow DOM
|
|
16282
|
+
const path = e.composedPath();
|
|
16283
|
+
const isInside = path.includes(containerRef.current);
|
|
16284
|
+
if (!isInside) {
|
|
16285
|
+
selectHighlightedRef.current();
|
|
16286
|
+
setIsActive(false);
|
|
15367
16287
|
}
|
|
15368
|
-
close();
|
|
15369
16288
|
};
|
|
15370
|
-
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
if (
|
|
15384
|
-
|
|
15385
|
-
const { key, altKey } = event;
|
|
15386
|
-
if (!open) {
|
|
15387
|
-
if (key === 'ArrowDown' || (key === 'ArrowUp' && !altKey)) {
|
|
15388
|
-
event.preventDefault();
|
|
15389
|
-
requestOpen(true, altKey ? 'none' : key === 'ArrowDown' ? 'first' : 'last');
|
|
15390
|
-
}
|
|
15391
|
-
return;
|
|
16289
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
16290
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
16291
|
+
}, [isActive, selectHighlightedRef]);
|
|
16292
|
+
// Read when the dropdown opens or closes, so an inline `onActiveChange`,
|
|
16293
|
+
// new on every render of the parent, isn't told again about a state that
|
|
16294
|
+
// didn't change.
|
|
16295
|
+
const onActiveChangeRef = useLatestRef(onActiveChange);
|
|
16296
|
+
// Notify active state change
|
|
16297
|
+
React.useEffect(() => {
|
|
16298
|
+
onActiveChangeRef.current?.(isActive);
|
|
16299
|
+
}, [isActive, onActiveChangeRef]);
|
|
16300
|
+
// Keep first highlighted
|
|
16301
|
+
React.useEffect(() => {
|
|
16302
|
+
if (keepFirst && isActive && filteredData.length > 0) {
|
|
16303
|
+
setHighlightedIndex(0);
|
|
15392
16304
|
}
|
|
15393
|
-
|
|
16305
|
+
}, [keepFirst, isActive, filteredData.length, inputValue]);
|
|
16306
|
+
// Handle keyboard navigation
|
|
16307
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
16308
|
+
if (disabled || isComposing$1(e))
|
|
16309
|
+
return;
|
|
16310
|
+
switch (e.key) {
|
|
15394
16311
|
case 'ArrowDown':
|
|
15395
|
-
|
|
15396
|
-
if (
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
16312
|
+
e.preventDefault();
|
|
16313
|
+
if (!isActive) {
|
|
16314
|
+
// A closed list opens on its first enabled option, as the ARIA
|
|
16315
|
+
// Authoring Practices' combobox does. Alt+Down Arrow opens it
|
|
16316
|
+
// without a highlight.
|
|
16317
|
+
setIsActive(true);
|
|
16318
|
+
setHighlightedIndex(e.altKey ? -1 : filteredData.map(isEnabled).indexOf(true));
|
|
15402
16319
|
}
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
const options = shownOptions();
|
|
15406
|
-
if (options.length === 0)
|
|
15407
|
-
return;
|
|
15408
|
-
const at = options.findIndex(option => option.value === highlighted);
|
|
15409
|
-
const step = key === 'ArrowDown' ? 1 : -1;
|
|
15410
|
-
const index = at === -1
|
|
15411
|
-
? step === 1
|
|
15412
|
-
? 0
|
|
15413
|
-
: options.length - 1
|
|
15414
|
-
: (at + step + options.length) % options.length;
|
|
15415
|
-
scrollToHighlightRef.current = true;
|
|
15416
|
-
setHighlighted(options[index].value);
|
|
15417
|
-
return;
|
|
15418
|
-
}
|
|
15419
|
-
case 'Enter': {
|
|
15420
|
-
const option = shownOptions().find(shown => shown.value === highlighted);
|
|
15421
|
-
if (option) {
|
|
15422
|
-
event.preventDefault();
|
|
15423
|
-
if (!option.disabled)
|
|
15424
|
-
submit(option.value);
|
|
15425
|
-
return;
|
|
16320
|
+
else {
|
|
16321
|
+
setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
|
|
15426
16322
|
}
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
16323
|
+
break;
|
|
16324
|
+
case 'ArrowUp':
|
|
16325
|
+
e.preventDefault();
|
|
16326
|
+
if (!isActive) {
|
|
16327
|
+
// And Up Arrow opens it on its last enabled option.
|
|
16328
|
+
setIsActive(true);
|
|
16329
|
+
setHighlightedIndex(filteredData.map(isEnabled).lastIndexOf(true));
|
|
16330
|
+
}
|
|
16331
|
+
else {
|
|
16332
|
+
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
|
|
16333
|
+
}
|
|
16334
|
+
break;
|
|
16335
|
+
case 'Enter':
|
|
16336
|
+
// With the list open on a highlighted option it can pick, Enter
|
|
16337
|
+
// is that option's. Otherwise it closes the list and goes on, so
|
|
16338
|
+
// a form around the field submits, as it would from any text
|
|
16339
|
+
// field.
|
|
16340
|
+
if (isActive &&
|
|
16341
|
+
highlightedIndex >= 0 &&
|
|
16342
|
+
filteredData[highlightedIndex] &&
|
|
16343
|
+
isEnabled(filteredData[highlightedIndex])) {
|
|
16344
|
+
e.preventDefault();
|
|
16345
|
+
handleSelect(filteredData[highlightedIndex]);
|
|
16346
|
+
}
|
|
16347
|
+
else {
|
|
16348
|
+
setIsActive(false);
|
|
16349
|
+
}
|
|
16350
|
+
break;
|
|
15432
16351
|
case 'Escape':
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
close();
|
|
15441
|
-
return;
|
|
16352
|
+
// Only closing the dropdown claims the key. With it closed, the
|
|
16353
|
+
// key goes on to a Modal around the field.
|
|
16354
|
+
if (menuOpen)
|
|
16355
|
+
e.preventDefault();
|
|
16356
|
+
setIsActive(false);
|
|
16357
|
+
setHighlightedIndex(-1);
|
|
16358
|
+
break;
|
|
15442
16359
|
case 'Tab':
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
setHighlighted(null);
|
|
15451
|
-
return;
|
|
15452
|
-
}
|
|
15453
|
-
};
|
|
15454
|
-
const onChange = () => {
|
|
15455
|
-
setTyped(true);
|
|
15456
|
-
if (open) {
|
|
15457
|
-
landingRef.current = 'typed';
|
|
15458
|
-
setHighlighted(null);
|
|
15459
|
-
refresh();
|
|
15460
|
-
}
|
|
15461
|
-
else {
|
|
15462
|
-
requestOpen(true, 'typed');
|
|
16360
|
+
if (isActive &&
|
|
16361
|
+
highlightedIndex >= 0 &&
|
|
16362
|
+
filteredData[highlightedIndex]) {
|
|
16363
|
+
handleSelect(filteredData[highlightedIndex]);
|
|
16364
|
+
}
|
|
16365
|
+
setIsActive(false);
|
|
16366
|
+
break;
|
|
15463
16367
|
}
|
|
15464
|
-
}
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
16368
|
+
}, [
|
|
16369
|
+
disabled,
|
|
16370
|
+
isActive,
|
|
16371
|
+
menuOpen,
|
|
16372
|
+
highlightedIndex,
|
|
16373
|
+
filteredData,
|
|
16374
|
+
handleSelect,
|
|
16375
|
+
]);
|
|
16376
|
+
// Handle dropdown scroll for infinite scroll
|
|
16377
|
+
const handleDropdownScroll = React.useCallback(() => {
|
|
16378
|
+
if (!checkInfiniteScroll || !dropdownRef.current)
|
|
15472
16379
|
return;
|
|
16380
|
+
const { scrollTop, scrollHeight, clientHeight } = dropdownRef.current;
|
|
16381
|
+
if (scrollHeight - scrollTop - clientHeight <= infiniteScrollDistance) {
|
|
16382
|
+
onInfiniteScroll?.();
|
|
15473
16383
|
}
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
// ignores that, or a tap, can leave it elsewhere.
|
|
15484
|
-
const input = inputRef.current;
|
|
15485
|
-
if (input && input.isConnected && getActiveElementInTree(input) !== input) {
|
|
15486
|
-
input.focus();
|
|
16384
|
+
}, [checkInfiniteScroll, infiniteScrollDistance, onInfiniteScroll]);
|
|
16385
|
+
// Scroll highlighted item into view
|
|
16386
|
+
React.useEffect(() => {
|
|
16387
|
+
if (highlightedIndex >= 0 && dropdownRef.current) {
|
|
16388
|
+
const highlightedEl = dropdownRef.current.querySelector(`[data-index="${highlightedIndex}"]`);
|
|
16389
|
+
if (highlightedEl &&
|
|
16390
|
+
typeof highlightedEl.scrollIntoView === 'function') {
|
|
16391
|
+
highlightedEl.scrollIntoView({ block: 'nearest' });
|
|
16392
|
+
}
|
|
15487
16393
|
}
|
|
15488
|
-
};
|
|
15489
|
-
|
|
15490
|
-
const
|
|
15491
|
-
|
|
15492
|
-
|
|
15493
|
-
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
refresh();
|
|
15506
|
-
};
|
|
15507
|
-
}, []);
|
|
15508
|
-
return {
|
|
15509
|
-
open,
|
|
15510
|
-
expanded,
|
|
15511
|
-
highlighted,
|
|
15512
|
-
typed,
|
|
15513
|
-
listboxId,
|
|
15514
|
-
itemId,
|
|
15515
|
-
rootRef,
|
|
15516
|
-
inputRef,
|
|
15517
|
-
listboxRef,
|
|
15518
|
-
registerPopup,
|
|
15519
|
-
registerItem,
|
|
15520
|
-
onKeyDown,
|
|
15521
|
-
onChange,
|
|
15522
|
-
onClick,
|
|
15523
|
-
onBlur,
|
|
15524
|
-
hover,
|
|
15525
|
-
pick,
|
|
15526
|
-
keepFocus,
|
|
15527
|
-
markInside,
|
|
15528
|
-
};
|
|
15529
|
-
}
|
|
15530
|
-
|
|
15531
|
-
/**
|
|
15532
|
-
* Bulma Input component with full Bulma helper class support.
|
|
15533
|
-
*
|
|
15534
|
-
* @function
|
|
15535
|
-
* @param {InputBaseProps} props - Props for the Input component.
|
|
15536
|
-
* @returns {JSX.Element} The rendered input element.
|
|
15537
|
-
* @see {@link https://bulma.io/documentation/form/input/ | Bulma Input documentation}
|
|
15538
|
-
*/
|
|
15539
|
-
const InputBase = React.forwardRef(({ color, size, isRounded, isStatic, isHovered, isFocused, isLoading, className, disabled, readOnly, ...props }, ref) => {
|
|
15540
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
15541
|
-
color,
|
|
15542
|
-
...props,
|
|
16394
|
+
}, [highlightedIndex]);
|
|
16395
|
+
// Combined ref
|
|
16396
|
+
const combinedRef = React.useCallback((node) => {
|
|
16397
|
+
inputRef.current =
|
|
16398
|
+
node;
|
|
16399
|
+
if (typeof ref === 'function') {
|
|
16400
|
+
ref(node);
|
|
16401
|
+
}
|
|
16402
|
+
else if (ref) {
|
|
16403
|
+
ref.current =
|
|
16404
|
+
node;
|
|
16405
|
+
}
|
|
16406
|
+
}, [ref]);
|
|
16407
|
+
// Generate classes
|
|
16408
|
+
const autocompleteClasses = usePrefixedClassNames('autocomplete', {
|
|
16409
|
+
'is-active': isActive,
|
|
16410
|
+
[`is-${size}`]: !!size,
|
|
15543
16411
|
});
|
|
15544
|
-
|
|
15545
|
-
// an undefined own key), reported to that Field (#1004).
|
|
15546
|
-
const id = useFieldLabelTarget(rest.id);
|
|
15547
|
-
const mainClass = usePrefixedClassNames('input', {
|
|
16412
|
+
const inputClasses = usePrefixedClassNames('input', {
|
|
15548
16413
|
[`is-${color}`]: !!color,
|
|
15549
16414
|
[`is-${size}`]: !!size,
|
|
15550
|
-
'is-
|
|
15551
|
-
'is-static': isStatic,
|
|
15552
|
-
'is-hovered': isHovered,
|
|
15553
|
-
'is-focused': isFocused,
|
|
15554
|
-
'is-loading': isLoading,
|
|
15555
|
-
});
|
|
15556
|
-
const inputClass = classNames(mainClass, bulmaHelperClasses, className);
|
|
15557
|
-
return (jsxRuntime.jsx("input", { ref: ref, className: inputClass, disabled: disabled, readOnly: readOnly, ...rest,
|
|
15558
|
-
// After the spread, which may carry an undefined id key.
|
|
15559
|
-
id: id }));
|
|
15560
|
-
});
|
|
15561
|
-
InputBase.displayName = 'InputBase';
|
|
15562
|
-
|
|
15563
|
-
/**
|
|
15564
|
-
* The `Input` component provides a Bulma-styled text input, supporting colors, sizes, rounded corners, static state, hover/focus/loading states, and all Bulma helper props.
|
|
15565
|
-
*
|
|
15566
|
-
* @function
|
|
15567
|
-
* @param {InputProps} props - Props for Input.
|
|
15568
|
-
* @returns {JSX.Element} The composed field element.
|
|
15569
|
-
*
|
|
15570
|
-
* @example
|
|
15571
|
-
* <Input label="Username" placeholder="Enter username" iconLeftName="user" />
|
|
15572
|
-
*
|
|
15573
|
-
* @example
|
|
15574
|
-
* <Input
|
|
15575
|
-
* label="Email"
|
|
15576
|
-
* type="email"
|
|
15577
|
-
* message="Please enter a valid email"
|
|
15578
|
-
* messageColor="danger"
|
|
15579
|
-
* color="danger"
|
|
15580
|
-
* />
|
|
15581
|
-
*/
|
|
15582
|
-
const Input = React.forwardRef(({
|
|
15583
|
-
// Field props
|
|
15584
|
-
label, labelSize, labelProps, horizontal,
|
|
15585
|
-
// Control props
|
|
15586
|
-
iconLeft, iconRight, iconLeftName, iconRightName, iconLeftSize, iconRightSize, hasIconsLeft, hasIconsRight, isLoading, isExpanded, controlSize,
|
|
15587
|
-
// Message props
|
|
15588
|
-
message, messageColor,
|
|
15589
|
-
// Container class overrides
|
|
15590
|
-
fieldClassName, controlClassName,
|
|
15591
|
-
// Everything else goes to Input
|
|
15592
|
-
...inputProps }, ref) => {
|
|
15593
|
-
const insideField = useInsideField();
|
|
15594
|
-
const insideControl = useInsideControl();
|
|
15595
|
-
const ownField = rendersOwnField('Input', {
|
|
15596
|
-
insideField,
|
|
15597
|
-
insideControl,
|
|
15598
|
-
label,
|
|
15599
|
-
message,
|
|
15600
|
-
horizontal,
|
|
15601
|
-
fieldClassName,
|
|
15602
|
-
});
|
|
15603
|
-
const ownControl = rendersOwnControl('Input', {
|
|
15604
|
-
insideControl,
|
|
15605
|
-
isLoading,
|
|
15606
|
-
iconLeft,
|
|
15607
|
-
iconLeftName,
|
|
15608
|
-
iconLeftSize,
|
|
15609
|
-
iconRight,
|
|
15610
|
-
iconRightName,
|
|
15611
|
-
iconRightSize,
|
|
15612
|
-
hasIconsLeft,
|
|
15613
|
-
hasIconsRight,
|
|
15614
|
-
isExpanded,
|
|
15615
|
-
controlSize,
|
|
15616
|
-
controlClassName,
|
|
16415
|
+
'is-loading': loading,
|
|
15617
16416
|
});
|
|
15618
|
-
const
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
labelProps,
|
|
15622
|
-
rendersLabel: ownField,
|
|
16417
|
+
const controlClasses = usePrefixedClassNames('control', 'is-expanded', 'has-icons-right');
|
|
16418
|
+
const dropdownMenuClasses = usePrefixedClassNames('dropdown-menu', {
|
|
16419
|
+
'is-active': menuOpen,
|
|
15623
16420
|
});
|
|
16421
|
+
const dropdownContentClass = usePrefixedClassNames('dropdown-content');
|
|
16422
|
+
const dropdownHeaderClass = usePrefixedClassNames('dropdown-header');
|
|
16423
|
+
const dropdownFooterClass = usePrefixedClassNames('dropdown-footer');
|
|
16424
|
+
const emptyItemClasses = usePrefixedClassNames('dropdown-item', 'has-text-grey');
|
|
16425
|
+
const iconRightClickableClass = usePrefixedClassNames('icon', 'is-right', 'is-clickable');
|
|
16426
|
+
const iconRightClass = usePrefixedClassNames('icon', 'is-right');
|
|
16427
|
+
const loaderClass = usePrefixedClassNames('loader', 'is-loading');
|
|
16428
|
+
const combinedClasses = classNames(autocompleteClasses, bulmaHelperClasses, className);
|
|
15624
16429
|
const helpClass = usePrefixedClassNames('help', {
|
|
15625
16430
|
[`is-${messageColor}`]: !!messageColor,
|
|
15626
16431
|
});
|
|
15627
|
-
let content = jsxRuntime.jsx(InputBase, { ref: ref, ...inputProps, id: controlId });
|
|
15628
|
-
if (ownControl) {
|
|
15629
|
-
content = (jsxRuntime.jsx(Control, { iconLeft: iconLeft, iconRight: iconRight, iconLeftName: iconLeftName, iconRightName: iconRightName, iconLeftSize: iconLeftSize, iconRightSize: iconRightSize, hasIconsLeft: hasIconsLeft, hasIconsRight: hasIconsRight, isLoading: isLoading, isExpanded: isExpanded, size: controlSize, className: controlClassName, children: content }));
|
|
15630
|
-
}
|
|
15631
16432
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
15632
|
-
|
|
15633
|
-
|
|
16433
|
+
// The clear button gives way to a spinner at its spot: the `loading` one,
|
|
16434
|
+
// or that of a loading Control this sits in.
|
|
16435
|
+
const controlLoading = useControlLoading();
|
|
16436
|
+
const showClear = clearable && !!inputValue && !disabled && !loading && !controlLoading;
|
|
16437
|
+
// The combobox controls its listbox and points at the highlighted option.
|
|
16438
|
+
// The listbox takes its name from the label that names the combobox, the
|
|
16439
|
+
// one this renders or a surrounding Field's, so the two say the same
|
|
16440
|
+
// thing. When no label names the combobox, or the one that does has no
|
|
16441
|
+
// id to point at, the list keeps a fallback name. While the dropdown
|
|
16442
|
+
// shows only the `empty` content there's no listbox, so the combobox is
|
|
16443
|
+
// collapsed and points at nothing.
|
|
16444
|
+
const activeOptionId = listOpen &&
|
|
16445
|
+
highlightedIndex >= 0 &&
|
|
16446
|
+
highlightedIndex < filteredData.length
|
|
16447
|
+
? `${listboxId}-${highlightedIndex}`
|
|
16448
|
+
: undefined;
|
|
16449
|
+
// A polite status region, in the page from the start so it's there
|
|
16450
|
+
// before anything is written into it, announces the `empty` content
|
|
16451
|
+
// when the user's typing leaves nothing that matches what's in the
|
|
16452
|
+
// field. It follows what's on screen, so results that come in after the
|
|
16453
|
+
// typing count too. It's said once each time that happens, not again on
|
|
16454
|
+
// each keystroke while nothing matches. An empty field isn't a search,
|
|
16455
|
+
// so it stays quiet then, even with no data. While `loading`, or a
|
|
16456
|
+
// loading Control around it, says results are on their way, it waits,
|
|
16457
|
+
// and it's said once loading ends with nothing matching.
|
|
16458
|
+
const emptyItems = typed &&
|
|
16459
|
+
menuOpen &&
|
|
16460
|
+
!listOpen &&
|
|
16461
|
+
!!inputValue &&
|
|
16462
|
+
!loading &&
|
|
16463
|
+
!controlLoading
|
|
16464
|
+
? typedNoMatches$1
|
|
16465
|
+
: nothingToAnnounce$1;
|
|
16466
|
+
// The region reads the text on screen, whatever `empty` renders.
|
|
16467
|
+
const emptyRef = React.useRef(null);
|
|
16468
|
+
const describeEmpty = React.useCallback(() => {
|
|
16469
|
+
const node = emptyRef.current;
|
|
16470
|
+
/* istanbul ignore if: it's only announced while it's on screen */
|
|
16471
|
+
if (!node)
|
|
16472
|
+
return null;
|
|
16473
|
+
return spokenText(node);
|
|
16474
|
+
}, []);
|
|
16475
|
+
const autocompleteElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, ...rest, children: [jsxRuntime.jsxs("div", { className: controlClasses, children: [jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: placeholder, disabled: disabled, name: name, form: form, required: required, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, role: "combobox", "aria-expanded": listOpen, "aria-controls": listOpen ? listboxId : undefined, "aria-activedescendant": activeOptionId, "aria-haspopup": "listbox", "aria-autocomplete": "list", autoComplete: "off" }), showClear && (jsxRuntime.jsx("button", { type: "button", className: iconRightClickableClass, onClick: handleClear, "aria-label": "Clear", children: jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", width: "16", height: "16", "aria-hidden": "true", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), menuOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsxs("div", { ref: dropdownRef, className: dropdownContentClass, style: { maxHeight: `${maxHeight}px`, overflowY: 'auto' }, onScroll: handleDropdownScroll, children: [header && jsxRuntime.jsx("div", { className: dropdownHeaderClass, children: header }), listOpen ? (jsxRuntime.jsx("div", { id: listboxId, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', onMouseDown: keepFocus, children: filteredData.map((item, index) => {
|
|
16476
|
+
const isDisabled = typeof item !== 'string' && item.disabled;
|
|
16477
|
+
const isHighlighted = index === highlightedIndex;
|
|
16478
|
+
const itemClasses = prefixedClassNames(classPrefix, 'dropdown-item', {
|
|
16479
|
+
'is-active': isHighlighted,
|
|
16480
|
+
'is-disabled': isDisabled,
|
|
16481
|
+
});
|
|
16482
|
+
return (jsxRuntime.jsx("a", { id: `${listboxId}-${index}`, "data-index": index, className: itemClasses, onClick: () => !isDisabled && handleSelect(item), onMouseEnter: () => !isDisabled && setHighlightedIndex(index), role: "option", "aria-selected": isHighlighted && !isDisabled, "aria-disabled": isDisabled, children: itemTemplate
|
|
16483
|
+
? itemTemplate(item)
|
|
16484
|
+
: getDisplayValue(item) }, index));
|
|
16485
|
+
}) })) : (jsxRuntime.jsx("div", { ref: emptyRef, className: emptyItemClasses, onMouseDown: keepFocus, children: empty })), footer && jsxRuntime.jsx("div", { className: dropdownFooterClass, children: footer })] }) })), jsxRuntime.jsx(StatusRegion, { items: emptyItems, describe: describeEmpty })] }));
|
|
16486
|
+
if (!insideField) {
|
|
16487
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [autocompleteElement, messageEl] }));
|
|
15634
16488
|
}
|
|
15635
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
16489
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [autocompleteElement, messageEl] }));
|
|
15636
16490
|
});
|
|
15637
|
-
|
|
16491
|
+
Autocomplete.displayName = 'Autocomplete';
|
|
15638
16492
|
|
|
15639
16493
|
const validComboboxSizes = ['small', 'medium', 'large'];
|
|
15640
16494
|
// What the status region announces: the `Combobox.Empty` content, as the one
|
|
@@ -23462,6 +24316,7 @@ exports.Carousel = Carousel;
|
|
|
23462
24316
|
exports.CarouselItem = CarouselItem;
|
|
23463
24317
|
exports.Cell = Cell;
|
|
23464
24318
|
exports.Checkbox = Checkbox;
|
|
24319
|
+
exports.CheckboxCard = CheckboxCard;
|
|
23465
24320
|
exports.Checkboxes = Checkboxes;
|
|
23466
24321
|
exports.ClientOnly = ClientOnly;
|
|
23467
24322
|
exports.Code = Code;
|
|
@@ -23476,6 +24331,13 @@ exports.ComboboxInput = ComboboxInput;
|
|
|
23476
24331
|
exports.ComboboxItem = ComboboxItem;
|
|
23477
24332
|
exports.ComboboxList = ComboboxList;
|
|
23478
24333
|
exports.ComboboxMenu = ComboboxMenu;
|
|
24334
|
+
exports.Command = Command;
|
|
24335
|
+
exports.CommandDialog = CommandDialog;
|
|
24336
|
+
exports.CommandEmpty = CommandEmpty;
|
|
24337
|
+
exports.CommandGroup = CommandGroup;
|
|
24338
|
+
exports.CommandInput = CommandInput;
|
|
24339
|
+
exports.CommandItem = CommandItem;
|
|
24340
|
+
exports.CommandList = CommandList;
|
|
23479
24341
|
exports.ConfigProvider = ConfigProvider;
|
|
23480
24342
|
exports.Container = Container;
|
|
23481
24343
|
exports.Content = Content;
|
|
@@ -23584,6 +24446,7 @@ exports.Portal = Portal;
|
|
|
23584
24446
|
exports.Pre = Pre;
|
|
23585
24447
|
exports.Progress = Progress;
|
|
23586
24448
|
exports.Radio = Radio;
|
|
24449
|
+
exports.RadioCard = RadioCard;
|
|
23587
24450
|
exports.Radios = Radios;
|
|
23588
24451
|
exports.Rate = Rate;
|
|
23589
24452
|
exports.Reveal = Reveal;
|