@allxsmith/bestax-bulma 5.54.0 → 5.55.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 +22 -22
- package/dist/esm/components/Command.js.map +1 -1
- package/dist/esm/components/DataTable.js +215 -0
- package/dist/esm/components/DataTable.js.map +1 -0
- package/dist/esm/components/useDataTable.js +482 -0
- package/dist/esm/components/useDataTable.js.map +1 -0
- package/dist/esm/elements/ColorSwatch.js +49 -0
- package/dist/esm/elements/ColorSwatch.js.map +1 -0
- package/dist/esm/elements/RingProgress.js +3 -3
- package/dist/esm/elements/Td.js +4 -2
- package/dist/esm/elements/Td.js.map +1 -1
- package/dist/esm/elements/Th.js +13 -5
- package/dist/esm/elements/Th.js.map +1 -1
- package/dist/esm/elements/Tr.js.map +1 -1
- package/dist/esm/form/ColorPicker.js +540 -0
- package/dist/esm/form/ColorPicker.js.map +1 -0
- package/dist/esm/form/Combobox.js +18 -18
- package/dist/esm/form/Combobox.js.map +1 -1
- package/dist/esm/form/Dropzone.js +524 -0
- package/dist/esm/form/Dropzone.js.map +1 -0
- package/dist/esm/form/File.js.map +1 -1
- package/dist/esm/form/_colorInternals/colorMath.js +292 -0
- package/dist/esm/form/_colorInternals/colorMath.js.map +1 -0
- package/dist/esm/form/_colorInternals/pickerState.js +160 -0
- package/dist/esm/form/_colorInternals/pickerState.js.map +1 -0
- package/dist/esm/helpers/statusRegion.js +3 -3
- package/dist/esm/helpers/statusRegion.js.map +1 -1
- package/dist/esm/helpers/useFocusTrap.js +2 -2
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +2986 -796
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +5 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/DataTable.d.ts +120 -0
- package/dist/types/components/useDataTable.d.ts +445 -0
- package/dist/types/elements/ColorSwatch.d.ts +54 -0
- package/dist/types/elements/Td.d.ts +9 -1
- package/dist/types/elements/Th.d.ts +14 -2
- package/dist/types/elements/Tr.d.ts +10 -1
- package/dist/types/form/ColorPicker.d.ts +266 -0
- package/dist/types/form/Dropzone.d.ts +138 -0
- package/dist/types/form/File.d.ts +2 -0
- package/dist/types/form/_colorInternals/colorMath.d.ts +63 -0
- package/dist/types/form/_colorInternals/pickerState.d.ts +62 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types-cjs/components/DataTable.d.ts +120 -0
- package/dist/types-cjs/components/useDataTable.d.ts +445 -0
- package/dist/types-cjs/elements/ColorSwatch.d.ts +54 -0
- package/dist/types-cjs/elements/Td.d.ts +9 -1
- package/dist/types-cjs/elements/Th.d.ts +14 -2
- package/dist/types-cjs/elements/Tr.d.ts +10 -1
- package/dist/types-cjs/form/ColorPicker.d.ts +266 -0
- package/dist/types-cjs/form/Dropzone.d.ts +138 -0
- package/dist/types-cjs/form/File.d.ts +2 -0
- package/dist/types-cjs/form/_colorInternals/colorMath.d.ts +63 -0
- package/dist/types-cjs/form/_colorInternals/pickerState.d.ts +62 -0
- package/dist/types-cjs/index.d.ts +4 -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/_datatable.scss +152 -0
- package/src/scss/components/_index.scss +1 -0
- package/src/scss/elements/_colorswatch.scss +100 -0
- package/src/scss/elements/_index.scss +1 -0
- package/src/scss/form/_checkbox.scss +53 -0
- package/src/scss/form/_colorpicker.scss +354 -0
- package/src/scss/form/_dropzone.scss +233 -0
- package/src/scss/form/_file.scss +29 -1
- package/src/scss/form/_index.scss +2 -0
package/dist/index.cjs
CHANGED
|
@@ -4530,7 +4530,7 @@ function flatChildren(el) {
|
|
|
4530
4530
|
}
|
|
4531
4531
|
return Array.from(el.children);
|
|
4532
4532
|
}
|
|
4533
|
-
const isShown$
|
|
4533
|
+
const isShown$2 = (el) => !el.hasAttribute('inert') && getComputedStyle(el).display !== 'none';
|
|
4534
4534
|
const isVisible = (el) => {
|
|
4535
4535
|
const { visibility } = getComputedStyle(el);
|
|
4536
4536
|
return visibility !== 'hidden' && visibility !== 'collapse';
|
|
@@ -4544,7 +4544,7 @@ const isVisible = (el) => {
|
|
|
4544
4544
|
function isShownUpTo(el, top) {
|
|
4545
4545
|
let below = null;
|
|
4546
4546
|
for (let node = el; node && node !== top; node = flatParent(node)) {
|
|
4547
|
-
if (!isShown$
|
|
4547
|
+
if (!isShown$2(node))
|
|
4548
4548
|
return false;
|
|
4549
4549
|
if (below &&
|
|
4550
4550
|
node.localName === 'details' &&
|
|
@@ -8578,11 +8578,11 @@ const announceDelay = 100;
|
|
|
8578
8578
|
* again.
|
|
8579
8579
|
*/
|
|
8580
8580
|
const announcementLifetime = 1000;
|
|
8581
|
-
const noAnnouncements = [];
|
|
8581
|
+
const noAnnouncements$1 = [];
|
|
8582
8582
|
const nothingWritten = [];
|
|
8583
8583
|
const noAnnouncementsYet = {
|
|
8584
8584
|
key: 0,
|
|
8585
|
-
waiting: noAnnouncements,
|
|
8585
|
+
waiting: noAnnouncements$1,
|
|
8586
8586
|
written: nothingWritten,
|
|
8587
8587
|
};
|
|
8588
8588
|
// Hides the region with inline styles rather than a class, so it needs no
|
|
@@ -8684,7 +8684,7 @@ function useAnnouncements(items, describe) {
|
|
|
8684
8684
|
setAnnouncements(current => current.key === key
|
|
8685
8685
|
? {
|
|
8686
8686
|
...current,
|
|
8687
|
-
waiting: noAnnouncements,
|
|
8687
|
+
waiting: noAnnouncements$1,
|
|
8688
8688
|
written: [
|
|
8689
8689
|
...current.written,
|
|
8690
8690
|
...current.waiting
|
|
@@ -12697,12 +12697,12 @@ const defaultEmpty = 'No results';
|
|
|
12697
12697
|
const typedNoMatches$2 = [{ id: 'empty' }];
|
|
12698
12698
|
const nothingToAnnounce$2 = [];
|
|
12699
12699
|
/** Whether a slot was given something to show. */
|
|
12700
|
-
const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
|
|
12701
|
-
const noop$
|
|
12702
|
-
const registerNothing$1 = () => noop$
|
|
12700
|
+
const isShown$1 = (node) => node !== undefined && node !== null && node !== false && node !== '';
|
|
12701
|
+
const noop$2 = () => { };
|
|
12702
|
+
const registerNothing$1 = () => noop$2;
|
|
12703
12703
|
// What a part rendered outside a Command reads: nothing to report to, so it
|
|
12704
12704
|
// renders its markup and does nothing.
|
|
12705
|
-
const standalone$
|
|
12705
|
+
const standalone$2 = {
|
|
12706
12706
|
engine: {
|
|
12707
12707
|
open: false,
|
|
12708
12708
|
expanded: false,
|
|
@@ -12715,26 +12715,26 @@ const standalone$1 = {
|
|
|
12715
12715
|
listboxRef: { current: null },
|
|
12716
12716
|
registerPopup: registerNothing$1,
|
|
12717
12717
|
registerItem: registerNothing$1,
|
|
12718
|
-
onKeyDown: noop$
|
|
12719
|
-
onChange: noop$
|
|
12720
|
-
onClick: noop$
|
|
12721
|
-
onBlur: noop$
|
|
12722
|
-
hover: noop$
|
|
12723
|
-
pick: noop$
|
|
12724
|
-
keepFocus: noop$
|
|
12725
|
-
markInside: noop$
|
|
12718
|
+
onKeyDown: noop$2,
|
|
12719
|
+
onChange: noop$2,
|
|
12720
|
+
onClick: noop$2,
|
|
12721
|
+
onBlur: noop$2,
|
|
12722
|
+
hover: noop$2,
|
|
12723
|
+
pick: noop$2,
|
|
12724
|
+
keepFocus: noop$2,
|
|
12725
|
+
markInside: noop$2,
|
|
12726
12726
|
},
|
|
12727
12727
|
label: defaultLabel,
|
|
12728
12728
|
query: '',
|
|
12729
|
-
onFieldChange: noop$
|
|
12730
|
-
reportLoading: noop$
|
|
12729
|
+
onFieldChange: noop$2,
|
|
12730
|
+
reportLoading: noop$2,
|
|
12731
12731
|
reportEmpty: registerNothing$1,
|
|
12732
12732
|
registerHandler: registerNothing$1,
|
|
12733
12733
|
};
|
|
12734
12734
|
const CommandContext = React.createContext(null);
|
|
12735
|
-
const useCommand = () => React.useContext(CommandContext) ?? standalone$
|
|
12735
|
+
const useCommand = () => React.useContext(CommandContext) ?? standalone$2;
|
|
12736
12736
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
12737
|
-
function setRefs$
|
|
12737
|
+
function setRefs$2(node, own, forwarded) {
|
|
12738
12738
|
own.current = node;
|
|
12739
12739
|
if (typeof forwarded === 'function')
|
|
12740
12740
|
forwarded(node);
|
|
@@ -12851,7 +12851,7 @@ const CommandParts = React.forwardRef(({ children, label, query: controlledQuery
|
|
|
12851
12851
|
registerHandler,
|
|
12852
12852
|
};
|
|
12853
12853
|
const { rootRef } = engine;
|
|
12854
|
-
const rootRefs = React.useCallback((node) => setRefs$
|
|
12854
|
+
const rootRefs = React.useCallback((node) => setRefs$2(node, rootRef, ref), [rootRef, ref]);
|
|
12855
12855
|
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 })] }));
|
|
12856
12856
|
});
|
|
12857
12857
|
CommandParts.displayName = 'CommandParts';
|
|
@@ -12871,7 +12871,7 @@ const CommandInput = React.forwardRef(({ isLoading, iconLeft, iconLeftName, onKe
|
|
|
12871
12871
|
return () => reportLoading(false);
|
|
12872
12872
|
}, [reportLoading, isLoading]);
|
|
12873
12873
|
const { inputRef } = engine;
|
|
12874
|
-
const inputRefs = React.useCallback((node) => setRefs$
|
|
12874
|
+
const inputRefs = React.useCallback((node) => setRefs$2(node, inputRef, ref), [inputRef, ref]);
|
|
12875
12875
|
const { expanded, highlighted } = engine;
|
|
12876
12876
|
// A name the caller gave the field wins over the Command's `label`.
|
|
12877
12877
|
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
@@ -12960,10 +12960,10 @@ const CommandItem = ({ value, disabled = false, onSelect, icon, description, hin
|
|
|
12960
12960
|
// the rest, and described by the description and the hint.
|
|
12961
12961
|
const baseId = React.useId();
|
|
12962
12962
|
const labelId = `${baseId}label`;
|
|
12963
|
-
const descriptionId = isShown(description)
|
|
12963
|
+
const descriptionId = isShown$1(description)
|
|
12964
12964
|
? `${baseId}description`
|
|
12965
12965
|
: undefined;
|
|
12966
|
-
const hintId = isShown(hint) ? `${baseId}hint` : undefined;
|
|
12966
|
+
const hintId = isShown$1(hint) ? `${baseId}hint` : undefined;
|
|
12967
12967
|
const describedBy = [descriptionId, hintId].filter(Boolean).join(' ') || undefined;
|
|
12968
12968
|
const callerNamed = !!props['aria-label'] || 'aria-labelledby' in props;
|
|
12969
12969
|
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 => {
|
|
@@ -12974,7 +12974,7 @@ const CommandItem = ({ value, disabled = false, onSelect, icon, description, hin
|
|
|
12974
12974
|
onPointerMove?.(event);
|
|
12975
12975
|
if (!disabled && !highlighted)
|
|
12976
12976
|
engine.hover(value);
|
|
12977
|
-
}, 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 }))] }));
|
|
12977
|
+
}, children: [isShown$1(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 }))] }));
|
|
12978
12978
|
};
|
|
12979
12979
|
CommandItem.displayName = 'CommandItem';
|
|
12980
12980
|
/**
|
|
@@ -13013,7 +13013,7 @@ const CommandActionsContent = ({ actions, filter, placeholder, empty, }) => {
|
|
|
13013
13013
|
sections.set(group, [...(sections.get(group) ?? []), action]);
|
|
13014
13014
|
}
|
|
13015
13015
|
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));
|
|
13016
|
-
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] }));
|
|
13016
|
+
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$1(empty) ? (jsxRuntime.jsx(CommandEmpty, { children: empty })) : null] }));
|
|
13017
13017
|
};
|
|
13018
13018
|
/**
|
|
13019
13019
|
* The Command given `actions`. It renders the parts itself and filters the
|
|
@@ -13316,343 +13316,1026 @@ const Command = withSubComponents(CommandRoot, {
|
|
|
13316
13316
|
}, 'Command');
|
|
13317
13317
|
|
|
13318
13318
|
/**
|
|
13319
|
-
*
|
|
13319
|
+
* Thead component for rendering a styled Bulma table header.
|
|
13320
|
+
*
|
|
13321
|
+
* Supports Bulma helper classes for additional styling.
|
|
13320
13322
|
*
|
|
13321
13323
|
* @function
|
|
13322
|
-
* @param {
|
|
13323
|
-
* @returns {JSX.Element} The rendered
|
|
13324
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
13324
|
+
* @param {TheadProps} props - Props for the Thead component.
|
|
13325
|
+
* @returns {JSX.Element} The rendered table header element.
|
|
13326
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
|
|
13325
13327
|
*/
|
|
13326
|
-
const
|
|
13328
|
+
const Thead = ({ className, children, ...props }) => {
|
|
13327
13329
|
/**
|
|
13328
13330
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13329
13331
|
*/
|
|
13330
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13331
|
-
|
|
13332
|
-
|
|
13333
|
-
...props,
|
|
13334
|
-
});
|
|
13335
|
-
const bulmaClasses = usePrefixedClassNames('block');
|
|
13336
|
-
const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13337
|
-
return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
|
|
13332
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13333
|
+
const theadClasses = classNames(className, bulmaHelperClasses);
|
|
13334
|
+
return (jsxRuntime.jsx("thead", { className: theadClasses || undefined, ...rest, children: children }));
|
|
13338
13335
|
};
|
|
13339
13336
|
|
|
13340
13337
|
/**
|
|
13341
|
-
*
|
|
13338
|
+
* Tbody component for rendering a styled Bulma table body.
|
|
13339
|
+
*
|
|
13340
|
+
* Supports Bulma helper classes for additional styling.
|
|
13342
13341
|
*
|
|
13343
13342
|
* @function
|
|
13344
|
-
* @param {
|
|
13345
|
-
* @returns {JSX.Element} The rendered
|
|
13346
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
13343
|
+
* @param {TbodyProps} props - Props for the Tbody component.
|
|
13344
|
+
* @returns {JSX.Element} The rendered table body element.
|
|
13345
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
|
|
13347
13346
|
*/
|
|
13348
|
-
const
|
|
13347
|
+
const Tbody = ({ className, children, ...props }) => {
|
|
13349
13348
|
/**
|
|
13350
13349
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13351
13350
|
*/
|
|
13352
|
-
const { bulmaHelperClasses,
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
...props,
|
|
13356
|
-
});
|
|
13357
|
-
const bulmaClasses = usePrefixedClassNames('box', {
|
|
13358
|
-
'is-shadowless': !hasShadow,
|
|
13359
|
-
});
|
|
13360
|
-
const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13361
|
-
return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
|
|
13351
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13352
|
+
const tbodyClasses = classNames(className, bulmaHelperClasses);
|
|
13353
|
+
return (jsxRuntime.jsx("tbody", { className: tbodyClasses || undefined, ...rest, children: children }));
|
|
13362
13354
|
};
|
|
13363
13355
|
|
|
13364
13356
|
/**
|
|
13365
|
-
*
|
|
13357
|
+
* Tfoot component for rendering a styled Bulma table footer.
|
|
13358
|
+
*
|
|
13359
|
+
* Supports Bulma helper classes for additional styling.
|
|
13366
13360
|
*
|
|
13367
13361
|
* @function
|
|
13368
|
-
* @param {
|
|
13369
|
-
* @returns {JSX.Element} The rendered
|
|
13362
|
+
* @param {TfootProps} props - Props for the Tfoot component.
|
|
13363
|
+
* @returns {JSX.Element} The rendered table footer element.
|
|
13364
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-footer | Bulma Table documentation}
|
|
13370
13365
|
*/
|
|
13371
|
-
const
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13379
|
-
return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
|
|
13366
|
+
const Tfoot = ({ className, children, ...props }) => {
|
|
13367
|
+
/**
|
|
13368
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
13369
|
+
*/
|
|
13370
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13371
|
+
const tfootClasses = classNames(className, bulmaHelperClasses);
|
|
13372
|
+
return (jsxRuntime.jsx("tfoot", { className: tfootClasses || undefined, ...rest, children: children }));
|
|
13380
13373
|
};
|
|
13381
13374
|
|
|
13382
|
-
// Valid size modifiers for the content class
|
|
13383
|
-
const validSizes = ['small', 'medium', 'large'];
|
|
13384
13375
|
/**
|
|
13385
|
-
* The `
|
|
13376
|
+
* The `isAligned` values `Th` and `Td` accept. Each one writes
|
|
13377
|
+
* `has-text-<value>` on the cell, and a value outside the tuple adds no class.
|
|
13378
|
+
*
|
|
13379
|
+
* One copy, imported by both cells, so a header and its body cells always
|
|
13380
|
+
* take the same values.
|
|
13381
|
+
*
|
|
13382
|
+
* Its own module rather than `Th.tsx` or `Td.tsx` because `src/index.ts`
|
|
13383
|
+
* wildcard-exports those files, and everything published there is public
|
|
13384
|
+
* forever. `iconProps.ts` makes the same call.
|
|
13385
|
+
*/
|
|
13386
|
+
const validTableAlignments = ['left', 'right', 'centered'];
|
|
13387
|
+
|
|
13388
|
+
/**
|
|
13389
|
+
* The values the table `color` prop accepts, as a readonly tuple.
|
|
13390
|
+
*
|
|
13391
|
+
* `TableColor` is typed from it, and `Tr`, `Th` and `Td` all take a
|
|
13392
|
+
* `TableColor`, so the tuple and those props list the same values. Map over
|
|
13393
|
+
* it to build a color picker, or check a value that arrives at runtime before
|
|
13394
|
+
* passing it in: the components add no color class for a value outside the
|
|
13395
|
+
* tuple.
|
|
13396
|
+
*
|
|
13397
|
+
* @example
|
|
13398
|
+
* import { Tr, Td, validTableColors } from '@allxsmith/bestax-bulma';
|
|
13399
|
+
*
|
|
13400
|
+
* <Tr>
|
|
13401
|
+
* {validTableColors.map(color => (
|
|
13402
|
+
* <Td key={color} color={color}>
|
|
13403
|
+
* {color}
|
|
13404
|
+
* </Td>
|
|
13405
|
+
* ))}
|
|
13406
|
+
* </Tr>;
|
|
13407
|
+
*/
|
|
13408
|
+
const validTableColors = [
|
|
13409
|
+
'primary',
|
|
13410
|
+
'link',
|
|
13411
|
+
'info',
|
|
13412
|
+
'success',
|
|
13413
|
+
'warning',
|
|
13414
|
+
'danger',
|
|
13415
|
+
'black',
|
|
13416
|
+
'dark',
|
|
13417
|
+
'light',
|
|
13418
|
+
'white',
|
|
13419
|
+
];
|
|
13420
|
+
/**
|
|
13421
|
+
* Td component for rendering a styled Bulma table cell.
|
|
13422
|
+
*
|
|
13423
|
+
* Supports alignment, narrow and vertically centered cells, Bulma color modifiers, and helper classes for additional styling.
|
|
13386
13424
|
*
|
|
13387
13425
|
* @function
|
|
13388
|
-
* @param {
|
|
13389
|
-
* @returns {JSX.Element} The rendered
|
|
13390
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
13426
|
+
* @param {TdProps} props - Props for the Td component.
|
|
13427
|
+
* @returns {JSX.Element} The rendered table cell element.
|
|
13428
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
|
|
13391
13429
|
*/
|
|
13392
|
-
const
|
|
13430
|
+
const Td = ({ className, isAligned, isNarrow, isVcentered, color, children, ...props }) => {
|
|
13431
|
+
const bulmaClasses = usePrefixedClassNames('', {
|
|
13432
|
+
[`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
|
|
13433
|
+
[`is-${color}`]: color && validTableColors.includes(color),
|
|
13434
|
+
'is-narrow': isNarrow,
|
|
13435
|
+
'is-vcentered': isVcentered,
|
|
13436
|
+
});
|
|
13393
13437
|
/**
|
|
13394
13438
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13395
13439
|
*/
|
|
13396
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
...props,
|
|
13400
|
-
});
|
|
13401
|
-
const bulmaClasses = usePrefixedClassNames('content', {
|
|
13402
|
-
[`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
|
|
13403
|
-
});
|
|
13404
|
-
const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13405
|
-
return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
|
|
13440
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13441
|
+
const tdClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
13442
|
+
return (jsxRuntime.jsx("td", { className: tdClasses || undefined, ...rest, children: children }));
|
|
13406
13443
|
};
|
|
13407
13444
|
|
|
13408
13445
|
/**
|
|
13409
|
-
*
|
|
13446
|
+
* Tr component for rendering a styled Bulma table row.
|
|
13410
13447
|
*
|
|
13411
|
-
*
|
|
13448
|
+
* Supports the is-selected modifier and color modifiers.
|
|
13412
13449
|
*
|
|
13413
13450
|
* @function
|
|
13414
|
-
* @param {
|
|
13415
|
-
* @returns {JSX.Element} The rendered
|
|
13416
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
13451
|
+
* @param {TrProps} props - Props for the Tr component.
|
|
13452
|
+
* @returns {JSX.Element} The rendered table row element.
|
|
13453
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-row | Bulma Table documentation}
|
|
13417
13454
|
*/
|
|
13418
|
-
const
|
|
13455
|
+
const Tr = ({ className, isSelected, color, children, ...props }) => {
|
|
13456
|
+
const bulmaClasses = usePrefixedClassNames('', {
|
|
13457
|
+
'is-selected': isSelected,
|
|
13458
|
+
[`is-${color}`]: color && validTableColors.includes(color),
|
|
13459
|
+
});
|
|
13419
13460
|
/**
|
|
13420
13461
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13421
13462
|
*/
|
|
13422
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
...props,
|
|
13426
|
-
});
|
|
13427
|
-
const bulmaClasses = usePrefixedClassNames('delete', {
|
|
13428
|
-
[`is-${size}`]: size,
|
|
13429
|
-
'is-disabled': disabled,
|
|
13430
|
-
});
|
|
13431
|
-
const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13432
|
-
return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
|
|
13463
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13464
|
+
const trClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
13465
|
+
return (jsxRuntime.jsx("tr", { className: trClasses || undefined, ...rest, children: children }));
|
|
13433
13466
|
};
|
|
13434
13467
|
|
|
13435
|
-
const validDescriptionListOrientations = ['vertical', 'horizontal'];
|
|
13436
|
-
const isOrientation = (orientation) => !!orientation &&
|
|
13437
|
-
validDescriptionListOrientations.includes(orientation);
|
|
13438
|
-
/** Whether a prop was given something to render. */
|
|
13439
|
-
const isGiven = (node) => node !== undefined &&
|
|
13440
|
-
node !== null &&
|
|
13441
|
-
typeof node !== 'boolean' &&
|
|
13442
|
-
node !== '';
|
|
13443
13468
|
/**
|
|
13444
|
-
*
|
|
13469
|
+
* Th component for rendering a styled Bulma table header cell.
|
|
13445
13470
|
*
|
|
13446
|
-
*
|
|
13447
|
-
* @param {DescriptionListLabelProps} props - Props for DescriptionList.Label.
|
|
13448
|
-
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
|
|
13449
|
-
* @returns {JSX.Element} The rendered label.
|
|
13471
|
+
* Supports alignment, width, narrow and vertically centered cells, and color modifiers.
|
|
13450
13472
|
*
|
|
13451
|
-
* @
|
|
13452
|
-
*
|
|
13473
|
+
* @function
|
|
13474
|
+
* @param {ThProps} props - Props for the Th component.
|
|
13475
|
+
* @returns {JSX.Element} The rendered table header cell element.
|
|
13476
|
+
* @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
|
|
13453
13477
|
*/
|
|
13454
|
-
const
|
|
13455
|
-
const
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13478
|
+
const Th = ({ className, isAligned, isNarrow, isVcentered, width, color, style, children, ...props }) => {
|
|
13479
|
+
const bulmaClasses = usePrefixedClassNames('', {
|
|
13480
|
+
[`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
|
|
13481
|
+
[`is-${color}`]: color && validTableColors.includes(color),
|
|
13482
|
+
'is-narrow': isNarrow,
|
|
13483
|
+
'is-vcentered': isVcentered,
|
|
13459
13484
|
});
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13485
|
+
/**
|
|
13486
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
13487
|
+
*/
|
|
13488
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13489
|
+
const thClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
13490
|
+
return (jsxRuntime.jsx("th", { className: thClasses || undefined, style:
|
|
13491
|
+
// A `width` key the style leaves undefined isn't a width, so the
|
|
13492
|
+
// prop's stays.
|
|
13493
|
+
width && style?.width === undefined
|
|
13494
|
+
? {
|
|
13495
|
+
...style,
|
|
13496
|
+
width: typeof width === 'number' ? `${width}px` : width,
|
|
13497
|
+
}
|
|
13498
|
+
: style, ...rest, children: children }));
|
|
13499
|
+
};
|
|
13500
|
+
|
|
13464
13501
|
/**
|
|
13465
|
-
*
|
|
13502
|
+
* The `Table` component and its subcomponents provide a highly composable, Bulma-styled table system for React.
|
|
13466
13503
|
*
|
|
13467
13504
|
* @function
|
|
13468
|
-
* @param {
|
|
13469
|
-
* @
|
|
13470
|
-
* @
|
|
13471
|
-
*
|
|
13472
|
-
* @example
|
|
13473
|
-
* <DescriptionList.Value>14 October 2026</DescriptionList.Value>
|
|
13505
|
+
* @param {TableProps} props - Props for the Table component.
|
|
13506
|
+
* @returns {JSX.Element} The rendered table element.
|
|
13507
|
+
* @see {@link https://bulma.io/documentation/elements/table/ | Bulma Table documentation}
|
|
13474
13508
|
*/
|
|
13475
|
-
const
|
|
13476
|
-
|
|
13477
|
-
|
|
13478
|
-
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
|
|
13484
|
-
|
|
13509
|
+
const TableComponent = ({ className, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isFullWidth, isResponsive, children, ...props }) => {
|
|
13510
|
+
/**
|
|
13511
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
13512
|
+
*/
|
|
13513
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
13514
|
+
const bulmaClasses = usePrefixedClassNames('table', {
|
|
13515
|
+
'is-bordered': isBordered,
|
|
13516
|
+
'is-striped': isStriped,
|
|
13517
|
+
'is-narrow': isNarrow,
|
|
13518
|
+
'is-hoverable': isHoverable,
|
|
13519
|
+
'is-fullwidth': isFullwidth ?? isFullWidth,
|
|
13520
|
+
});
|
|
13521
|
+
const containerClass = usePrefixedClassNames('table-container');
|
|
13522
|
+
const tableClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
13523
|
+
const tableElement = (jsxRuntime.jsx("table", { className: tableClasses, ...rest, children: children }));
|
|
13524
|
+
if (isResponsive) {
|
|
13525
|
+
return jsxRuntime.jsx("div", { className: containerClass, children: tableElement });
|
|
13526
|
+
}
|
|
13527
|
+
return tableElement;
|
|
13528
|
+
};
|
|
13529
|
+
const Table = withSubComponents(TableComponent, {
|
|
13530
|
+
Thead,
|
|
13531
|
+
Tbody,
|
|
13532
|
+
Tfoot,
|
|
13533
|
+
Tr,
|
|
13534
|
+
Th,
|
|
13535
|
+
Td,
|
|
13536
|
+
}, 'Table');
|
|
13537
|
+
|
|
13485
13538
|
/**
|
|
13486
|
-
*
|
|
13539
|
+
* The `Skeleton` component provides a Bulma-styled skeleton loader for React applications, useful for indicating that content is loading.
|
|
13487
13540
|
*
|
|
13488
13541
|
* @function
|
|
13489
|
-
* @param {
|
|
13490
|
-
* @
|
|
13491
|
-
* @returns {JSX.Element} The rendered item.
|
|
13542
|
+
* @param {SkeletonProps} props - Props for the Skeleton component.
|
|
13543
|
+
* @returns {JSX.Element} The rendered skeleton element.
|
|
13492
13544
|
*
|
|
13493
13545
|
* @example
|
|
13494
|
-
*
|
|
13546
|
+
* // Block skeleton
|
|
13547
|
+
* <Skeleton />
|
|
13495
13548
|
*
|
|
13496
13549
|
* @example
|
|
13497
|
-
*
|
|
13498
|
-
*
|
|
13499
|
-
* <DescriptionList.Value>Priority</DescriptionList.Value>
|
|
13500
|
-
* </DescriptionList.Item>
|
|
13550
|
+
* // Multi-line skeleton
|
|
13551
|
+
* <Skeleton variant="lines" lines={5} />
|
|
13501
13552
|
*/
|
|
13502
|
-
const
|
|
13503
|
-
const
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
...props,
|
|
13553
|
+
const Skeleton = ({ className, variant = 'block', lines = 3, children, ...props }) => {
|
|
13554
|
+
const linesClass = usePrefixedClassNames('skeleton-lines');
|
|
13555
|
+
const blockClass = usePrefixedClassNames('skeleton-block');
|
|
13556
|
+
if (variant === 'lines') {
|
|
13557
|
+
return (jsxRuntime.jsx("div", { className: classNames(linesClass, className), ...props, children: Array.from({ length: lines }).map((_, i) => (jsxRuntime.jsx("div", {}, i))) }));
|
|
13558
|
+
}
|
|
13559
|
+
return (jsxRuntime.jsx("div", { className: classNames(blockClass, className), ...props, children: children }));
|
|
13560
|
+
};
|
|
13561
|
+
|
|
13562
|
+
/** A column's raw value for a row. */
|
|
13563
|
+
function readValue(column, row) {
|
|
13564
|
+
const { accessor } = column;
|
|
13565
|
+
if (typeof accessor === 'function')
|
|
13566
|
+
return accessor(row);
|
|
13567
|
+
if (row === null || typeof row !== 'object')
|
|
13568
|
+
return undefined;
|
|
13569
|
+
return row[(accessor ?? column.id)];
|
|
13570
|
+
}
|
|
13571
|
+
/** A column's text for a row. */
|
|
13572
|
+
function readText(column, row) {
|
|
13573
|
+
const value = readValue(column, row);
|
|
13574
|
+
if (column.format)
|
|
13575
|
+
return column.format(value, row);
|
|
13576
|
+
return value === null || value === undefined ? '' : String(value);
|
|
13577
|
+
}
|
|
13578
|
+
/** Whether a value sorts as empty: nothing, no text, or no number or date. */
|
|
13579
|
+
function isEmptyValue(value) {
|
|
13580
|
+
return (value === null ||
|
|
13581
|
+
value === undefined ||
|
|
13582
|
+
value === '' ||
|
|
13583
|
+
(typeof value === 'number' && Number.isNaN(value)) ||
|
|
13584
|
+
(value instanceof Date && Number.isNaN(value.getTime())));
|
|
13585
|
+
}
|
|
13586
|
+
/** Compares two values that aren't empty, for an ascending sort. */
|
|
13587
|
+
function compareValues(a, b, collator) {
|
|
13588
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
13589
|
+
return a - b;
|
|
13590
|
+
if (a instanceof Date && b instanceof Date)
|
|
13591
|
+
return a.getTime() - b.getTime();
|
|
13592
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
13593
|
+
return Number(a) - Number(b);
|
|
13594
|
+
}
|
|
13595
|
+
return collator.compare(String(a), String(b));
|
|
13596
|
+
}
|
|
13597
|
+
/** Whether a filter value filters anything. */
|
|
13598
|
+
function isActiveFilter(value) {
|
|
13599
|
+
return (value !== undefined &&
|
|
13600
|
+
value !== null &&
|
|
13601
|
+
value !== '' &&
|
|
13602
|
+
!(Array.isArray(value) && value.length === 0));
|
|
13603
|
+
}
|
|
13604
|
+
/**
|
|
13605
|
+
* Whether a search or a filter value is narrowing the rows. Used by
|
|
13606
|
+
* DataTable to tell an empty table from one with no matches.
|
|
13607
|
+
*/
|
|
13608
|
+
function isFiltering(search, filters) {
|
|
13609
|
+
return search.trim() !== '' || Object.values(filters).some(isActiveFilter);
|
|
13610
|
+
}
|
|
13611
|
+
/** A row's id from its `id` property, or nothing when it has none. */
|
|
13612
|
+
function idOf(row) {
|
|
13613
|
+
if (row === null || typeof row !== 'object')
|
|
13614
|
+
return undefined;
|
|
13615
|
+
const id = row.id;
|
|
13616
|
+
return id === undefined || id === null || id === '' ? undefined : String(id);
|
|
13617
|
+
}
|
|
13618
|
+
/**
|
|
13619
|
+
* A piece of state that's the caller's when they pass its value, and the
|
|
13620
|
+
* table's own otherwise. Setting it calls the change handler either way.
|
|
13621
|
+
*
|
|
13622
|
+
* It also returns `latest`, what a change should build on. While the table
|
|
13623
|
+
* holds the state, that's the value last set, even before React renders it,
|
|
13624
|
+
* so two changes in one handler build on each other. While the caller holds
|
|
13625
|
+
* it, it's the value as rendered, since only the caller knows what it made
|
|
13626
|
+
* of a change.
|
|
13627
|
+
*/
|
|
13628
|
+
function useTableState(value, initial, onChange) {
|
|
13629
|
+
const [own, setOwn] = React.useState(initial);
|
|
13630
|
+
const held = value !== undefined;
|
|
13631
|
+
const current = held ? value : own;
|
|
13632
|
+
const rendered = useLatestRef(current);
|
|
13633
|
+
// A value set since the last commit, cleared once a commit shows it.
|
|
13634
|
+
const pendingRef = React.useRef(null);
|
|
13635
|
+
useClientLayoutEffect(() => {
|
|
13636
|
+
pendingRef.current = null;
|
|
13507
13637
|
});
|
|
13508
|
-
const
|
|
13509
|
-
|
|
13510
|
-
}
|
|
13511
|
-
|
|
13638
|
+
const set = React.useCallback((next) => {
|
|
13639
|
+
if (!held) {
|
|
13640
|
+
pendingRef.current = { value: next };
|
|
13641
|
+
setOwn(next);
|
|
13642
|
+
}
|
|
13643
|
+
onChange?.(next);
|
|
13644
|
+
}, [held, onChange]);
|
|
13645
|
+
const latest = React.useCallback(() => (pendingRef.current ? pendingRef.current.value : rendered.current), [rendered]);
|
|
13646
|
+
return [current, set, latest];
|
|
13647
|
+
}
|
|
13648
|
+
const noSort = [];
|
|
13649
|
+
const noIds = [];
|
|
13650
|
+
const noFilters = {};
|
|
13512
13651
|
/**
|
|
13513
|
-
*
|
|
13652
|
+
* `useDataTable` holds a table's sort, selection, page, search and filters,
|
|
13653
|
+
* works out its rows, and hands out props for bestax's own table parts, so
|
|
13654
|
+
* custom markup behaves as `DataTable` does. Pass its result to
|
|
13655
|
+
* `<DataTable table={table} />` to render it with DataTable's own markup,
|
|
13656
|
+
* with a toolbar or an export button elsewhere on the page.
|
|
13514
13657
|
*
|
|
13515
|
-
*
|
|
13516
|
-
*
|
|
13517
|
-
*
|
|
13518
|
-
*
|
|
13519
|
-
|
|
13520
|
-
|
|
13658
|
+
* @example
|
|
13659
|
+
* const table = useDataTable({ columns, data: users, pageSize: 10 });
|
|
13660
|
+
*
|
|
13661
|
+
* <DataTable table={table} caption="Users" hasSearch />
|
|
13662
|
+
*/
|
|
13663
|
+
function useDataTable(options) {
|
|
13664
|
+
const { columns, data, getRowId: rowIdOf, sort: sortProp, defaultSort = noSort, onSortChange, selectionMode = 'none', selected: selectedProp, defaultSelected = noIds, onSelectedChange, pageSize: pageSizeProp, page: pageProp, defaultPage = 1, onPageChange, search: searchProp, defaultSearch = '', onSearchChange, filters: filtersProp, defaultFilters = noFilters, onFiltersChange, manual = false, rowCount: rowCountProp, labels, } = options;
|
|
13665
|
+
const [sort, setSort] = useTableState(sortProp, defaultSort, onSortChange);
|
|
13666
|
+
const [selected, setSelected, latestSelected] = useTableState(selectedProp, defaultSelected, onSelectedChange);
|
|
13667
|
+
const [pageState, setPageState, latestPage] = useTableState(pageProp, defaultPage, onPageChange);
|
|
13668
|
+
const [search, setSearchState] = useTableState(searchProp, defaultSearch, onSearchChange);
|
|
13669
|
+
const [filters, setFiltersState, latestFilters] = useTableState(filtersProp, defaultFilters, onFiltersChange);
|
|
13670
|
+
const prefix = useClassPrefix();
|
|
13671
|
+
const { locale } = useLocale();
|
|
13672
|
+
const matchText = useLocaleMatcher();
|
|
13673
|
+
const formatNumber = useNumberFormat();
|
|
13674
|
+
const collator = React.useMemo(() => new Intl.Collator(locale, { numeric: true }), [locale]);
|
|
13675
|
+
const pageSize = pageSizeProp !== undefined &&
|
|
13676
|
+
Number.isFinite(pageSizeProp) &&
|
|
13677
|
+
pageSizeProp >= 1
|
|
13678
|
+
? Math.floor(pageSizeProp)
|
|
13679
|
+
: undefined;
|
|
13680
|
+
const columnsById = React.useMemo(() => {
|
|
13681
|
+
const byId = new Map();
|
|
13682
|
+
for (const column of columns) {
|
|
13683
|
+
if (byId.has(column.id)) {
|
|
13684
|
+
warnOnce('DataTable:duplicate-column', `[bestax-bulma] DataTable has two columns with the id "${column.id}". ` +
|
|
13685
|
+
'Give each column its own id: the sort, the filters and an export ' +
|
|
13686
|
+
'find a column by it.');
|
|
13687
|
+
}
|
|
13688
|
+
else {
|
|
13689
|
+
byId.set(column.id, column);
|
|
13690
|
+
}
|
|
13691
|
+
}
|
|
13692
|
+
return byId;
|
|
13693
|
+
}, [columns]);
|
|
13694
|
+
// Each row's id, by its index in `data`.
|
|
13695
|
+
const ids = React.useMemo(() => {
|
|
13696
|
+
const out = data.map((row, index) => {
|
|
13697
|
+
const id = rowIdOf ? rowIdOf(row, index) : idOf(row);
|
|
13698
|
+
if (id === undefined || id === '') {
|
|
13699
|
+
warnOnce('DataTable:missing-row-id', '[bestax-bulma] A DataTable row has no id, so it falls back to its ' +
|
|
13700
|
+
"index, which doesn't survive the data changing. Give each row " +
|
|
13701
|
+
'an `id`, or pass `getRowId`.');
|
|
13702
|
+
return String(index);
|
|
13703
|
+
}
|
|
13704
|
+
return id;
|
|
13705
|
+
});
|
|
13706
|
+
if (isDev() && new Set(out).size < out.length) {
|
|
13707
|
+
warnOnce('DataTable:duplicate-row-id', '[bestax-bulma] Two DataTable rows have the same id, so selecting ' +
|
|
13708
|
+
'one selects both. Make the ids unique, or pass a `getRowId` ' +
|
|
13709
|
+
'that does.');
|
|
13710
|
+
}
|
|
13711
|
+
return out;
|
|
13712
|
+
}, [data, rowIdOf]);
|
|
13713
|
+
const indexOf = React.useMemo(() => {
|
|
13714
|
+
const map = new Map();
|
|
13715
|
+
data.forEach((row, index) => {
|
|
13716
|
+
if (!map.has(row))
|
|
13717
|
+
map.set(row, index);
|
|
13718
|
+
});
|
|
13719
|
+
return map;
|
|
13720
|
+
}, [data]);
|
|
13721
|
+
/** A row's id, from its index when the row is one of `data`. */
|
|
13722
|
+
const getRowId = React.useCallback((row) => {
|
|
13723
|
+
const index = indexOf.get(row);
|
|
13724
|
+
if (index !== undefined)
|
|
13725
|
+
return ids[index];
|
|
13726
|
+
const id = rowIdOf ? rowIdOf(row, -1) : idOf(row);
|
|
13727
|
+
return id ?? '';
|
|
13728
|
+
}, [indexOf, ids, rowIdOf]);
|
|
13729
|
+
// Every column's text for every row, worked out once per data and
|
|
13730
|
+
// columns, since the search reads them on every keystroke.
|
|
13731
|
+
const texts = React.useMemo(() => data.map(row => columns.map(column => readText(column, row))), [data, columns]);
|
|
13732
|
+
const sortedIndexes = React.useMemo(() => {
|
|
13733
|
+
const all = data.map((_, index) => index);
|
|
13734
|
+
if (manual)
|
|
13735
|
+
return all;
|
|
13736
|
+
const keys = sort
|
|
13737
|
+
.map(entry => ({
|
|
13738
|
+
column: columnsById.get(entry.id),
|
|
13739
|
+
descending: entry.direction === 'descending',
|
|
13740
|
+
}))
|
|
13741
|
+
.filter((key) => key.column !== undefined);
|
|
13742
|
+
if (keys.length === 0)
|
|
13743
|
+
return all;
|
|
13744
|
+
const values = keys.map(({ column }) => column.sortFn ? null : data.map(row => readValue(column, row)));
|
|
13745
|
+
return all.sort((x, y) => {
|
|
13746
|
+
for (let k = 0; k < keys.length; k++) {
|
|
13747
|
+
const { column, descending } = keys[k];
|
|
13748
|
+
let result;
|
|
13749
|
+
if (column.sortFn) {
|
|
13750
|
+
result = column.sortFn(data[x], data[y]);
|
|
13751
|
+
if (descending)
|
|
13752
|
+
result = -result;
|
|
13753
|
+
}
|
|
13754
|
+
else {
|
|
13755
|
+
const a = values[k][x];
|
|
13756
|
+
const b = values[k][y];
|
|
13757
|
+
const aEmpty = isEmptyValue(a);
|
|
13758
|
+
const bEmpty = isEmptyValue(b);
|
|
13759
|
+
if (aEmpty || bEmpty) {
|
|
13760
|
+
// Empty values go last whichever way the column sorts.
|
|
13761
|
+
result = Number(aEmpty) - Number(bEmpty);
|
|
13762
|
+
}
|
|
13763
|
+
else {
|
|
13764
|
+
result = compareValues(a, b, collator);
|
|
13765
|
+
if (descending)
|
|
13766
|
+
result = -result;
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
if (result)
|
|
13770
|
+
return result;
|
|
13771
|
+
}
|
|
13772
|
+
// Ties keep the order of `data`.
|
|
13773
|
+
return x - y;
|
|
13774
|
+
});
|
|
13775
|
+
}, [data, sort, columnsById, collator, manual]);
|
|
13776
|
+
const filteredIndexes = React.useMemo(() => {
|
|
13777
|
+
if (manual)
|
|
13778
|
+
return sortedIndexes;
|
|
13779
|
+
const query = search.trim();
|
|
13780
|
+
const holds = matchText
|
|
13781
|
+
? (text, needle) => matchText(text, needle)
|
|
13782
|
+
: (text, needle) => text.toLowerCase().includes(needle.toLowerCase());
|
|
13783
|
+
const searched = columns
|
|
13784
|
+
.map((column, index) => ({ column, index }))
|
|
13785
|
+
.filter(({ column }) => column.filterable !== false);
|
|
13786
|
+
const filtered = Object.entries(filters)
|
|
13787
|
+
.filter(([, value]) => isActiveFilter(value))
|
|
13788
|
+
.map(([id, value]) => ({
|
|
13789
|
+
column: columnsById.get(id),
|
|
13790
|
+
index: columns.findIndex(column => column.id === id),
|
|
13791
|
+
value,
|
|
13792
|
+
}))
|
|
13793
|
+
.filter(({ column }) => column !== undefined && column.filterable !== false);
|
|
13794
|
+
if (query === '' && filtered.length === 0)
|
|
13795
|
+
return sortedIndexes;
|
|
13796
|
+
return sortedIndexes.filter(rowIndex => {
|
|
13797
|
+
const row = data[rowIndex];
|
|
13798
|
+
const rowTexts = texts[rowIndex];
|
|
13799
|
+
for (const { column, index, value } of filtered) {
|
|
13800
|
+
// An array is any of its entries, as a multiple-choice filter gives.
|
|
13801
|
+
const passes = column.filterFn
|
|
13802
|
+
? column.filterFn(row, value)
|
|
13803
|
+
: Array.isArray(value)
|
|
13804
|
+
? value.some(entry => holds(rowTexts[index], String(entry)))
|
|
13805
|
+
: holds(rowTexts[index], String(value));
|
|
13806
|
+
if (!passes)
|
|
13807
|
+
return false;
|
|
13808
|
+
}
|
|
13809
|
+
return (query === '' ||
|
|
13810
|
+
searched.some(({ index }) => holds(rowTexts[index], query)));
|
|
13811
|
+
});
|
|
13812
|
+
}, [
|
|
13813
|
+
manual,
|
|
13814
|
+
sortedIndexes,
|
|
13815
|
+
search,
|
|
13816
|
+
filters,
|
|
13817
|
+
matchText,
|
|
13818
|
+
columns,
|
|
13819
|
+
columnsById,
|
|
13820
|
+
data,
|
|
13821
|
+
texts,
|
|
13822
|
+
]);
|
|
13823
|
+
const knowsCount = rowCountProp !== undefined && Number.isFinite(rowCountProp);
|
|
13824
|
+
if (manual && pageSize !== undefined && !knowsCount) {
|
|
13825
|
+
warnOnce('DataTable:manual-row-count', '[bestax-bulma] A DataTable with `manual` and a `pageSize` has no ' +
|
|
13826
|
+
'`rowCount`, so it counts only the rows in `data`, makes one page ' +
|
|
13827
|
+
'of them and shows no pager. Pass `rowCount`, how many rows match ' +
|
|
13828
|
+
'on the server.');
|
|
13829
|
+
}
|
|
13830
|
+
const rowCount = manual
|
|
13831
|
+
? Math.max(Math.floor(knowsCount ? rowCountProp : data.length), 0)
|
|
13832
|
+
: filteredIndexes.length;
|
|
13833
|
+
const pageCount = pageSize ? Math.max(Math.ceil(rowCount / pageSize), 1) : 1;
|
|
13834
|
+
const page = Math.min(Math.max(Number.isFinite(pageState) ? Math.floor(pageState) : 1, 1), pageCount);
|
|
13835
|
+
const pageIndexes = React.useMemo(() => {
|
|
13836
|
+
if (manual || !pageSize)
|
|
13837
|
+
return filteredIndexes;
|
|
13838
|
+
return filteredIndexes.slice((page - 1) * pageSize, page * pageSize);
|
|
13839
|
+
}, [manual, pageSize, filteredIndexes, page]);
|
|
13840
|
+
const selectedSet = React.useMemo(() => new Set(selected), [selected]);
|
|
13841
|
+
const selectable = selectionMode === 'multiple';
|
|
13842
|
+
const rowsOf = React.useCallback((indexes) => indexes.map(index => data[index]), [data]);
|
|
13843
|
+
const getRows = React.useCallback((scope = 'filtered') => {
|
|
13844
|
+
switch (scope) {
|
|
13845
|
+
case 'all':
|
|
13846
|
+
return rowsOf(sortedIndexes);
|
|
13847
|
+
case 'page':
|
|
13848
|
+
return rowsOf(pageIndexes);
|
|
13849
|
+
case 'selected':
|
|
13850
|
+
return rowsOf(filteredIndexes.filter(index => selectedSet.has(ids[index])));
|
|
13851
|
+
default:
|
|
13852
|
+
return rowsOf(filteredIndexes);
|
|
13853
|
+
}
|
|
13854
|
+
}, [rowsOf, sortedIndexes, pageIndexes, filteredIndexes, selectedSet, ids]);
|
|
13855
|
+
const getValue = React.useCallback((row, columnId) => {
|
|
13856
|
+
const column = columnsById.get(columnId);
|
|
13857
|
+
return column ? readValue(column, row) : undefined;
|
|
13858
|
+
}, [columnsById]);
|
|
13859
|
+
const getText = React.useCallback((row, columnId) => {
|
|
13860
|
+
const column = columnsById.get(columnId);
|
|
13861
|
+
if (!column)
|
|
13862
|
+
return '';
|
|
13863
|
+
const index = indexOf.get(row);
|
|
13864
|
+
return index !== undefined
|
|
13865
|
+
? texts[index][columns.indexOf(column)]
|
|
13866
|
+
: readText(column, row);
|
|
13867
|
+
}, [columnsById, indexOf, columns, texts]);
|
|
13868
|
+
const labelColumn = columns.find(column => column.isRowHeader) ?? columns[0];
|
|
13869
|
+
const getRowLabel = React.useCallback((row) => (labelColumn && getText(row, labelColumn.id)) || getRowId(row), [labelColumn, getText, getRowId]);
|
|
13870
|
+
const isSelected = React.useCallback((row) => selectable && selectedSet.has(getRowId(row)), [selectable, selectedSet, getRowId]);
|
|
13871
|
+
const setPage = React.useCallback((next) => {
|
|
13872
|
+
if (next !== page)
|
|
13873
|
+
setPageState(next);
|
|
13874
|
+
}, [page, setPageState]);
|
|
13875
|
+
// A new search or filter starts from the first page. It's measured against
|
|
13876
|
+
// the page as stored, not as shown: a page past the end shows the last,
|
|
13877
|
+
// and left stored it would come back once the rows grew again.
|
|
13878
|
+
const resetPage = React.useCallback(() => {
|
|
13879
|
+
if (latestPage() !== 1)
|
|
13880
|
+
setPageState(1);
|
|
13881
|
+
}, [latestPage, setPageState]);
|
|
13882
|
+
const setSearch = React.useCallback((next) => {
|
|
13883
|
+
setSearchState(next);
|
|
13884
|
+
resetPage();
|
|
13885
|
+
}, [setSearchState, resetPage]);
|
|
13886
|
+
const setFilters = React.useCallback((next) => {
|
|
13887
|
+
setFiltersState(next);
|
|
13888
|
+
resetPage();
|
|
13889
|
+
}, [setFiltersState, resetPage]);
|
|
13890
|
+
const setFilter = React.useCallback((columnId, value) => {
|
|
13891
|
+
const next = { ...latestFilters() };
|
|
13892
|
+
if (isActiveFilter(value))
|
|
13893
|
+
next[columnId] = value;
|
|
13894
|
+
else
|
|
13895
|
+
delete next[columnId];
|
|
13896
|
+
setFilters(next);
|
|
13897
|
+
}, [latestFilters, setFilters]);
|
|
13898
|
+
const toggleSort = React.useCallback((columnId) => {
|
|
13899
|
+
const column = columnsById.get(columnId);
|
|
13900
|
+
if (!column)
|
|
13901
|
+
return;
|
|
13902
|
+
const first = column.sortDescFirst
|
|
13903
|
+
? 'descending'
|
|
13904
|
+
: 'ascending';
|
|
13905
|
+
const current = sort[0]?.id === columnId ? sort[0].direction : undefined;
|
|
13906
|
+
if (current === undefined) {
|
|
13907
|
+
setSort([{ id: columnId, direction: first }]);
|
|
13908
|
+
}
|
|
13909
|
+
else if (current === first) {
|
|
13910
|
+
setSort([
|
|
13911
|
+
{
|
|
13912
|
+
id: columnId,
|
|
13913
|
+
direction: first === 'ascending' ? 'descending' : 'ascending',
|
|
13914
|
+
},
|
|
13915
|
+
]);
|
|
13916
|
+
}
|
|
13917
|
+
else {
|
|
13918
|
+
setSort([]);
|
|
13919
|
+
}
|
|
13920
|
+
}, [columnsById, sort, setSort]);
|
|
13921
|
+
const toggleRowSelected = React.useCallback((row, value) => {
|
|
13922
|
+
const id = getRowId(row);
|
|
13923
|
+
const current = latestSelected();
|
|
13924
|
+
const has = current.includes(id);
|
|
13925
|
+
const on = value ?? !has;
|
|
13926
|
+
if (on === has)
|
|
13927
|
+
return;
|
|
13928
|
+
setSelected(on ? [...current, id] : current.filter(s => s !== id));
|
|
13929
|
+
}, [getRowId, latestSelected, setSelected]);
|
|
13930
|
+
const pageIds = React.useMemo(() => pageIndexes.map(index => ids[index]), [pageIndexes, ids]);
|
|
13931
|
+
const pageSelectedCount = pageIds.filter(id => selectedSet.has(id)).length;
|
|
13932
|
+
const allPageSelected = pageIds.length > 0 && pageSelectedCount === pageIds.length;
|
|
13933
|
+
const togglePageSelected = React.useCallback((value) => {
|
|
13934
|
+
const current = latestSelected();
|
|
13935
|
+
const has = new Set(current);
|
|
13936
|
+
const on = value ?? !(pageIds.length > 0 && pageIds.every(id => has.has(id)));
|
|
13937
|
+
if (on) {
|
|
13938
|
+
const adding = pageIds.filter(id => !has.has(id));
|
|
13939
|
+
if (adding.length > 0)
|
|
13940
|
+
setSelected([...current, ...adding]);
|
|
13941
|
+
}
|
|
13942
|
+
else {
|
|
13943
|
+
const onPage = new Set(pageIds);
|
|
13944
|
+
const kept = current.filter(id => !onPage.has(id));
|
|
13945
|
+
if (kept.length !== current.length)
|
|
13946
|
+
setSelected(kept);
|
|
13947
|
+
}
|
|
13948
|
+
}, [latestSelected, pageIds, setSelected]);
|
|
13949
|
+
const count = React.useCallback((value) => (formatNumber ? formatNumber(value) : String(value)), [formatNumber]);
|
|
13950
|
+
const resolvedLabels = {
|
|
13951
|
+
selectAll: labels?.selectAll ||
|
|
13952
|
+
(pageCount > 1 ? 'Select all rows on this page' : 'Select all rows'),
|
|
13953
|
+
selectRow: labels?.selectRow ?? (label => `Select ${label}`),
|
|
13954
|
+
sortHint: labels?.sortHint || 'column headers with buttons are sortable',
|
|
13955
|
+
search: labels?.search || 'Search',
|
|
13956
|
+
results: labels?.results ??
|
|
13957
|
+
(total => total === 0
|
|
13958
|
+
? 'No rows match'
|
|
13959
|
+
: `${count(total)} ${total === 1 ? 'row matches' : 'rows match'}`),
|
|
13960
|
+
pageRange: labels?.pageRange ??
|
|
13961
|
+
((first, last, total) => `Showing ${count(first)} to ${count(last)} of ${count(total)}`),
|
|
13962
|
+
loading: labels?.loading || 'Loading',
|
|
13963
|
+
pagination: labels?.pagination || undefined,
|
|
13964
|
+
empty: labels?.empty || 'No rows to show.',
|
|
13965
|
+
noResults: labels?.noResults || 'No rows match.',
|
|
13966
|
+
};
|
|
13967
|
+
const sortButtonClass = prefixedClassNames(prefix, 'data-table-sort');
|
|
13968
|
+
return {
|
|
13969
|
+
columns,
|
|
13970
|
+
state: { sort, selected, page, pageSize, search, filters },
|
|
13971
|
+
selectionMode,
|
|
13972
|
+
manual,
|
|
13973
|
+
rowCount,
|
|
13974
|
+
pageCount,
|
|
13975
|
+
labels: resolvedLabels,
|
|
13976
|
+
getRows,
|
|
13977
|
+
getRowId,
|
|
13978
|
+
getValue,
|
|
13979
|
+
getText,
|
|
13980
|
+
getRowLabel,
|
|
13981
|
+
isSelected,
|
|
13982
|
+
setSort,
|
|
13983
|
+
toggleSort,
|
|
13984
|
+
setSelected,
|
|
13985
|
+
toggleRowSelected,
|
|
13986
|
+
togglePageSelected,
|
|
13987
|
+
setPage,
|
|
13988
|
+
setSearch,
|
|
13989
|
+
setFilter,
|
|
13990
|
+
setFilters,
|
|
13991
|
+
getHeaderCellProps: column => ({
|
|
13992
|
+
scope: 'col',
|
|
13993
|
+
'aria-sort': sort[0]?.id === column.id ? sort[0].direction : undefined,
|
|
13994
|
+
isAligned: column.align,
|
|
13995
|
+
width: column.width,
|
|
13996
|
+
}),
|
|
13997
|
+
getSortButtonProps: column => ({
|
|
13998
|
+
type: 'button',
|
|
13999
|
+
className: sortButtonClass,
|
|
14000
|
+
onClick: () => toggleSort(column.id),
|
|
14001
|
+
}),
|
|
14002
|
+
getRowProps: row => ({ isSelected: isSelected(row) }),
|
|
14003
|
+
getCellProps: (_row, column) => ({
|
|
14004
|
+
scope: column.isRowHeader ? 'row' : undefined,
|
|
14005
|
+
isAligned: column.align,
|
|
14006
|
+
}),
|
|
14007
|
+
getSelectAllProps: () => ({
|
|
14008
|
+
'aria-label': resolvedLabels.selectAll,
|
|
14009
|
+
checked: allPageSelected,
|
|
14010
|
+
indeterminate: pageSelectedCount > 0 && !allPageSelected,
|
|
14011
|
+
disabled: pageIds.length === 0,
|
|
14012
|
+
onChange: () => togglePageSelected(),
|
|
14013
|
+
}),
|
|
14014
|
+
getSelectRowProps: row => ({
|
|
14015
|
+
'aria-label': resolvedLabels.selectRow(getRowLabel(row)),
|
|
14016
|
+
checked: selectedSet.has(getRowId(row)),
|
|
14017
|
+
onChange: () => toggleRowSelected(row),
|
|
14018
|
+
}),
|
|
14019
|
+
getPaginationProps: () => ({
|
|
14020
|
+
total: pageCount,
|
|
14021
|
+
current: page,
|
|
14022
|
+
onPageChange: setPage,
|
|
14023
|
+
}),
|
|
14024
|
+
getSearchProps: () => ({
|
|
14025
|
+
type: 'search',
|
|
14026
|
+
value: search,
|
|
14027
|
+
onChange: event => setSearch(event.target.value),
|
|
14028
|
+
}),
|
|
14029
|
+
};
|
|
14030
|
+
}
|
|
14031
|
+
|
|
14032
|
+
/** How long the status region waits after a change before it speaks, in ms. */
|
|
14033
|
+
const statusDelay = 500;
|
|
14034
|
+
const noColumns = [];
|
|
14035
|
+
const noRows = [];
|
|
14036
|
+
/** A text signature of the search and the filters, to tell when they change. */
|
|
14037
|
+
function filterSignature(search, filters) {
|
|
14038
|
+
try {
|
|
14039
|
+
return JSON.stringify([search, Object.entries(filters).sort()]);
|
|
14040
|
+
}
|
|
14041
|
+
catch {
|
|
14042
|
+
return search;
|
|
14043
|
+
}
|
|
14044
|
+
}
|
|
14045
|
+
/** Whether a node renders something. */
|
|
14046
|
+
const isShown = (node) => node !== undefined && node !== null && node !== false && node !== '';
|
|
14047
|
+
/**
|
|
14048
|
+
* The `DataTable` component is a table of records that people can sort,
|
|
14049
|
+
* search, page through and select rows from, rendered with bestax's own
|
|
14050
|
+
* `Table` parts, `Checkbox` and `Pagination`.
|
|
13521
14051
|
*
|
|
13522
|
-
*
|
|
13523
|
-
*
|
|
13524
|
-
*
|
|
13525
|
-
*
|
|
14052
|
+
* It's a native table, as in the WAI-ARIA Authoring Practices' sortable
|
|
14053
|
+
* table: a sortable header holds a button, the sorted one carries
|
|
14054
|
+
* `aria-sort`, and each row's checkbox is named for the row. It runs on
|
|
14055
|
+
* `useDataTable`, which custom markup can use too.
|
|
13526
14056
|
*
|
|
13527
14057
|
* @example
|
|
13528
|
-
* <
|
|
13529
|
-
*
|
|
13530
|
-
*
|
|
13531
|
-
*
|
|
14058
|
+
* <DataTable
|
|
14059
|
+
* caption="Users"
|
|
14060
|
+
* columns={[
|
|
14061
|
+
* { id: 'name', header: 'Name', sortable: true, isRowHeader: true },
|
|
14062
|
+
* { id: 'role', header: 'Role', sortable: true },
|
|
14063
|
+
* ]}
|
|
14064
|
+
* data={users}
|
|
14065
|
+
* selectionMode="multiple"
|
|
14066
|
+
* pageSize={10}
|
|
14067
|
+
* hasSearch
|
|
14068
|
+
* />
|
|
13532
14069
|
*/
|
|
13533
|
-
const
|
|
13534
|
-
const
|
|
13535
|
-
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
13539
|
-
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
14070
|
+
const DataTable = React.forwardRef(function DataTableRender({ columns, data, table: given, getRowId, sort, defaultSort, onSortChange, selectionMode, selected, defaultSelected, onSelectedChange, pageSize, page, defaultPage, onPageChange, search, defaultSearch, onSearchChange, filters, defaultFilters, onFiltersChange, manual, rowCount, labels, caption, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isResponsive, hasSearch, toolbar, loading, empty, noResults, rowProps, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...props }, ref) {
|
|
14071
|
+
const own = useDataTable({
|
|
14072
|
+
columns: columns ?? noColumns,
|
|
14073
|
+
data: data ?? noRows,
|
|
14074
|
+
getRowId,
|
|
14075
|
+
sort,
|
|
14076
|
+
defaultSort,
|
|
14077
|
+
onSortChange,
|
|
14078
|
+
selectionMode,
|
|
14079
|
+
selected,
|
|
14080
|
+
defaultSelected,
|
|
14081
|
+
onSelectedChange,
|
|
14082
|
+
pageSize,
|
|
14083
|
+
page,
|
|
14084
|
+
defaultPage,
|
|
14085
|
+
onPageChange,
|
|
14086
|
+
search,
|
|
14087
|
+
defaultSearch,
|
|
14088
|
+
onSearchChange,
|
|
14089
|
+
filters,
|
|
14090
|
+
defaultFilters,
|
|
14091
|
+
onFiltersChange,
|
|
14092
|
+
manual,
|
|
14093
|
+
rowCount,
|
|
14094
|
+
labels,
|
|
14095
|
+
});
|
|
14096
|
+
const table = given ?? own;
|
|
14097
|
+
const { state } = table;
|
|
14098
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
14099
|
+
const rootClass = classNames(usePrefixedClassNames('data-table'), bulmaHelperClasses, className);
|
|
14100
|
+
const toolbarClass = usePrefixedClassNames('data-table-toolbar');
|
|
14101
|
+
const actionsClass = usePrefixedClassNames('data-table-actions');
|
|
14102
|
+
const emptyClass = usePrefixedClassNames('data-table-empty');
|
|
14103
|
+
const nameId = React.useId();
|
|
14104
|
+
const hasCaption = isShown(caption);
|
|
14105
|
+
const named = hasCaption || Boolean(ariaLabel || ariaLabelledby);
|
|
14106
|
+
React.useEffect(() => {
|
|
14107
|
+
if (!named) {
|
|
14108
|
+
warnOnce('DataTable:unnamed', '[bestax-bulma] <DataTable> has no accessible name. Pass a caption, ' +
|
|
14109
|
+
'aria-label or aria-labelledby, so a screen reader can say which ' +
|
|
14110
|
+
'table it is in.');
|
|
14111
|
+
}
|
|
14112
|
+
}, [named]);
|
|
14113
|
+
const mixed = Boolean(given) && Boolean(columns || data);
|
|
14114
|
+
const bare = !given && (!columns || !data);
|
|
14115
|
+
React.useEffect(() => {
|
|
14116
|
+
if (mixed) {
|
|
14117
|
+
warnOnce('DataTable:table-and-columns', '[bestax-bulma] <DataTable> was given a `table` and `columns` or ' +
|
|
14118
|
+
'`data` too. It renders the `table` and leaves the rest unread, so ' +
|
|
14119
|
+
'pass those to useDataTable instead.');
|
|
14120
|
+
}
|
|
14121
|
+
if (bare) {
|
|
14122
|
+
warnOnce('DataTable:no-data', '[bestax-bulma] <DataTable> needs `columns` and `data`, or a `table` ' +
|
|
14123
|
+
'from useDataTable.');
|
|
14124
|
+
}
|
|
14125
|
+
}, [mixed, bare]);
|
|
14126
|
+
// The status region speaks only after the search, a filter or the page
|
|
14127
|
+
// changes, so nothing is read out as the page loads. `kind` is what last
|
|
14128
|
+
// changed, kept beside the values it changed from.
|
|
14129
|
+
const signature = filterSignature(state.search, state.filters);
|
|
14130
|
+
const [last, setLast] = React.useState({
|
|
14131
|
+
signature,
|
|
14132
|
+
page: state.page,
|
|
14133
|
+
kind: null,
|
|
14134
|
+
});
|
|
14135
|
+
if (last.signature !== signature) {
|
|
14136
|
+
setLast({ signature, page: state.page, kind: 'results' });
|
|
14137
|
+
}
|
|
14138
|
+
else if (last.page !== state.page) {
|
|
14139
|
+
setLast({ signature, page: state.page, kind: 'page' });
|
|
14140
|
+
}
|
|
14141
|
+
const { labels: words, rowCount: total, pageCount } = table;
|
|
14142
|
+
const size = state.pageSize;
|
|
14143
|
+
let message = '';
|
|
14144
|
+
if (last.kind && loading) {
|
|
14145
|
+
message = words.loading;
|
|
14146
|
+
}
|
|
14147
|
+
else if (last.kind === 'results' || (last.kind === 'page' && !total)) {
|
|
14148
|
+
message = words.results(total);
|
|
14149
|
+
}
|
|
14150
|
+
else if (last.kind === 'page') {
|
|
14151
|
+
// Without a page size, as when it's just been taken away, there's one
|
|
14152
|
+
// page, holding every row.
|
|
14153
|
+
const per = size ?? total;
|
|
14154
|
+
message = words.pageRange((state.page - 1) * per + 1, Math.min(state.page * per, total), total);
|
|
14155
|
+
}
|
|
14156
|
+
// Written a moment after it changes, so typing in the search is read out
|
|
14157
|
+
// once it pauses rather than at every keystroke.
|
|
14158
|
+
const [said, setSaid] = React.useState('');
|
|
14159
|
+
React.useEffect(() => {
|
|
14160
|
+
const timer = setTimeout(() => setSaid(message), statusDelay);
|
|
14161
|
+
return () => clearTimeout(timer);
|
|
14162
|
+
}, [message]);
|
|
14163
|
+
const selectable = table.selectionMode === 'multiple';
|
|
14164
|
+
const sortable = table.columns.some(column => column.sortable);
|
|
14165
|
+
const columnCount = table.columns.length + (selectable ? 1 : 0);
|
|
14166
|
+
const pageRows = table.getRows('page');
|
|
14167
|
+
const selectAll = table.getSelectAllProps();
|
|
14168
|
+
const captionNode = hasCaption ? (jsxRuntime.jsxs("caption", { children: [jsxRuntime.jsx("span", { id: nameId, children: caption }), sortable && jsxRuntime.jsx("span", { style: visuallyHidden, children: `, ${words.sortHint}.` })] })) : sortable ? (jsxRuntime.jsx("caption", { children: jsxRuntime.jsx("span", { style: visuallyHidden, children: words.sortHint }) })) : null;
|
|
14169
|
+
const header = (jsxRuntime.jsx(Thead, { children: jsxRuntime.jsxs(Tr, { children: [selectable && (jsxRuntime.jsx(Th, { scope: "col", isNarrow: true, children: jsxRuntime.jsx(Checkbox, { ...selectAll,
|
|
14170
|
+
// While loading, the rows it would select aren't on screen.
|
|
14171
|
+
disabled: selectAll.disabled || Boolean(loading) }) })), table.columns.map(column => (jsxRuntime.jsx(Th, { ...table.getHeaderCellProps(column), children: column.sortable ? (jsxRuntime.jsx("button", { ...table.getSortButtonProps(column), children: column.header })) : (column.header) }, column.id)))] }) }));
|
|
14172
|
+
let body;
|
|
14173
|
+
if (loading) {
|
|
14174
|
+
body = Array.from({ length: size ?? 5 }, (_, index) => (jsxRuntime.jsx(Tr, { "aria-hidden": "true", children: Array.from({ length: columnCount }, (__, cell) => (jsxRuntime.jsx(Td, { children: jsxRuntime.jsx(Skeleton, { variant: "lines", lines: 1 }) }, cell))) }, index)));
|
|
14175
|
+
}
|
|
14176
|
+
else if (pageRows.length === 0) {
|
|
14177
|
+
// With `manual`, no rows under a search or a filter is no match: only
|
|
14178
|
+
// the server knows whether there's anything else.
|
|
14179
|
+
const nothing = table.getRows('all').length === 0 &&
|
|
14180
|
+
!(table.manual && isFiltering(state.search, state.filters));
|
|
14181
|
+
const content = nothing
|
|
14182
|
+
? isShown(empty)
|
|
14183
|
+
? empty
|
|
14184
|
+
: empty === undefined && (jsxRuntime.jsx(EmptyState, { size: "small", description: words.empty }))
|
|
14185
|
+
: isShown(noResults)
|
|
14186
|
+
? noResults
|
|
14187
|
+
: noResults === undefined && (jsxRuntime.jsx(EmptyState, { size: "small", description: words.noResults }));
|
|
14188
|
+
body = (jsxRuntime.jsx(Tr, { children: jsxRuntime.jsx(Td, { colSpan: columnCount, className: emptyClass, children: content }) }));
|
|
14189
|
+
}
|
|
14190
|
+
else {
|
|
14191
|
+
body = pageRows.map(row => {
|
|
14192
|
+
const rowId = table.getRowId(row);
|
|
14193
|
+
return (jsxRuntime.jsxs(Tr, { ...rowProps?.(row), ...table.getRowProps(row), children: [selectable && (jsxRuntime.jsx(Td, { isNarrow: true, children: jsxRuntime.jsx(Checkbox, { ...table.getSelectRowProps(row) }) })), table.columns.map(column => {
|
|
14194
|
+
const text = table.getText(row, column.id);
|
|
14195
|
+
const content = column.cell
|
|
14196
|
+
? column.cell(row, {
|
|
14197
|
+
value: table.getValue(row, column.id),
|
|
14198
|
+
text,
|
|
14199
|
+
rowId,
|
|
14200
|
+
})
|
|
14201
|
+
: text;
|
|
14202
|
+
const cellProps = table.getCellProps(row, column);
|
|
14203
|
+
return column.isRowHeader ? (jsxRuntime.jsx(Th, { ...cellProps, children: content }, column.id)) : (jsxRuntime.jsx(Td, { ...cellProps, children: content }, column.id));
|
|
14204
|
+
})] }, rowId));
|
|
14205
|
+
});
|
|
14206
|
+
}
|
|
14207
|
+
const tableElement = (jsxRuntime.jsxs(Table, { isBordered: isBordered, isStriped: isStriped, isNarrow: isNarrow, isHoverable: isHoverable, isFullwidth: isFullwidth, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-busy": loading || undefined, children: [captionNode, header, jsxRuntime.jsx(Tbody, { children: body })] }));
|
|
14208
|
+
// The name the scroll area and the pager take from the table.
|
|
14209
|
+
const textName = ariaLabel || (typeof caption === 'string' ? caption : undefined);
|
|
14210
|
+
const scrollName = ariaLabel
|
|
14211
|
+
? { 'aria-label': ariaLabel }
|
|
14212
|
+
: {
|
|
14213
|
+
'aria-labelledby': ariaLabelledby || (hasCaption ? nameId : undefined),
|
|
14214
|
+
};
|
|
14215
|
+
// Pagination names itself "pagination" unless given another name.
|
|
14216
|
+
const pagerLabel = words.pagination || (textName ? `${textName} pages` : undefined);
|
|
14217
|
+
const pagerName = pagerLabel ? { 'aria-label': pagerLabel } : {};
|
|
14218
|
+
const toolbarContent = toolbar?.(table);
|
|
14219
|
+
const hasToolbar = hasSearch || isShown(toolbarContent);
|
|
14220
|
+
return (jsxRuntime.jsxs("div", { ref: ref, className: rootClass, ...rest, children: [hasToolbar && (jsxRuntime.jsxs("div", { className: toolbarClass, children: [hasSearch && (jsxRuntime.jsx(Input, { label: words.search, ...table.getSearchProps() })), isShown(toolbarContent) && (jsxRuntime.jsx("div", { className: actionsClass, children: toolbarContent }))] })), isResponsive ? (jsxRuntime.jsx(ScrollArea, { orientation: "horizontal", ...scrollName, children: tableElement })) : (tableElement), jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", style: visuallyHidden, children: said }), pageCount > 1 && (jsxRuntime.jsx(Pagination, { ...table.getPaginationProps(), ...pagerName }))] }));
|
|
13552
14221
|
});
|
|
13553
|
-
|
|
13554
|
-
Item: DescriptionListItem,
|
|
13555
|
-
Label: DescriptionListLabel,
|
|
13556
|
-
Value: DescriptionListValue,
|
|
13557
|
-
}, 'DescriptionList');
|
|
14222
|
+
DataTable.displayName = 'DataTable';
|
|
13558
14223
|
|
|
13559
14224
|
/**
|
|
13560
|
-
* The `
|
|
14225
|
+
* The `Block` component renders a simple container with Bulma's `.block` class, adding vertical margin between sections of content.
|
|
13561
14226
|
*
|
|
13562
14227
|
* @function
|
|
13563
|
-
* @param {
|
|
13564
|
-
* @returns {JSX.Element} The rendered
|
|
14228
|
+
* @param {BlockProps} props - Props for the Block component.
|
|
14229
|
+
* @returns {JSX.Element} The rendered block element.
|
|
14230
|
+
* @see {@link https://bulma.io/documentation/elements/block/ | Bulma Block documentation}
|
|
13565
14231
|
*/
|
|
13566
|
-
const
|
|
14232
|
+
const Block = ({ className, textColor, color, bgColor, children, ...props }) => {
|
|
13567
14233
|
/**
|
|
13568
14234
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13569
14235
|
*/
|
|
13570
14236
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14237
|
+
color: textColor ?? color,
|
|
13571
14238
|
backgroundColor: bgColor,
|
|
13572
14239
|
...props,
|
|
13573
14240
|
});
|
|
13574
|
-
const bulmaClasses = usePrefixedClassNames();
|
|
13575
|
-
const
|
|
13576
|
-
return jsxRuntime.jsx("
|
|
14241
|
+
const bulmaClasses = usePrefixedClassNames('block');
|
|
14242
|
+
const blockClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14243
|
+
return (jsxRuntime.jsx("div", { className: blockClasses, ...rest, children: children }));
|
|
13577
14244
|
};
|
|
13578
14245
|
|
|
13579
14246
|
/**
|
|
13580
|
-
* The `
|
|
14247
|
+
* The `Box` component renders a bordered, padded container with an optional shadow using Bulma's `.box` class.
|
|
13581
14248
|
*
|
|
13582
14249
|
* @function
|
|
13583
|
-
* @param {
|
|
13584
|
-
* @returns {JSX.Element} The rendered
|
|
14250
|
+
* @param {BoxProps} props - Props for the Box component.
|
|
14251
|
+
* @returns {JSX.Element} The rendered box element.
|
|
14252
|
+
* @see {@link https://bulma.io/documentation/elements/box/ | Bulma Box documentation}
|
|
13585
14253
|
*/
|
|
13586
|
-
const
|
|
13587
|
-
|
|
13588
|
-
|
|
14254
|
+
const Box = ({ className, textColor, color, bgColor, hasShadow = true, children, style, ...props }) => {
|
|
14255
|
+
/**
|
|
14256
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
14257
|
+
*/
|
|
14258
|
+
const { bulmaHelperClasses, bulmaHelperStyles, rest } = useBulmaClasses({
|
|
14259
|
+
color: textColor ?? color,
|
|
13589
14260
|
backgroundColor: bgColor,
|
|
13590
14261
|
...props,
|
|
13591
14262
|
});
|
|
13592
|
-
const bulmaClasses = usePrefixedClassNames(
|
|
13593
|
-
|
|
13594
|
-
|
|
14263
|
+
const bulmaClasses = usePrefixedClassNames('box', {
|
|
14264
|
+
'is-shadowless': !hasShadow,
|
|
14265
|
+
});
|
|
14266
|
+
const boxClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14267
|
+
return (jsxRuntime.jsx("div", { className: boxClasses, style: mergeBulmaStyles(bulmaHelperStyles, style), ...rest, children: children }));
|
|
13595
14268
|
};
|
|
13596
14269
|
|
|
13597
14270
|
/**
|
|
13598
|
-
*
|
|
13599
|
-
*
|
|
13600
|
-
* A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
|
|
13601
|
-
* Use it inside Figure components to provide captions for images or other media.
|
|
14271
|
+
* The `Code` component renders a styled `<code>` element with Bulma helper class integration.
|
|
13602
14272
|
*
|
|
13603
14273
|
* @function
|
|
13604
|
-
* @param {
|
|
13605
|
-
* @returns {JSX.Element} The rendered
|
|
14274
|
+
* @param {CodeProps} props - Props for the Code component.
|
|
14275
|
+
* @returns {JSX.Element} The rendered code element.
|
|
13606
14276
|
*/
|
|
13607
|
-
const
|
|
14277
|
+
const Code = ({ className, textColor, bgColor, children, ...props }) => {
|
|
13608
14278
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13609
14279
|
color: textColor,
|
|
13610
14280
|
backgroundColor: bgColor,
|
|
13611
14281
|
...props,
|
|
13612
14282
|
});
|
|
13613
14283
|
const bulmaClasses = usePrefixedClassNames();
|
|
13614
|
-
const
|
|
13615
|
-
return (jsxRuntime.jsx("
|
|
14284
|
+
const codeClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14285
|
+
return (jsxRuntime.jsx("code", { className: codeClasses || undefined, ...rest, children: children }));
|
|
13616
14286
|
};
|
|
14287
|
+
|
|
14288
|
+
const validColorSwatchSizes = ['small', 'medium', 'large'];
|
|
13617
14289
|
/**
|
|
13618
|
-
* The `
|
|
14290
|
+
* The `ColorSwatch` component shows a color as a small chip, named by its label or by the color itself.
|
|
14291
|
+
*
|
|
14292
|
+
* It's an image to assistive tech, so the color is always there in words.
|
|
14293
|
+
* In forced colors it keeps its color, edged in the system's text color.
|
|
13619
14294
|
*
|
|
13620
14295
|
* @function
|
|
13621
|
-
* @param {
|
|
13622
|
-
* @
|
|
14296
|
+
* @param {ColorSwatchProps} props - Props for the ColorSwatch component.
|
|
14297
|
+
* @param {React.Ref<HTMLSpanElement>} ref - Forwarded ref to the `<span>`.
|
|
14298
|
+
* @returns {JSX.Element} The rendered swatch.
|
|
14299
|
+
*
|
|
14300
|
+
* @example
|
|
14301
|
+
* <ColorSwatch color="#1e6b99" label="Brand blue" />
|
|
13623
14302
|
*/
|
|
13624
|
-
const
|
|
14303
|
+
const ColorSwatch = React.forwardRef(({ className, textColor, color, label, size, isRounded, children, style, ...props }, ref) => {
|
|
13625
14304
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13626
14305
|
color: textColor,
|
|
13627
|
-
backgroundColor: bgColor,
|
|
13628
14306
|
...props,
|
|
13629
14307
|
});
|
|
13630
|
-
const
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
}
|
|
14308
|
+
const unnamed = /^\s*var\(/i.test(color) &&
|
|
14309
|
+
!label &&
|
|
14310
|
+
!rest['aria-label'] &&
|
|
14311
|
+
!rest['aria-labelledby'] &&
|
|
14312
|
+
!rest['aria-hidden'];
|
|
14313
|
+
React.useEffect(() => {
|
|
14314
|
+
if (unnamed) {
|
|
14315
|
+
warnOnce('ColorSwatch:unnamed-var', '[bestax-bulma] <ColorSwatch> shows a CSS variable with no label, ' +
|
|
14316
|
+
'so a screen reader reads the var() as its name. Pass label, ' +
|
|
14317
|
+
'such as "Primary".');
|
|
14318
|
+
}
|
|
14319
|
+
}, [unnamed]);
|
|
14320
|
+
const swatchClass = usePrefixedClassNames('color-swatch', {
|
|
14321
|
+
[`is-${size}`]: !!size && validColorSwatchSizes.includes(size),
|
|
14322
|
+
'is-rounded': !!isRounded,
|
|
14323
|
+
});
|
|
14324
|
+
return (jsxRuntime.jsx("span", { ref: ref, className: classNames(swatchClass, bulmaHelperClasses, className), style: { ...style, ['--color-swatch-color']: color }, ...rest, role: "img", "aria-label": rest['aria-label'] || label || color, children: children !== undefined && children !== null && (jsxRuntime.jsx("span", { "aria-hidden": "true", children: children })) }));
|
|
14325
|
+
});
|
|
14326
|
+
ColorSwatch.displayName = 'ColorSwatch';
|
|
13646
14327
|
|
|
14328
|
+
// Valid size modifiers for the content class
|
|
14329
|
+
const validSizes = ['small', 'medium', 'large'];
|
|
13647
14330
|
/**
|
|
13648
|
-
* The `
|
|
14331
|
+
* The `Content` component applies Bulma’s typographic styles to its children, enhancing the appearance of HTML elements like paragraphs, headings, lists, and tables.
|
|
13649
14332
|
*
|
|
13650
14333
|
* @function
|
|
13651
|
-
* @param {
|
|
13652
|
-
* @returns {JSX.Element} The rendered
|
|
13653
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
14334
|
+
* @param {ContentProps} props - Props for the Content component.
|
|
14335
|
+
* @returns {JSX.Element} The rendered content block.
|
|
14336
|
+
* @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
|
|
13654
14337
|
*/
|
|
13655
|
-
const
|
|
14338
|
+
const Content = ({ className, textColor, color, bgColor, size, children, ...props }) => {
|
|
13656
14339
|
/**
|
|
13657
14340
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13658
14341
|
*/
|
|
@@ -13661,23 +14344,24 @@ const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, ch
|
|
|
13661
14344
|
backgroundColor: bgColor,
|
|
13662
14345
|
...props,
|
|
13663
14346
|
});
|
|
13664
|
-
const bulmaClasses = usePrefixedClassNames('
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
14347
|
+
const bulmaClasses = usePrefixedClassNames('content', {
|
|
14348
|
+
[`is-${size}`]: size && size !== 'normal' && validSizes.includes(size),
|
|
14349
|
+
});
|
|
14350
|
+
const contentClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14351
|
+
return (jsxRuntime.jsx("div", { className: contentClasses, ...rest, children: children }));
|
|
13669
14352
|
};
|
|
13670
|
-
const IconText = withSubComponents(IconTextComponent, { Icon }, 'IconText');
|
|
13671
14353
|
|
|
13672
14354
|
/**
|
|
13673
|
-
* The `
|
|
14355
|
+
* The `Delete` component provides a Bulma-styled close/delete button for dismissing modals, notifications, tags, messages, and more.
|
|
14356
|
+
*
|
|
14357
|
+
* It renders `type="button"`, so a delete button inside a form does not submit it.
|
|
13674
14358
|
*
|
|
13675
14359
|
* @function
|
|
13676
|
-
* @param {
|
|
13677
|
-
* @returns {JSX.Element} The rendered
|
|
13678
|
-
* @see {@link https://bulma.io/documentation/elements/
|
|
14360
|
+
* @param {DeleteProps} props - Props for the Delete component.
|
|
14361
|
+
* @returns {JSX.Element} The rendered delete button.
|
|
14362
|
+
* @see {@link https://bulma.io/documentation/elements/delete/ | Bulma Delete documentation}
|
|
13679
14363
|
*/
|
|
13680
|
-
const
|
|
14364
|
+
const Delete = ({ className, textColor, color, bgColor, onClick, size, ariaLabel, disabled = false, ...props }) => {
|
|
13681
14365
|
/**
|
|
13682
14366
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13683
14367
|
*/
|
|
@@ -13686,150 +14370,412 @@ const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRe
|
|
|
13686
14370
|
backgroundColor: bgColor,
|
|
13687
14371
|
...props,
|
|
13688
14372
|
});
|
|
13689
|
-
|
|
13690
|
-
// container carries just its `is-<ratio>` class.
|
|
13691
|
-
const bulmaClasses = usePrefixedClassNames('image', {
|
|
14373
|
+
const bulmaClasses = usePrefixedClassNames('delete', {
|
|
13692
14374
|
[`is-${size}`]: size,
|
|
14375
|
+
'is-disabled': disabled,
|
|
13693
14376
|
});
|
|
13694
|
-
const
|
|
13695
|
-
|
|
13696
|
-
// If not, use <figure> for aspect ratios, <div> otherwise.
|
|
13697
|
-
const Tag = as ?? (size?.includes('by') ? 'figure' : 'div');
|
|
13698
|
-
const roundedClass = usePrefixedClassNames('is-rounded');
|
|
13699
|
-
const content = children ? (children) : (jsxRuntime.jsx("img", { className: classNames({ [roundedClass]: isRounded }) || undefined, src: src, alt: alt, ...(isRetina && src ? { srcSet: `${src} 2x` } : {}) }));
|
|
13700
|
-
return (jsxRuntime.jsx(Tag, { className: imageClasses, ...rest, children: content }));
|
|
14377
|
+
const classes = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14378
|
+
return (jsxRuntime.jsx("button", { className: classes, onClick: onClick, disabled: disabled, ...rest, "aria-label": rest['aria-label'] || ariaLabel || 'Close', type: buttonType(rest.type) }));
|
|
13701
14379
|
};
|
|
13702
14380
|
|
|
14381
|
+
const validDescriptionListOrientations = ['vertical', 'horizontal'];
|
|
14382
|
+
const isOrientation = (orientation) => !!orientation &&
|
|
14383
|
+
validDescriptionListOrientations.includes(orientation);
|
|
14384
|
+
/** Whether a prop was given something to render. */
|
|
14385
|
+
const isGiven = (node) => node !== undefined &&
|
|
14386
|
+
node !== null &&
|
|
14387
|
+
typeof node !== 'boolean' &&
|
|
14388
|
+
node !== '';
|
|
13703
14389
|
/**
|
|
13704
|
-
*
|
|
13705
|
-
*
|
|
13706
|
-
* It doesn't wrap in the middle and isn't drawn as a key itself. It sets no
|
|
13707
|
-
* font size, so the keys in it are the size of a key on its own.
|
|
14390
|
+
* A label, the name of a fact: a `<dt>`, in the weaker text color.
|
|
13708
14391
|
*
|
|
13709
14392
|
* @function
|
|
13710
|
-
* @param {
|
|
13711
|
-
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the
|
|
13712
|
-
* @returns {JSX.Element} The rendered
|
|
14393
|
+
* @param {DescriptionListLabelProps} props - Props for DescriptionList.Label.
|
|
14394
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dt>`.
|
|
14395
|
+
* @returns {JSX.Element} The rendered label.
|
|
13713
14396
|
*
|
|
13714
14397
|
* @example
|
|
13715
|
-
* <
|
|
13716
|
-
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
|
|
14398
|
+
* <DescriptionList.Label>Placed on</DescriptionList.Label>
|
|
14399
|
+
*/
|
|
14400
|
+
const DescriptionListLabel = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
|
|
14401
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14402
|
+
color: textColor,
|
|
14403
|
+
backgroundColor: bgColor,
|
|
14404
|
+
...props,
|
|
14405
|
+
});
|
|
14406
|
+
const labelClass = usePrefixedClassNames('description-list-label');
|
|
14407
|
+
return (jsxRuntime.jsx("dt", { ref: ref, className: classNames(labelClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
14408
|
+
});
|
|
14409
|
+
DescriptionListLabel.displayName = 'DescriptionList.Label';
|
|
14410
|
+
/**
|
|
14411
|
+
* A value, what the label says about the thing: a `<dd>`. An item may have several.
|
|
14412
|
+
*
|
|
14413
|
+
* @function
|
|
14414
|
+
* @param {DescriptionListValueProps} props - Props for DescriptionList.Value.
|
|
14415
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<dd>`.
|
|
14416
|
+
* @returns {JSX.Element} The rendered value.
|
|
13720
14417
|
*
|
|
13721
14418
|
* @example
|
|
13722
|
-
* <
|
|
13723
|
-
* <Kbd>G</Kbd>
|
|
13724
|
-
* <Kbd>I</Kbd>
|
|
13725
|
-
* </Kbd.Group>
|
|
14419
|
+
* <DescriptionList.Value>14 October 2026</DescriptionList.Value>
|
|
13726
14420
|
*/
|
|
13727
|
-
const
|
|
14421
|
+
const DescriptionListValue = React.forwardRef(({ className, textColor, bgColor, children, ...props }, ref) => {
|
|
13728
14422
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13729
14423
|
color: textColor,
|
|
14424
|
+
backgroundColor: bgColor,
|
|
13730
14425
|
...props,
|
|
13731
14426
|
});
|
|
13732
|
-
const
|
|
13733
|
-
|
|
13734
|
-
// Null, undefined and booleans drop out, and Fragments are opened up, so
|
|
13735
|
-
// an empty Fragment leaves no separator behind and the keys in one are
|
|
13736
|
-
// separated like the rest. The spaces are part of the text, so a screen
|
|
13737
|
-
// reader reads "Ctrl plus S" rather than one run-together word, whatever
|
|
13738
|
-
// the CSS does.
|
|
13739
|
-
const keys = flattenChildren(children);
|
|
13740
|
-
return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(groupClass, bulmaHelperClasses, className), ...rest, children: keys.map((key, index) => (jsxRuntime.jsxs(React.Fragment, { children: [index > 0 && (jsxRuntime.jsx("span", { className: separatorClass, children: ` ${separator} ` })), key] }, React.isValidElement(key) && key.key != null ? key.key : index))) }));
|
|
14427
|
+
const valueClass = usePrefixedClassNames('description-list-value');
|
|
14428
|
+
return (jsxRuntime.jsx("dd", { ref: ref, className: classNames(valueClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
13741
14429
|
});
|
|
13742
|
-
|
|
14430
|
+
DescriptionListValue.displayName = 'DescriptionList.Value';
|
|
13743
14431
|
/**
|
|
13744
|
-
*
|
|
13745
|
-
*
|
|
13746
|
-
* It only shows the key: it doesn't listen for it. The key takes its colors
|
|
13747
|
-
* from the text it sits in, and its size in `em`, so it fits a paragraph, a
|
|
13748
|
-
* title or a filled button as it is, and `textSize` sizes it outright. For a
|
|
13749
|
-
* combination or a sequence, put the keys in a `Kbd.Group`. What tells
|
|
13750
|
-
* assistive tech a control has a shortcut is `aria-keyshortcuts` on that
|
|
13751
|
-
* control, with `Kbd` as the hint people see next to it.
|
|
14432
|
+
* One fact: its label and its value, in a `<div>`, from the `label` and `value` props or from `Label` and `Value` children.
|
|
13752
14433
|
*
|
|
13753
14434
|
* @function
|
|
13754
|
-
* @param {
|
|
13755
|
-
* @param {React.Ref<
|
|
13756
|
-
* @returns {JSX.Element} The rendered
|
|
14435
|
+
* @param {DescriptionListItemProps} props - Props for DescriptionList.Item.
|
|
14436
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the `<div>`.
|
|
14437
|
+
* @returns {JSX.Element} The rendered item.
|
|
13757
14438
|
*
|
|
13758
14439
|
* @example
|
|
13759
|
-
* <
|
|
14440
|
+
* <DescriptionList.Item label="Order" value="#1234" />
|
|
13760
14441
|
*
|
|
13761
14442
|
* @example
|
|
13762
|
-
* <
|
|
14443
|
+
* <DescriptionList.Item label="Tags">
|
|
14444
|
+
* <DescriptionList.Value>Gift</DescriptionList.Value>
|
|
14445
|
+
* <DescriptionList.Value>Priority</DescriptionList.Value>
|
|
14446
|
+
* </DescriptionList.Item>
|
|
13763
14447
|
*/
|
|
13764
|
-
const
|
|
14448
|
+
const DescriptionListItem = React.forwardRef(({ className, label, value, textColor, bgColor, children, ...props }, ref) => {
|
|
13765
14449
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13766
14450
|
color: textColor,
|
|
13767
14451
|
backgroundColor: bgColor,
|
|
13768
14452
|
...props,
|
|
13769
14453
|
});
|
|
13770
|
-
const
|
|
13771
|
-
|
|
13772
|
-
// prohibits on kbd. Its styles are inline, so it stays hidden in a
|
|
13773
|
-
// modular build that leaves out Bulma's helper classes.
|
|
13774
|
-
return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(kbdClass, bulmaHelperClasses, className), ...rest, children: label ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { "aria-hidden": "true", children: children }), jsxRuntime.jsx("span", { style: visuallyHidden, children: label })] })) : (children) }));
|
|
14454
|
+
const itemClass = usePrefixedClassNames('description-list-item');
|
|
14455
|
+
return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(itemClass, bulmaHelperClasses, className), ...rest, children: [isGiven(label) && jsxRuntime.jsx(DescriptionListLabel, { children: label }), children, isGiven(value) && jsxRuntime.jsx(DescriptionListValue, { children: value })] }));
|
|
13775
14456
|
});
|
|
13776
|
-
|
|
13777
|
-
|
|
14457
|
+
DescriptionListItem.displayName = 'DescriptionList.Item';
|
|
13778
14458
|
/**
|
|
13779
|
-
* The `
|
|
14459
|
+
* The `DescriptionList` component lists facts about one thing, each a label and its value, such as an order's details or a profile.
|
|
14460
|
+
*
|
|
14461
|
+
* It's a `<dl>`, with a `<div>` for each item, a `<dt>` for each label and
|
|
14462
|
+
* a `<dd>` for each value, so assistive tech gets HTML's own list of terms
|
|
14463
|
+
* and definitions and no roles are added. Labels sit above their values on
|
|
14464
|
+
* a phone and beside them from Bulma's tablet breakpoint up, where every
|
|
14465
|
+
* label shares one column so the values line up. For data with two
|
|
14466
|
+
* dimensions, such as several records with the same fields, use a `Table`.
|
|
13780
14467
|
*
|
|
13781
14468
|
* @function
|
|
13782
|
-
* @param {
|
|
13783
|
-
* @param {React.Ref} ref - Forwarded ref to the
|
|
13784
|
-
* @returns {JSX.Element} The rendered
|
|
13785
|
-
*
|
|
14469
|
+
* @param {DescriptionListProps} props - Props for the DescriptionList component.
|
|
14470
|
+
* @param {React.Ref<HTMLDListElement>} ref - Forwarded ref to the `<dl>`.
|
|
14471
|
+
* @returns {JSX.Element} The rendered list.
|
|
14472
|
+
*
|
|
14473
|
+
* @example
|
|
14474
|
+
* <DescriptionList aria-labelledby="order-heading">
|
|
14475
|
+
* <DescriptionList.Item label="Order" value="#1234" />
|
|
14476
|
+
* <DescriptionList.Item label="Placed on" value="14 October 2026" />
|
|
14477
|
+
* </DescriptionList>
|
|
13786
14478
|
*/
|
|
13787
|
-
const
|
|
13788
|
-
const { className, textColor, bgColor, isActive, as: Component = 'a', children, ...props } = linkProps;
|
|
13789
|
-
/**
|
|
13790
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
13791
|
-
*/
|
|
14479
|
+
const DescriptionListRoot = React.forwardRef(({ className, orientation, orientationMobile, orientationTablet, orientationDesktop, orientationWidescreen, orientationFullhd, hasDividers, textColor, bgColor, children, ...props }, ref) => {
|
|
13792
14480
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13793
14481
|
color: textColor,
|
|
13794
14482
|
backgroundColor: bgColor,
|
|
13795
14483
|
...props,
|
|
13796
14484
|
});
|
|
13797
|
-
|
|
13798
|
-
|
|
14485
|
+
// A class for each orientation given, which the stylesheet applies from
|
|
14486
|
+
// that viewport's breakpoint, so the layout is right on the first paint
|
|
14487
|
+
// and on the server. With none, the stylesheet's default is responsive.
|
|
14488
|
+
const rootClass = usePrefixedClassNames('description-list', {
|
|
14489
|
+
[`is-${orientation}`]: isOrientation(orientation),
|
|
14490
|
+
[`is-${orientationMobile}-mobile`]: isOrientation(orientationMobile),
|
|
14491
|
+
[`is-${orientationTablet}-tablet`]: isOrientation(orientationTablet),
|
|
14492
|
+
[`is-${orientationDesktop}-desktop`]: isOrientation(orientationDesktop),
|
|
14493
|
+
[`is-${orientationWidescreen}-widescreen`]: isOrientation(orientationWidescreen),
|
|
14494
|
+
[`is-${orientationFullhd}-fullhd`]: isOrientation(orientationFullhd),
|
|
14495
|
+
'has-dividers': !!hasDividers,
|
|
13799
14496
|
});
|
|
13800
|
-
|
|
13801
|
-
return (jsxRuntime.jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
|
|
14497
|
+
return (jsxRuntime.jsx("dl", { ref: ref, className: classNames(rootClass, bulmaHelperClasses, className), ...rest, children: children }));
|
|
13802
14498
|
});
|
|
13803
|
-
|
|
14499
|
+
const DescriptionList = withSubComponents(DescriptionListRoot, {
|
|
14500
|
+
Item: DescriptionListItem,
|
|
14501
|
+
Label: DescriptionListLabel,
|
|
14502
|
+
Value: DescriptionListValue,
|
|
14503
|
+
}, 'DescriptionList');
|
|
13804
14504
|
|
|
13805
14505
|
/**
|
|
13806
|
-
* The `
|
|
14506
|
+
* The `Divider` component renders a styled horizontal rule (`<hr>`) element with Bulma helper class integration.
|
|
13807
14507
|
*
|
|
13808
14508
|
* @function
|
|
13809
|
-
* @param {
|
|
13810
|
-
* @returns {JSX.Element} The rendered
|
|
14509
|
+
* @param {DividerProps} props - Props for the Divider component.
|
|
14510
|
+
* @returns {JSX.Element} The rendered hr element.
|
|
13811
14511
|
*/
|
|
13812
|
-
const
|
|
14512
|
+
const Divider = ({ className, bgColor, ...props }) => {
|
|
13813
14513
|
/**
|
|
13814
14514
|
* Generates Bulma helper classes and separates out remaining props.
|
|
13815
14515
|
*/
|
|
13816
14516
|
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
13817
|
-
color: textColor,
|
|
13818
14517
|
backgroundColor: bgColor,
|
|
13819
14518
|
...props,
|
|
13820
14519
|
});
|
|
13821
14520
|
const bulmaClasses = usePrefixedClassNames();
|
|
13822
|
-
const
|
|
13823
|
-
return
|
|
14521
|
+
const dividerClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14522
|
+
return jsxRuntime.jsx("hr", { className: dividerClasses || undefined, ...rest });
|
|
13824
14523
|
};
|
|
13825
14524
|
|
|
13826
14525
|
/**
|
|
13827
|
-
* The `
|
|
14526
|
+
* The `Emphasis` component renders a styled `<em>` element with Bulma helper class integration.
|
|
13828
14527
|
*
|
|
13829
|
-
*
|
|
13830
|
-
*
|
|
13831
|
-
*
|
|
13832
|
-
|
|
14528
|
+
* @function
|
|
14529
|
+
* @param {EmphasisProps} props - Props for the Emphasis component.
|
|
14530
|
+
* @returns {JSX.Element} The rendered em element.
|
|
14531
|
+
*/
|
|
14532
|
+
const Emphasis = ({ className, textColor, bgColor, children, ...props }) => {
|
|
14533
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14534
|
+
color: textColor,
|
|
14535
|
+
backgroundColor: bgColor,
|
|
14536
|
+
...props,
|
|
14537
|
+
});
|
|
14538
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
14539
|
+
const emphasisClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14540
|
+
return (jsxRuntime.jsx("em", { className: emphasisClasses || undefined, ...rest, children: children }));
|
|
14541
|
+
};
|
|
14542
|
+
|
|
14543
|
+
/**
|
|
14544
|
+
* FigureCaption component for rendering a styled figcaption element.
|
|
14545
|
+
*
|
|
14546
|
+
* A FigureCaption wraps the HTML `<figcaption>` element with Bulma helper class integration.
|
|
14547
|
+
* Use it inside Figure components to provide captions for images or other media.
|
|
14548
|
+
*
|
|
14549
|
+
* @function
|
|
14550
|
+
* @param {FigureCaptionProps} props - Props for the FigureCaption component.
|
|
14551
|
+
* @returns {JSX.Element} The rendered figcaption element.
|
|
14552
|
+
*/
|
|
14553
|
+
const FigureCaption = ({ className, textColor, bgColor, children, ...props }) => {
|
|
14554
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14555
|
+
color: textColor,
|
|
14556
|
+
backgroundColor: bgColor,
|
|
14557
|
+
...props,
|
|
14558
|
+
});
|
|
14559
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
14560
|
+
const captionClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14561
|
+
return (jsxRuntime.jsx("figcaption", { className: captionClasses || undefined, ...rest, children: children }));
|
|
14562
|
+
};
|
|
14563
|
+
/**
|
|
14564
|
+
* The `Figure` component renders a styled `<figure>` element with Bulma helper class integration.
|
|
14565
|
+
*
|
|
14566
|
+
* @function
|
|
14567
|
+
* @param {FigureProps} props - Props for the Figure component.
|
|
14568
|
+
* @returns {JSX.Element} The rendered figure element.
|
|
14569
|
+
*/
|
|
14570
|
+
const FigureComponent = ({ className, textColor, bgColor, children, ...props }) => {
|
|
14571
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14572
|
+
color: textColor,
|
|
14573
|
+
backgroundColor: bgColor,
|
|
14574
|
+
...props,
|
|
14575
|
+
});
|
|
14576
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
14577
|
+
const figureClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14578
|
+
return (jsxRuntime.jsx("figure", { className: figureClasses || undefined, ...rest, children: children }));
|
|
14579
|
+
};
|
|
14580
|
+
/**
|
|
14581
|
+
* Figure component with Caption subcomponent.
|
|
14582
|
+
*
|
|
14583
|
+
* @example
|
|
14584
|
+
* <Figure>
|
|
14585
|
+
* <img src="image.jpg" alt="Description" />
|
|
14586
|
+
* <Figure.Caption>Image caption text</Figure.Caption>
|
|
14587
|
+
* </Figure>
|
|
14588
|
+
*/
|
|
14589
|
+
const Figure = withSubComponents(FigureComponent, {
|
|
14590
|
+
Caption: FigureCaption,
|
|
14591
|
+
}, 'Figure');
|
|
14592
|
+
|
|
14593
|
+
/**
|
|
14594
|
+
* The `IconText` component provides a Bulma-styled horizontal arrangement of one or more `Icon` components and optional text.
|
|
14595
|
+
*
|
|
14596
|
+
* @function
|
|
14597
|
+
* @param {IconTextProps} props - Props for the IconText component.
|
|
14598
|
+
* @returns {JSX.Element} The rendered icon text element.
|
|
14599
|
+
* @see {@link https://bulma.io/documentation/elements/icon/#icon-text | Bulma IconText documentation}
|
|
14600
|
+
*/
|
|
14601
|
+
const IconTextComponent = ({ className, textColor, color, bgColor, iconProps, children, items, ...props }) => {
|
|
14602
|
+
/**
|
|
14603
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
14604
|
+
*/
|
|
14605
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14606
|
+
color: textColor ?? color,
|
|
14607
|
+
backgroundColor: bgColor,
|
|
14608
|
+
...props,
|
|
14609
|
+
});
|
|
14610
|
+
const bulmaClasses = usePrefixedClassNames('icon-text');
|
|
14611
|
+
const iconTextClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14612
|
+
return (jsxRuntime.jsx("span", { className: iconTextClasses, ...rest, children: items ? (items.map((item, index) => (jsxRuntime.jsxs(React.Fragment, { children: [item.iconProps &&
|
|
14613
|
+
(isIconProps(item.iconProps) ? (jsxRuntime.jsx(Icon, { ...item.iconProps })) : (jsxRuntime.jsx(Icon, { children: item.iconProps }))), item.text && jsxRuntime.jsx("span", { children: item.text })] }, index)))) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [iconProps &&
|
|
14614
|
+
(isIconProps(iconProps) ? (jsxRuntime.jsx(Icon, { ...iconProps })) : (jsxRuntime.jsx(Icon, { children: iconProps }))), children && jsxRuntime.jsx("span", { children: children })] })) }));
|
|
14615
|
+
};
|
|
14616
|
+
const IconText = withSubComponents(IconTextComponent, { Icon }, 'IconText');
|
|
14617
|
+
|
|
14618
|
+
/**
|
|
14619
|
+
* The `Image` component wraps images, iframes, or custom content in a Bulma-styled container, supporting fixed sizes, aspect ratios, rounded corners, retina images, and all Bulma helper props for color and spacing.
|
|
14620
|
+
*
|
|
14621
|
+
* @function
|
|
14622
|
+
* @param {ImageProps} props - Props for the Image component.
|
|
14623
|
+
* @returns {JSX.Element} The rendered image element.
|
|
14624
|
+
* @see {@link https://bulma.io/documentation/elements/image/ | Bulma Image documentation}
|
|
14625
|
+
*/
|
|
14626
|
+
const Image = ({ as, className, textColor, color, bgColor, size, isRounded, isRetina, src, alt, children, ...props }) => {
|
|
14627
|
+
/**
|
|
14628
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
14629
|
+
*/
|
|
14630
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14631
|
+
color: textColor ?? color,
|
|
14632
|
+
backgroundColor: bgColor,
|
|
14633
|
+
...props,
|
|
14634
|
+
});
|
|
14635
|
+
// Bulma reads `.has-ratio` only on a child of the ratio container, so the
|
|
14636
|
+
// container carries just its `is-<ratio>` class.
|
|
14637
|
+
const bulmaClasses = usePrefixedClassNames('image', {
|
|
14638
|
+
[`is-${size}`]: size,
|
|
14639
|
+
});
|
|
14640
|
+
const imageClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14641
|
+
// Default tag logic: if "as" is provided, use it.
|
|
14642
|
+
// If not, use <figure> for aspect ratios, <div> otherwise.
|
|
14643
|
+
const Tag = as ?? (size?.includes('by') ? 'figure' : 'div');
|
|
14644
|
+
const roundedClass = usePrefixedClassNames('is-rounded');
|
|
14645
|
+
const content = children ? (children) : (jsxRuntime.jsx("img", { className: classNames({ [roundedClass]: isRounded }) || undefined, src: src, alt: alt, ...(isRetina && src ? { srcSet: `${src} 2x` } : {}) }));
|
|
14646
|
+
return (jsxRuntime.jsx(Tag, { className: imageClasses, ...rest, children: content }));
|
|
14647
|
+
};
|
|
14648
|
+
|
|
14649
|
+
/**
|
|
14650
|
+
* Keys pressed together or one after another, in the outer `<kbd>` HTML gives a combination, with the separator between each pair as text.
|
|
14651
|
+
*
|
|
14652
|
+
* It doesn't wrap in the middle and isn't drawn as a key itself. It sets no
|
|
14653
|
+
* font size, so the keys in it are the size of a key on its own.
|
|
14654
|
+
*
|
|
14655
|
+
* @function
|
|
14656
|
+
* @param {KbdGroupProps} props - Props for Kbd.Group.
|
|
14657
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the outer `<kbd>`.
|
|
14658
|
+
* @returns {JSX.Element} The rendered combination.
|
|
14659
|
+
*
|
|
14660
|
+
* @example
|
|
14661
|
+
* <Kbd.Group>
|
|
14662
|
+
* <Kbd>Ctrl</Kbd>
|
|
14663
|
+
* <Kbd>Shift</Kbd>
|
|
14664
|
+
* <Kbd>P</Kbd>
|
|
14665
|
+
* </Kbd.Group>
|
|
14666
|
+
*
|
|
14667
|
+
* @example
|
|
14668
|
+
* <Kbd.Group separator="then">
|
|
14669
|
+
* <Kbd>G</Kbd>
|
|
14670
|
+
* <Kbd>I</Kbd>
|
|
14671
|
+
* </Kbd.Group>
|
|
14672
|
+
*/
|
|
14673
|
+
const KbdGroup = React.forwardRef(({ className, textColor, separator = '+', children, ...props }, ref) => {
|
|
14674
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14675
|
+
color: textColor,
|
|
14676
|
+
...props,
|
|
14677
|
+
});
|
|
14678
|
+
const groupClass = usePrefixedClassNames('kbd-group');
|
|
14679
|
+
const separatorClass = usePrefixedClassNames('kbd-separator');
|
|
14680
|
+
// Null, undefined and booleans drop out, and Fragments are opened up, so
|
|
14681
|
+
// an empty Fragment leaves no separator behind and the keys in one are
|
|
14682
|
+
// separated like the rest. The spaces are part of the text, so a screen
|
|
14683
|
+
// reader reads "Ctrl plus S" rather than one run-together word, whatever
|
|
14684
|
+
// the CSS does.
|
|
14685
|
+
const keys = flattenChildren(children);
|
|
14686
|
+
return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(groupClass, bulmaHelperClasses, className), ...rest, children: keys.map((key, index) => (jsxRuntime.jsxs(React.Fragment, { children: [index > 0 && (jsxRuntime.jsx("span", { className: separatorClass, children: ` ${separator} ` })), key] }, React.isValidElement(key) && key.key != null ? key.key : index))) }));
|
|
14687
|
+
});
|
|
14688
|
+
KbdGroup.displayName = 'Kbd.Group';
|
|
14689
|
+
/**
|
|
14690
|
+
* The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.
|
|
14691
|
+
*
|
|
14692
|
+
* It only shows the key: it doesn't listen for it. The key takes its colors
|
|
14693
|
+
* from the text it sits in, and its size in `em`, so it fits a paragraph, a
|
|
14694
|
+
* title or a filled button as it is, and `textSize` sizes it outright. For a
|
|
14695
|
+
* combination or a sequence, put the keys in a `Kbd.Group`. What tells
|
|
14696
|
+
* assistive tech a control has a shortcut is `aria-keyshortcuts` on that
|
|
14697
|
+
* control, with `Kbd` as the hint people see next to it.
|
|
14698
|
+
*
|
|
14699
|
+
* @function
|
|
14700
|
+
* @param {KbdProps} props - Props for the Kbd component.
|
|
14701
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<kbd>`.
|
|
14702
|
+
* @returns {JSX.Element} The rendered key.
|
|
14703
|
+
*
|
|
14704
|
+
* @example
|
|
14705
|
+
* <Kbd>Esc</Kbd>
|
|
14706
|
+
*
|
|
14707
|
+
* @example
|
|
14708
|
+
* <Kbd label="Command">⌘</Kbd>
|
|
14709
|
+
*/
|
|
14710
|
+
const KbdRoot = React.forwardRef(({ className, textColor, bgColor, label, children, ...props }, ref) => {
|
|
14711
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14712
|
+
color: textColor,
|
|
14713
|
+
backgroundColor: bgColor,
|
|
14714
|
+
...props,
|
|
14715
|
+
});
|
|
14716
|
+
const kbdClass = usePrefixedClassNames('kbd');
|
|
14717
|
+
// The name is text rather than an aria-label, which ARIA in HTML
|
|
14718
|
+
// prohibits on kbd. Its styles are inline, so it stays hidden in a
|
|
14719
|
+
// modular build that leaves out Bulma's helper classes.
|
|
14720
|
+
return (jsxRuntime.jsx("kbd", { ref: ref, className: classNames(kbdClass, bulmaHelperClasses, className), ...rest, children: label ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { "aria-hidden": "true", children: children }), jsxRuntime.jsx("span", { style: visuallyHidden, children: label })] })) : (children) }));
|
|
14721
|
+
});
|
|
14722
|
+
const Kbd = withSubComponents(KbdRoot, { Group: KbdGroup }, 'Kbd');
|
|
14723
|
+
|
|
14724
|
+
/**
|
|
14725
|
+
* The `Link` component renders a styled anchor (`<a>`) element with Bulma helper class integration.
|
|
14726
|
+
*
|
|
14727
|
+
* @function
|
|
14728
|
+
* @param {LinkProps} props - Props for the Link component.
|
|
14729
|
+
* @param {React.Ref} ref - Forwarded ref to the element `as` renders.
|
|
14730
|
+
* @returns {JSX.Element} The rendered anchor, or whatever `as` names.
|
|
14731
|
+
* @see {@link https://bulma.io/documentation/elements/content/ | Bulma Content documentation}
|
|
14732
|
+
*/
|
|
14733
|
+
const Link = React.forwardRef(function Link(linkProps, ref) {
|
|
14734
|
+
const { className, textColor, bgColor, isActive, as: Component = 'a', children, ...props } = linkProps;
|
|
14735
|
+
/**
|
|
14736
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
14737
|
+
*/
|
|
14738
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14739
|
+
color: textColor,
|
|
14740
|
+
backgroundColor: bgColor,
|
|
14741
|
+
...props,
|
|
14742
|
+
});
|
|
14743
|
+
const bulmaClasses = usePrefixedClassNames({
|
|
14744
|
+
'is-active': isActive,
|
|
14745
|
+
});
|
|
14746
|
+
const linkClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14747
|
+
return (jsxRuntime.jsx(Component, { ref: ref, className: linkClasses || undefined, ...rest, ...(Component === 'button' ? { type: buttonType(rest.type) } : {}), children: children }));
|
|
14748
|
+
});
|
|
14749
|
+
Link.displayName = 'Link';
|
|
14750
|
+
|
|
14751
|
+
/**
|
|
14752
|
+
* The `ListItem` component renders a styled list item (`<li>`) element with Bulma helper class integration.
|
|
14753
|
+
*
|
|
14754
|
+
* @function
|
|
14755
|
+
* @param {ListItemProps} props - Props for the ListItem component.
|
|
14756
|
+
* @returns {JSX.Element} The rendered li element.
|
|
14757
|
+
*/
|
|
14758
|
+
const ListItem = ({ className, textColor, bgColor, children, ...props }) => {
|
|
14759
|
+
/**
|
|
14760
|
+
* Generates Bulma helper classes and separates out remaining props.
|
|
14761
|
+
*/
|
|
14762
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
14763
|
+
color: textColor,
|
|
14764
|
+
backgroundColor: bgColor,
|
|
14765
|
+
...props,
|
|
14766
|
+
});
|
|
14767
|
+
const bulmaClasses = usePrefixedClassNames();
|
|
14768
|
+
const itemClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14769
|
+
return (jsxRuntime.jsx("li", { className: itemClasses || undefined, ...rest, children: children }));
|
|
14770
|
+
};
|
|
14771
|
+
|
|
14772
|
+
/**
|
|
14773
|
+
* The `Loader` component renders Bulma's `.loader`, a small spinning ring for inline loading states.
|
|
14774
|
+
*
|
|
14775
|
+
* The ring is `1em` square, so `textSize` scales it. It is exposed as an
|
|
14776
|
+
* indeterminate `progressbar` named by `ariaLabel`, and it stops spinning
|
|
14777
|
+
* under `prefers-reduced-motion: reduce` while staying drawn. In forced
|
|
14778
|
+
* colors its two open sides take the page's color, so it stays open.
|
|
13833
14779
|
*
|
|
13834
14780
|
* @function
|
|
13835
14781
|
* @param {LoaderProps} props - Props for the Loader component.
|
|
@@ -14235,7 +15181,7 @@ const validRingProgressColors = [
|
|
|
14235
15181
|
'white',
|
|
14236
15182
|
];
|
|
14237
15183
|
const validRingProgressSizes = ['small', 'medium', 'large'];
|
|
14238
|
-
const clamp$
|
|
15184
|
+
const clamp$4 = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
14239
15185
|
/**
|
|
14240
15186
|
* The `RingProgress` component draws a value as a ring that fills as it grows, with anything you pass shown in the middle.
|
|
14241
15187
|
*
|
|
@@ -14273,7 +15219,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
|
|
|
14273
15219
|
}
|
|
14274
15220
|
}, [named, mixed]);
|
|
14275
15221
|
const total = max > 0 ? max : 100;
|
|
14276
|
-
const share = (part) => (clamp$
|
|
15222
|
+
const share = (part) => (clamp$4(part, 0, total) / total) * 100;
|
|
14277
15223
|
// Each arc starts where the one before ended, and the last stops at the
|
|
14278
15224
|
// top, so a total over max fills the ring and no more.
|
|
14279
15225
|
const arcs = [];
|
|
@@ -14319,7 +15265,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
|
|
|
14319
15265
|
}
|
|
14320
15266
|
: {
|
|
14321
15267
|
role: isMeter ? 'meter' : 'progressbar',
|
|
14322
|
-
'aria-valuenow': clamp$
|
|
15268
|
+
'aria-valuenow': clamp$4(value ?? 0, 0, total),
|
|
14323
15269
|
'aria-valuemin': 0,
|
|
14324
15270
|
'aria-valuemax': total,
|
|
14325
15271
|
'aria-label': label || rest['aria-label'],
|
|
@@ -14334,31 +15280,7 @@ const RingProgress = React.forwardRef(({ className, textColor, bgColor, value, m
|
|
|
14334
15280
|
RingProgress.displayName = 'RingProgress';
|
|
14335
15281
|
|
|
14336
15282
|
/**
|
|
14337
|
-
* The `
|
|
14338
|
-
*
|
|
14339
|
-
* @function
|
|
14340
|
-
* @param {SkeletonProps} props - Props for the Skeleton component.
|
|
14341
|
-
* @returns {JSX.Element} The rendered skeleton element.
|
|
14342
|
-
*
|
|
14343
|
-
* @example
|
|
14344
|
-
* // Block skeleton
|
|
14345
|
-
* <Skeleton />
|
|
14346
|
-
*
|
|
14347
|
-
* @example
|
|
14348
|
-
* // Multi-line skeleton
|
|
14349
|
-
* <Skeleton variant="lines" lines={5} />
|
|
14350
|
-
*/
|
|
14351
|
-
const Skeleton = ({ className, variant = 'block', lines = 3, children, ...props }) => {
|
|
14352
|
-
const linesClass = usePrefixedClassNames('skeleton-lines');
|
|
14353
|
-
const blockClass = usePrefixedClassNames('skeleton-block');
|
|
14354
|
-
if (variant === 'lines') {
|
|
14355
|
-
return (jsxRuntime.jsx("div", { className: classNames(linesClass, className), ...props, children: Array.from({ length: lines }).map((_, i) => (jsxRuntime.jsx("div", {}, i))) }));
|
|
14356
|
-
}
|
|
14357
|
-
return (jsxRuntime.jsx("div", { className: classNames(blockClass, className), ...props, children: children }));
|
|
14358
|
-
};
|
|
14359
|
-
|
|
14360
|
-
/**
|
|
14361
|
-
* The `Span` component renders a styled inline `<span>` element with Bulma helper class integration.
|
|
15283
|
+
* The `Span` component renders a styled inline `<span>` element with Bulma helper class integration.
|
|
14362
15284
|
*
|
|
14363
15285
|
* @function
|
|
14364
15286
|
* @param {SpanProps} props - Props for the Span component.
|
|
@@ -14440,216 +15362,6 @@ const SubTitle = ({ className, size, as = 'h1', hasSkeleton, textColor, bgColor,
|
|
|
14440
15362
|
return (jsxRuntime.jsx(Tag, { className: subTitleClasses, ...rest, children: children }));
|
|
14441
15363
|
};
|
|
14442
15364
|
|
|
14443
|
-
/**
|
|
14444
|
-
* Thead component for rendering a styled Bulma table header.
|
|
14445
|
-
*
|
|
14446
|
-
* Supports Bulma helper classes for additional styling.
|
|
14447
|
-
*
|
|
14448
|
-
* @function
|
|
14449
|
-
* @param {TheadProps} props - Props for the Thead component.
|
|
14450
|
-
* @returns {JSX.Element} The rendered table header element.
|
|
14451
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
|
|
14452
|
-
*/
|
|
14453
|
-
const Thead = ({ className, children, ...props }) => {
|
|
14454
|
-
/**
|
|
14455
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14456
|
-
*/
|
|
14457
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14458
|
-
const theadClasses = classNames(className, bulmaHelperClasses);
|
|
14459
|
-
return (jsxRuntime.jsx("thead", { className: theadClasses || undefined, ...rest, children: children }));
|
|
14460
|
-
};
|
|
14461
|
-
|
|
14462
|
-
/**
|
|
14463
|
-
* Tbody component for rendering a styled Bulma table body.
|
|
14464
|
-
*
|
|
14465
|
-
* Supports Bulma helper classes for additional styling.
|
|
14466
|
-
*
|
|
14467
|
-
* @function
|
|
14468
|
-
* @param {TbodyProps} props - Props for the Tbody component.
|
|
14469
|
-
* @returns {JSX.Element} The rendered table body element.
|
|
14470
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
|
|
14471
|
-
*/
|
|
14472
|
-
const Tbody = ({ className, children, ...props }) => {
|
|
14473
|
-
/**
|
|
14474
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14475
|
-
*/
|
|
14476
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14477
|
-
const tbodyClasses = classNames(className, bulmaHelperClasses);
|
|
14478
|
-
return (jsxRuntime.jsx("tbody", { className: tbodyClasses || undefined, ...rest, children: children }));
|
|
14479
|
-
};
|
|
14480
|
-
|
|
14481
|
-
/**
|
|
14482
|
-
* Tfoot component for rendering a styled Bulma table footer.
|
|
14483
|
-
*
|
|
14484
|
-
* Supports Bulma helper classes for additional styling.
|
|
14485
|
-
*
|
|
14486
|
-
* @function
|
|
14487
|
-
* @param {TfootProps} props - Props for the Tfoot component.
|
|
14488
|
-
* @returns {JSX.Element} The rendered table footer element.
|
|
14489
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-footer | Bulma Table documentation}
|
|
14490
|
-
*/
|
|
14491
|
-
const Tfoot = ({ className, children, ...props }) => {
|
|
14492
|
-
/**
|
|
14493
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14494
|
-
*/
|
|
14495
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14496
|
-
const tfootClasses = classNames(className, bulmaHelperClasses);
|
|
14497
|
-
return (jsxRuntime.jsx("tfoot", { className: tfootClasses || undefined, ...rest, children: children }));
|
|
14498
|
-
};
|
|
14499
|
-
|
|
14500
|
-
/**
|
|
14501
|
-
* The `isAligned` values `Th` and `Td` accept. Each one writes
|
|
14502
|
-
* `has-text-<value>` on the cell, and a value outside the tuple adds no class.
|
|
14503
|
-
*
|
|
14504
|
-
* One copy, imported by both cells, so a header and its body cells always
|
|
14505
|
-
* take the same values.
|
|
14506
|
-
*
|
|
14507
|
-
* Its own module rather than `Th.tsx` or `Td.tsx` because `src/index.ts`
|
|
14508
|
-
* wildcard-exports those files, and everything published there is public
|
|
14509
|
-
* forever. `iconProps.ts` makes the same call.
|
|
14510
|
-
*/
|
|
14511
|
-
const validTableAlignments = ['left', 'right', 'centered'];
|
|
14512
|
-
|
|
14513
|
-
/**
|
|
14514
|
-
* The values the table `color` prop accepts, as a readonly tuple.
|
|
14515
|
-
*
|
|
14516
|
-
* `TableColor` is typed from it, and `Tr`, `Th` and `Td` all take a
|
|
14517
|
-
* `TableColor`, so the tuple and those props list the same values. Map over
|
|
14518
|
-
* it to build a color picker, or check a value that arrives at runtime before
|
|
14519
|
-
* passing it in: the components add no color class for a value outside the
|
|
14520
|
-
* tuple.
|
|
14521
|
-
*
|
|
14522
|
-
* @example
|
|
14523
|
-
* import { Tr, Td, validTableColors } from '@allxsmith/bestax-bulma';
|
|
14524
|
-
*
|
|
14525
|
-
* <Tr>
|
|
14526
|
-
* {validTableColors.map(color => (
|
|
14527
|
-
* <Td key={color} color={color}>
|
|
14528
|
-
* {color}
|
|
14529
|
-
* </Td>
|
|
14530
|
-
* ))}
|
|
14531
|
-
* </Tr>;
|
|
14532
|
-
*/
|
|
14533
|
-
const validTableColors = [
|
|
14534
|
-
'primary',
|
|
14535
|
-
'link',
|
|
14536
|
-
'info',
|
|
14537
|
-
'success',
|
|
14538
|
-
'warning',
|
|
14539
|
-
'danger',
|
|
14540
|
-
'black',
|
|
14541
|
-
'dark',
|
|
14542
|
-
'light',
|
|
14543
|
-
'white',
|
|
14544
|
-
];
|
|
14545
|
-
/**
|
|
14546
|
-
* Td component for rendering a styled Bulma table cell.
|
|
14547
|
-
*
|
|
14548
|
-
* Supports alignment, Bulma color modifiers, and helper classes for additional styling.
|
|
14549
|
-
*
|
|
14550
|
-
* @function
|
|
14551
|
-
* @param {TdProps} props - Props for the Td component.
|
|
14552
|
-
* @returns {JSX.Element} The rendered table cell element.
|
|
14553
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-body | Bulma Table documentation}
|
|
14554
|
-
*/
|
|
14555
|
-
const Td = ({ className, isAligned, color, children, ...props }) => {
|
|
14556
|
-
const bulmaClasses = usePrefixedClassNames('', {
|
|
14557
|
-
[`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
|
|
14558
|
-
[`is-${color}`]: color && validTableColors.includes(color),
|
|
14559
|
-
});
|
|
14560
|
-
/**
|
|
14561
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14562
|
-
*/
|
|
14563
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14564
|
-
const tdClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
14565
|
-
return (jsxRuntime.jsx("td", { className: tdClasses || undefined, ...rest, children: children }));
|
|
14566
|
-
};
|
|
14567
|
-
|
|
14568
|
-
/**
|
|
14569
|
-
* Tr component for rendering a styled Bulma table row.
|
|
14570
|
-
*
|
|
14571
|
-
* Supports the is-selected modifier and color modifiers.
|
|
14572
|
-
*
|
|
14573
|
-
* @function
|
|
14574
|
-
* @param {TrProps} props - Props for the Tr component.
|
|
14575
|
-
* @returns {JSX.Element} The rendered table row element.
|
|
14576
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-row | Bulma Table documentation}
|
|
14577
|
-
*/
|
|
14578
|
-
const Tr = ({ className, isSelected, color, children, ...props }) => {
|
|
14579
|
-
const bulmaClasses = usePrefixedClassNames('', {
|
|
14580
|
-
'is-selected': isSelected,
|
|
14581
|
-
[`is-${color}`]: color && validTableColors.includes(color),
|
|
14582
|
-
});
|
|
14583
|
-
/**
|
|
14584
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14585
|
-
*/
|
|
14586
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14587
|
-
const trClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
14588
|
-
return (jsxRuntime.jsx("tr", { className: trClasses || undefined, ...rest, children: children }));
|
|
14589
|
-
};
|
|
14590
|
-
|
|
14591
|
-
/**
|
|
14592
|
-
* Th component for rendering a styled Bulma table header cell.
|
|
14593
|
-
*
|
|
14594
|
-
* Supports alignment, width, and color modifiers.
|
|
14595
|
-
*
|
|
14596
|
-
* @function
|
|
14597
|
-
* @param {ThProps} props - Props for the Th component.
|
|
14598
|
-
* @returns {JSX.Element} The rendered table header cell element.
|
|
14599
|
-
* @see {@link https://bulma.io/documentation/elements/table/#table-head | Bulma Table documentation}
|
|
14600
|
-
*/
|
|
14601
|
-
const Th = ({ className, isAligned, width, color, children, ...props }) => {
|
|
14602
|
-
const bulmaClasses = usePrefixedClassNames('', {
|
|
14603
|
-
[`has-text-${isAligned}`]: isAligned && validTableAlignments.includes(isAligned),
|
|
14604
|
-
[`is-${color}`]: color && validTableColors.includes(color),
|
|
14605
|
-
});
|
|
14606
|
-
/**
|
|
14607
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14608
|
-
*/
|
|
14609
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14610
|
-
const thClasses = classNames(bulmaClasses, className, bulmaHelperClasses);
|
|
14611
|
-
return (jsxRuntime.jsx("th", { className: thClasses || undefined, style: width
|
|
14612
|
-
? { width: typeof width === 'number' ? `${width}px` : width }
|
|
14613
|
-
: undefined, ...rest, children: children }));
|
|
14614
|
-
};
|
|
14615
|
-
|
|
14616
|
-
/**
|
|
14617
|
-
* The `Table` component and its subcomponents provide a highly composable, Bulma-styled table system for React.
|
|
14618
|
-
*
|
|
14619
|
-
* @function
|
|
14620
|
-
* @param {TableProps} props - Props for the Table component.
|
|
14621
|
-
* @returns {JSX.Element} The rendered table element.
|
|
14622
|
-
* @see {@link https://bulma.io/documentation/elements/table/ | Bulma Table documentation}
|
|
14623
|
-
*/
|
|
14624
|
-
const TableComponent = ({ className, isBordered, isStriped, isNarrow, isHoverable, isFullwidth, isFullWidth, isResponsive, children, ...props }) => {
|
|
14625
|
-
/**
|
|
14626
|
-
* Generates Bulma helper classes and separates out remaining props.
|
|
14627
|
-
*/
|
|
14628
|
-
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
14629
|
-
const bulmaClasses = usePrefixedClassNames('table', {
|
|
14630
|
-
'is-bordered': isBordered,
|
|
14631
|
-
'is-striped': isStriped,
|
|
14632
|
-
'is-narrow': isNarrow,
|
|
14633
|
-
'is-hoverable': isHoverable,
|
|
14634
|
-
'is-fullwidth': isFullwidth ?? isFullWidth,
|
|
14635
|
-
});
|
|
14636
|
-
const containerClass = usePrefixedClassNames('table-container');
|
|
14637
|
-
const tableClasses = classNames(bulmaClasses, bulmaHelperClasses, className);
|
|
14638
|
-
const tableElement = (jsxRuntime.jsx("table", { className: tableClasses, ...rest, children: children }));
|
|
14639
|
-
if (isResponsive) {
|
|
14640
|
-
return jsxRuntime.jsx("div", { className: containerClass, children: tableElement });
|
|
14641
|
-
}
|
|
14642
|
-
return tableElement;
|
|
14643
|
-
};
|
|
14644
|
-
const Table = withSubComponents(TableComponent, {
|
|
14645
|
-
Thead,
|
|
14646
|
-
Tbody,
|
|
14647
|
-
Tfoot,
|
|
14648
|
-
Tr,
|
|
14649
|
-
Th,
|
|
14650
|
-
Td,
|
|
14651
|
-
}, 'Table');
|
|
14652
|
-
|
|
14653
15365
|
const validTagColors = [
|
|
14654
15366
|
'primary',
|
|
14655
15367
|
'link',
|
|
@@ -14964,20 +15676,528 @@ const CheckboxesComponent = ({ label, labelSize, labelProps, horizontal, message
|
|
|
14964
15676
|
};
|
|
14965
15677
|
const Checkboxes = withSubComponents(CheckboxesComponent, { Checkbox }, 'Checkboxes');
|
|
14966
15678
|
|
|
14967
|
-
|
|
14968
|
-
const
|
|
15679
|
+
const noFiles = [];
|
|
15680
|
+
const noRefusals = [];
|
|
15681
|
+
const noAnnouncements = [];
|
|
15682
|
+
const describeAnnouncement = (announcement) => announcement.text;
|
|
15683
|
+
/**
|
|
15684
|
+
* The tokens of an `accept` attribute that HTML recognizes, lowercased: an
|
|
15685
|
+
* extension (`.pdf`), a wildcard (`image/*`) or a MIME type. Others are
|
|
15686
|
+
* ignored, as browsers ignore them.
|
|
15687
|
+
*/
|
|
15688
|
+
const acceptTokens = (accept) => (accept ?? '')
|
|
15689
|
+
.split(',')
|
|
15690
|
+
.map(token => token.trim().toLowerCase())
|
|
15691
|
+
.filter(token => token.length > 1 && (token[0] === '.' || token.includes('/')));
|
|
15692
|
+
/** Whether `accept`'s tokens take a file with this name and type. */
|
|
15693
|
+
const accepts = (tokens, name, type) => {
|
|
15694
|
+
if (tokens.length === 0)
|
|
15695
|
+
return true;
|
|
15696
|
+
const lowerName = name.toLowerCase();
|
|
15697
|
+
const lowerType = type.toLowerCase();
|
|
15698
|
+
return tokens.some(token => token[0] === '.'
|
|
15699
|
+
? lowerName.endsWith(token)
|
|
15700
|
+
: token.endsWith('/*')
|
|
15701
|
+
? lowerType.startsWith(token.slice(0, -1))
|
|
15702
|
+
: lowerType === token);
|
|
15703
|
+
};
|
|
15704
|
+
/** Whether a drag or a clipboard carries files. */
|
|
15705
|
+
const carriesFiles = (data) => !!data && Array.from(data.types ?? []).includes('Files');
|
|
15706
|
+
/** The files a drop or a paste brings, with folders marked. */
|
|
15707
|
+
const incomingFrom = (data) => {
|
|
15708
|
+
if (!data)
|
|
15709
|
+
return [];
|
|
15710
|
+
const items = Array.from(data.items ?? []).filter(item => item.kind === 'file');
|
|
15711
|
+
if (items.length === 0) {
|
|
15712
|
+
return Array.from(data.files ?? [], file => ({ file, folder: false }));
|
|
15713
|
+
}
|
|
15714
|
+
const incoming = [];
|
|
15715
|
+
for (const item of items) {
|
|
15716
|
+
const file = item.getAsFile();
|
|
15717
|
+
if (!file)
|
|
15718
|
+
continue;
|
|
15719
|
+
const entry = item.webkitGetAsEntry?.();
|
|
15720
|
+
incoming.push({ file, folder: !!entry?.isDirectory });
|
|
15721
|
+
}
|
|
15722
|
+
return incoming;
|
|
15723
|
+
};
|
|
15724
|
+
/** The same file: one object, or one picked again with the same details. */
|
|
15725
|
+
const sameFile = (a, b) => a === b ||
|
|
15726
|
+
(a.name === b.name && a.size === b.size && a.lastModified === b.lastModified);
|
|
15727
|
+
/** Whether two lists hold the same files in the same order. */
|
|
15728
|
+
const sameFiles = (a, b) => !!a && a.length === b.length && b.every((file, i) => sameFile(a[i], file));
|
|
15729
|
+
/**
|
|
15730
|
+
* Writes files to a file input, through a `DataTransfer`, the one way script
|
|
15731
|
+
* can build a `FileList`. Where there's no `DataTransfer` the input is left as
|
|
15732
|
+
* it is.
|
|
15733
|
+
*/
|
|
15734
|
+
const writeFiles = (input, files) => {
|
|
15735
|
+
if (typeof DataTransfer !== 'function')
|
|
15736
|
+
return;
|
|
15737
|
+
const transfer = new DataTransfer();
|
|
15738
|
+
for (const file of files)
|
|
15739
|
+
transfer.items.add(file);
|
|
15740
|
+
input.files = transfer.files;
|
|
15741
|
+
};
|
|
15742
|
+
/** The units a file size is written in, largest first. */
|
|
15743
|
+
const sizeUnits = [
|
|
15744
|
+
['gigabyte', 1e9, 'GB'],
|
|
15745
|
+
['megabyte', 1e6, 'MB'],
|
|
15746
|
+
['kilobyte', 1e3, 'kB'],
|
|
15747
|
+
];
|
|
14969
15748
|
/**
|
|
14970
|
-
*
|
|
14971
|
-
*
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
15749
|
+
* A file size for display, in decimal units ("1.2 MB"), in the locale of a
|
|
15750
|
+
* `LocaleProvider` around it, or as English writes it outside one.
|
|
15751
|
+
*/
|
|
15752
|
+
const formatFileSize = (bytes, formatNumber) => {
|
|
15753
|
+
for (const [unit, factor, symbol] of sizeUnits) {
|
|
15754
|
+
if (bytes >= factor) {
|
|
15755
|
+
const value = bytes / factor;
|
|
15756
|
+
return formatNumber
|
|
15757
|
+
? formatNumber(value, {
|
|
15758
|
+
style: 'unit',
|
|
15759
|
+
unit,
|
|
15760
|
+
unitDisplay: 'short',
|
|
15761
|
+
maximumFractionDigits: 1,
|
|
15762
|
+
})
|
|
15763
|
+
: `${Math.round(value * 10) / 10} ${symbol}`;
|
|
15764
|
+
}
|
|
15765
|
+
}
|
|
15766
|
+
return formatNumber
|
|
15767
|
+
? formatNumber(bytes, { style: 'unit', unit: 'byte', unitDisplay: 'long' })
|
|
15768
|
+
: `${bytes} ${bytes === 1 ? 'byte' : 'bytes'}`;
|
|
15769
|
+
};
|
|
15770
|
+
/** A word for each kind of file `accept` names: "PDF", "image". */
|
|
15771
|
+
const typeNames = (tokens) => {
|
|
15772
|
+
const names = [];
|
|
15773
|
+
for (const token of tokens) {
|
|
15774
|
+
let name = token;
|
|
15775
|
+
if (token[0] === '.') {
|
|
15776
|
+
name = token.slice(1).toUpperCase();
|
|
15777
|
+
}
|
|
15778
|
+
else {
|
|
15779
|
+
const [type, subtype] = token.split('/');
|
|
15780
|
+
const core = subtype.split('+')[0];
|
|
15781
|
+
if (subtype === '*')
|
|
15782
|
+
name = type;
|
|
15783
|
+
else if (core === 'plain')
|
|
15784
|
+
name = 'text';
|
|
15785
|
+
else if (/^[a-z0-9]+$/.test(core))
|
|
15786
|
+
name = core.toUpperCase();
|
|
15787
|
+
}
|
|
15788
|
+
if (!names.includes(name))
|
|
15789
|
+
names.push(name);
|
|
15790
|
+
}
|
|
15791
|
+
return names;
|
|
15792
|
+
};
|
|
15793
|
+
/** "A", "A or B", "A, B, or C". */
|
|
15794
|
+
const orList = (items) => items.length <= 2
|
|
15795
|
+
? items.join(' or ')
|
|
15796
|
+
: `${items.slice(0, -1).join(', ')}, or ${items[items.length - 1]}`;
|
|
15797
|
+
/**
|
|
15798
|
+
* The `Dropzone` component is an area to drop files on or click to pick
|
|
15799
|
+
* them, which checks each file and lists the chosen ones with a way to remove
|
|
15800
|
+
* each.
|
|
15801
|
+
*
|
|
15802
|
+
* It's Bulma's boxed, full-width file widget with the real
|
|
15803
|
+
* `<input type="file">` lying over it, so the input is the one tab stop, the
|
|
15804
|
+
* click target and the drop target, and Enter and Space open the picker. The
|
|
15805
|
+
* input holds the chosen files: a pick, drop or paste that passes the checks
|
|
15806
|
+
* is written to it, so a form submits the files under `name`. It doesn't
|
|
15807
|
+
* upload anything.
|
|
14977
15808
|
*
|
|
14978
15809
|
* @function
|
|
14979
|
-
* @param {
|
|
14980
|
-
* @returns {JSX.Element} The rendered
|
|
15810
|
+
* @param {DropzoneProps} props - Props for the Dropzone component.
|
|
15811
|
+
* @returns {JSX.Element} The rendered dropzone.
|
|
15812
|
+
*
|
|
15813
|
+
* @example
|
|
15814
|
+
* <Dropzone
|
|
15815
|
+
* label="CV"
|
|
15816
|
+
* name="cv"
|
|
15817
|
+
* accept=".pdf,application/pdf"
|
|
15818
|
+
* maxSize={5_000_000}
|
|
15819
|
+
* hint="PDF up to 5 MB"
|
|
15820
|
+
* />
|
|
15821
|
+
*/
|
|
15822
|
+
const Dropzone = React.forwardRef(({
|
|
15823
|
+
// Field props
|
|
15824
|
+
label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, color, size, buttonLabel, icon, hint, maxSize, maxFiles, files: filesProp, onFilesChange, onReject, showFileList = true, allowPaste = true, preventDocumentDrop = true, removeLabel, rejectionMessage, addedMessage, removedMessage, className, accept, multiple, disabled: disabledProp, onChange, onClick, onDragEnter, onDragOver, onDragLeave, onDrop, onPaste, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
15825
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
15826
|
+
const controlled = filesProp !== undefined;
|
|
15827
|
+
const [ownFiles, setOwnFiles] = React.useState(noFiles);
|
|
15828
|
+
const files = filesProp ?? ownFiles;
|
|
15829
|
+
// A render the controlled list needs after a change, so that one the app
|
|
15830
|
+
// didn't take is undone.
|
|
15831
|
+
const [, rerender] = React.useReducer((n) => n + 1, 0);
|
|
15832
|
+
const [refusals, setRefusals] = React.useState(noRefusals);
|
|
15833
|
+
const [drag, setDrag] = React.useState(null);
|
|
15834
|
+
const [announcements, setAnnouncements] = React.useState(noAnnouncements);
|
|
15835
|
+
const inputRef = React.useRef(null);
|
|
15836
|
+
const listRef = React.useRef(null);
|
|
15837
|
+
// Set while Dropzone dispatches the input's own events, so its change
|
|
15838
|
+
// handler hands them on rather than reading them as a pick.
|
|
15839
|
+
const dispatchingRef = React.useRef(false);
|
|
15840
|
+
// The row a removal emptied, whose neighbour takes focus.
|
|
15841
|
+
const removedRowRef = React.useRef(null);
|
|
15842
|
+
const announcedRef = React.useRef(0);
|
|
15843
|
+
// What the input held when it was last clicked (Enter and Space click it
|
|
15844
|
+
// too), which a pick adds to: by the time the pick's change arrives, the
|
|
15845
|
+
// browser has replaced the input's files with the picked ones.
|
|
15846
|
+
const pickBaseRef = React.useRef(null);
|
|
15847
|
+
const setInputRef = React.useCallback((node) => {
|
|
15848
|
+
inputRef.current = node;
|
|
15849
|
+
if (typeof ref === 'function')
|
|
15850
|
+
ref(node);
|
|
15851
|
+
else if (ref) {
|
|
15852
|
+
ref.current =
|
|
15853
|
+
node;
|
|
15854
|
+
}
|
|
15855
|
+
}, [ref]);
|
|
15856
|
+
const formatNumber = useNumberFormat();
|
|
15857
|
+
const tokens = acceptTokens(accept);
|
|
15858
|
+
const sizeText = (bytes) => formatFileSize(bytes, formatNumber);
|
|
15859
|
+
const countText = (count) => formatNumber ? formatNumber(count) : String(count);
|
|
15860
|
+
// The files a pick, drop or paste builds on, and a removal takes one from.
|
|
15861
|
+
// Controlled, that's `files`. Uncontrolled, it's the input itself rather
|
|
15862
|
+
// than the list: a script can rewrite the input through the ref, emptying
|
|
15863
|
+
// it with `value = ''` or setting its `files`, and no event reports that,
|
|
15864
|
+
// so the list catches up only at the next change. Where there's no
|
|
15865
|
+
// DataTransfer to write the input with, the list is all there is.
|
|
15866
|
+
const current = () => controlled || typeof DataTransfer !== 'function'
|
|
15867
|
+
? files
|
|
15868
|
+
: Array.from(inputRef.current.files);
|
|
15869
|
+
const messageFor = (refusal) => {
|
|
15870
|
+
const { file, code } = refusal;
|
|
15871
|
+
if (rejectionMessage)
|
|
15872
|
+
return rejectionMessage({ file, code });
|
|
15873
|
+
if (code === 'file-too-large') {
|
|
15874
|
+
return `${file.name}: larger than ${sizeText(maxSize)}`;
|
|
15875
|
+
}
|
|
15876
|
+
if (code === 'too-many-files') {
|
|
15877
|
+
const most = multiple ? maxFiles : 1;
|
|
15878
|
+
return `${file.name}: only ${most === 1 ? 'one file' : `${countText(most)} files`}`;
|
|
15879
|
+
}
|
|
15880
|
+
return refusal.folder
|
|
15881
|
+
? `${file.name}: folders can't be added`
|
|
15882
|
+
: `${file.name}: only ${orList(typeNames(tokens))} files`;
|
|
15883
|
+
};
|
|
15884
|
+
const announce = (texts) => {
|
|
15885
|
+
if (texts.length === 0)
|
|
15886
|
+
return;
|
|
15887
|
+
const batch = ++announcedRef.current;
|
|
15888
|
+
setAnnouncements(texts.map((text, i) => ({ id: `${batch}-${i}`, text })));
|
|
15889
|
+
};
|
|
15890
|
+
/**
|
|
15891
|
+
* Makes `next` the chosen files: writes them to the input, and when they
|
|
15892
|
+
* differ from the list, dispatches the input's `input` and `change` and
|
|
15893
|
+
* reports the new list.
|
|
15894
|
+
*/
|
|
15895
|
+
const commit = (next) => {
|
|
15896
|
+
pickBaseRef.current = null;
|
|
15897
|
+
const input = inputRef.current;
|
|
15898
|
+
if (!sameFiles(input.files, next))
|
|
15899
|
+
writeFiles(input, next);
|
|
15900
|
+
if (sameFiles(files, next))
|
|
15901
|
+
return;
|
|
15902
|
+
if (controlled)
|
|
15903
|
+
rerender();
|
|
15904
|
+
else
|
|
15905
|
+
setOwnFiles(next);
|
|
15906
|
+
onFilesChange?.(next);
|
|
15907
|
+
dispatchingRef.current = true;
|
|
15908
|
+
try {
|
|
15909
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
15910
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
15911
|
+
}
|
|
15912
|
+
finally {
|
|
15913
|
+
dispatchingRef.current = false;
|
|
15914
|
+
}
|
|
15915
|
+
};
|
|
15916
|
+
/** Checks the files a pick, drop or paste brings, and takes those that pass. */
|
|
15917
|
+
const take = (incoming, base = current()) => {
|
|
15918
|
+
const kept = multiple ? base : noFiles;
|
|
15919
|
+
const room = multiple ? (maxFiles ?? Infinity) - kept.length : 1;
|
|
15920
|
+
const added = [];
|
|
15921
|
+
const refused = [];
|
|
15922
|
+
for (const { file, folder } of incoming) {
|
|
15923
|
+
const code = folder || !accepts(tokens, file.name, file.type)
|
|
15924
|
+
? 'file-invalid-type'
|
|
15925
|
+
: maxSize !== undefined && file.size > maxSize
|
|
15926
|
+
? 'file-too-large'
|
|
15927
|
+
: null;
|
|
15928
|
+
if (code) {
|
|
15929
|
+
refused.push({ file, code, folder });
|
|
15930
|
+
}
|
|
15931
|
+
else if (multiple &&
|
|
15932
|
+
[...kept, ...added].some(f => sameFile(f, file))) ;
|
|
15933
|
+
else if (added.length >= room) {
|
|
15934
|
+
refused.push({ file, code: 'too-many-files', folder });
|
|
15935
|
+
}
|
|
15936
|
+
else {
|
|
15937
|
+
added.push(file);
|
|
15938
|
+
}
|
|
15939
|
+
}
|
|
15940
|
+
setRefusals(refused.length > 0 ? refused : noRefusals);
|
|
15941
|
+
if (refused.length > 0) {
|
|
15942
|
+
onReject?.(refused.map(({ file, code }) => ({ file, code })));
|
|
15943
|
+
}
|
|
15944
|
+
commit(added.length > 0 ? [...kept, ...added] : base);
|
|
15945
|
+
const addedText = added.length === 0
|
|
15946
|
+
? []
|
|
15947
|
+
: [
|
|
15948
|
+
addedMessage
|
|
15949
|
+
? addedMessage(added)
|
|
15950
|
+
: added.length === 1
|
|
15951
|
+
? `${added[0].name} added`
|
|
15952
|
+
: `${countText(added.length)} files added`,
|
|
15953
|
+
];
|
|
15954
|
+
announce([...addedText, ...refused.map(messageFor)]);
|
|
15955
|
+
};
|
|
15956
|
+
const remove = (index) => {
|
|
15957
|
+
const file = files[index];
|
|
15958
|
+
removedRowRef.current = index;
|
|
15959
|
+
commit(current().filter(held => !sameFile(held, file)));
|
|
15960
|
+
announce([
|
|
15961
|
+
removedMessage ? removedMessage(file) : `${file.name} removed`,
|
|
15962
|
+
]);
|
|
15963
|
+
};
|
|
15964
|
+
const handleChange = (event) => {
|
|
15965
|
+
if (dispatchingRef.current) {
|
|
15966
|
+
onChange?.(event);
|
|
15967
|
+
return;
|
|
15968
|
+
}
|
|
15969
|
+
// A pick: the browser has put the picked files in the input. They're
|
|
15970
|
+
// added to what it held when it was clicked, or, for a picker opened
|
|
15971
|
+
// with showPicker(), which clicks nothing, to the list. With none (a
|
|
15972
|
+
// cancelled pick, in a browser that clears it), that stays.
|
|
15973
|
+
const base = pickBaseRef.current ?? files;
|
|
15974
|
+
const picked = Array.from(event.target.files ?? []);
|
|
15975
|
+
if (picked.length === 0)
|
|
15976
|
+
commit(base);
|
|
15977
|
+
else
|
|
15978
|
+
take(picked.map(file => ({ file, folder: false })), base);
|
|
15979
|
+
};
|
|
15980
|
+
const handleClick = (event) => {
|
|
15981
|
+
onClick?.(event);
|
|
15982
|
+
pickBaseRef.current = current();
|
|
15983
|
+
};
|
|
15984
|
+
// While files are dragged over the input it's cancelled, which makes the
|
|
15985
|
+
// input the drop target and keeps the browser's own drop from running.
|
|
15986
|
+
// Disabled, it refuses the drop, so the browser doesn't open the file.
|
|
15987
|
+
const handleDragOver = (event) => {
|
|
15988
|
+
(event.type === 'dragenter' ? onDragEnter : onDragOver)?.(event);
|
|
15989
|
+
if (!carriesFiles(event.dataTransfer))
|
|
15990
|
+
return;
|
|
15991
|
+
event.preventDefault();
|
|
15992
|
+
if (disabled) {
|
|
15993
|
+
event.dataTransfer.dropEffect = 'none';
|
|
15994
|
+
return;
|
|
15995
|
+
}
|
|
15996
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
15997
|
+
// Only the dragged files' types can be read, so a file is known to be
|
|
15998
|
+
// refused only when `accept` names no extension and its type is known.
|
|
15999
|
+
const room = multiple ? (maxFiles ?? Infinity) - current().length : 1;
|
|
16000
|
+
const types = Array.from(event.dataTransfer.items ?? [])
|
|
16001
|
+
.filter(item => item.kind === 'file')
|
|
16002
|
+
.map(item => item.type);
|
|
16003
|
+
const refused = room <= 0 ||
|
|
16004
|
+
(types.length > 0 &&
|
|
16005
|
+
tokens.length > 0 &&
|
|
16006
|
+
!tokens.some(token => token[0] === '.') &&
|
|
16007
|
+
types.every(type => type !== '' && !accepts(tokens, '', type)));
|
|
16008
|
+
setDrag(refused ? 'reject' : 'over');
|
|
16009
|
+
};
|
|
16010
|
+
const handleDragLeave = (event) => {
|
|
16011
|
+
onDragLeave?.(event);
|
|
16012
|
+
setDrag(null);
|
|
16013
|
+
};
|
|
16014
|
+
const handleDrop = (event) => {
|
|
16015
|
+
onDrop?.(event);
|
|
16016
|
+
setDrag(null);
|
|
16017
|
+
if (!carriesFiles(event.dataTransfer))
|
|
16018
|
+
return;
|
|
16019
|
+
event.preventDefault();
|
|
16020
|
+
if (!disabled)
|
|
16021
|
+
take(incomingFrom(event.dataTransfer));
|
|
16022
|
+
};
|
|
16023
|
+
const handlePaste = (event) => {
|
|
16024
|
+
onPaste?.(event);
|
|
16025
|
+
if (!allowPaste || disabled)
|
|
16026
|
+
return;
|
|
16027
|
+
const incoming = incomingFrom(event.clipboardData);
|
|
16028
|
+
if (incoming.length === 0)
|
|
16029
|
+
return;
|
|
16030
|
+
event.preventDefault();
|
|
16031
|
+
take(incoming);
|
|
16032
|
+
};
|
|
16033
|
+
// A controlled list: the input follows `files`, and a change the app
|
|
16034
|
+
// didn't take is undone.
|
|
16035
|
+
React.useEffect(() => {
|
|
16036
|
+
const input = inputRef.current;
|
|
16037
|
+
if (controlled && input && !sameFiles(input.files, files)) {
|
|
16038
|
+
writeFiles(input, files);
|
|
16039
|
+
}
|
|
16040
|
+
});
|
|
16041
|
+
// After a removal, focus goes to the remove button that took the removed
|
|
16042
|
+
// one's place, else the one before, else the input.
|
|
16043
|
+
React.useEffect(() => {
|
|
16044
|
+
const row = removedRowRef.current;
|
|
16045
|
+
if (row === null)
|
|
16046
|
+
return;
|
|
16047
|
+
removedRowRef.current = null;
|
|
16048
|
+
const buttons = listRef.current?.querySelectorAll('button') ?? [];
|
|
16049
|
+
const next = buttons[Math.min(row, buttons.length - 1)];
|
|
16050
|
+
(next ?? inputRef.current)?.focus();
|
|
16051
|
+
});
|
|
16052
|
+
// A reset of the input's form empties the input, so the list and the
|
|
16053
|
+
// refusals go too. The reset event fires before the form resets, and a
|
|
16054
|
+
// listener after this one can still cancel it, so the list is cleared a
|
|
16055
|
+
// task later, and only if the reset went ahead.
|
|
16056
|
+
const latestRef = React.useRef({ files, controlled, onFilesChange });
|
|
16057
|
+
React.useEffect(() => {
|
|
16058
|
+
latestRef.current = { files, controlled, onFilesChange };
|
|
16059
|
+
});
|
|
16060
|
+
const formAttribute = props.form;
|
|
16061
|
+
React.useEffect(() => {
|
|
16062
|
+
const form = inputRef.current?.form;
|
|
16063
|
+
if (!form)
|
|
16064
|
+
return undefined;
|
|
16065
|
+
let timer;
|
|
16066
|
+
const onReset = (event) => {
|
|
16067
|
+
timer = setTimeout(() => {
|
|
16068
|
+
if (event.defaultPrevented)
|
|
16069
|
+
return;
|
|
16070
|
+
const latest = latestRef.current;
|
|
16071
|
+
pickBaseRef.current = null;
|
|
16072
|
+
setRefusals(noRefusals);
|
|
16073
|
+
if (latest.files.length === 0)
|
|
16074
|
+
return;
|
|
16075
|
+
if (latest.controlled)
|
|
16076
|
+
rerender();
|
|
16077
|
+
else
|
|
16078
|
+
setOwnFiles(noFiles);
|
|
16079
|
+
latest.onFilesChange?.(noFiles);
|
|
16080
|
+
});
|
|
16081
|
+
};
|
|
16082
|
+
form.addEventListener('reset', onReset);
|
|
16083
|
+
return () => {
|
|
16084
|
+
form.removeEventListener('reset', onReset);
|
|
16085
|
+
clearTimeout(timer);
|
|
16086
|
+
};
|
|
16087
|
+
}, [formAttribute]);
|
|
16088
|
+
// A file dropped just outside the zone would open in the tab. The guard
|
|
16089
|
+
// cancels a file drag anywhere nothing else takes it, leaving alone
|
|
16090
|
+
// enabled file inputs and editable fields, which take drops of their own,
|
|
16091
|
+
// and drops something else already handled. It reads the event's real
|
|
16092
|
+
// target, so a file input inside a shadow root is left alone too.
|
|
16093
|
+
React.useEffect(() => {
|
|
16094
|
+
if (!preventDocumentDrop)
|
|
16095
|
+
return undefined;
|
|
16096
|
+
const guard = (event) => {
|
|
16097
|
+
if (event.defaultPrevented || !carriesFiles(event.dataTransfer)) {
|
|
16098
|
+
return;
|
|
16099
|
+
}
|
|
16100
|
+
const target = event.composedPath()[0];
|
|
16101
|
+
if (target instanceof Element &&
|
|
16102
|
+
target.closest('input[type="file"]:not(:disabled), textarea, [contenteditable]:not([contenteditable="false"])')) {
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
16105
|
+
event.preventDefault();
|
|
16106
|
+
if (event.type === 'dragover')
|
|
16107
|
+
event.dataTransfer.dropEffect = 'none';
|
|
16108
|
+
};
|
|
16109
|
+
document.addEventListener('dragover', guard);
|
|
16110
|
+
document.addEventListener('drop', guard);
|
|
16111
|
+
return () => {
|
|
16112
|
+
document.removeEventListener('dragover', guard);
|
|
16113
|
+
document.removeEventListener('drop', guard);
|
|
16114
|
+
};
|
|
16115
|
+
}, [preventDocumentDrop]);
|
|
16116
|
+
const warnsMaxFiles = maxFiles !== undefined && !multiple;
|
|
16117
|
+
React.useEffect(() => {
|
|
16118
|
+
if (warnsMaxFiles) {
|
|
16119
|
+
warnOnce('Dropzone:maxFiles-without-multiple', '[bestax-bulma] <Dropzone maxFiles> takes effect only with ' +
|
|
16120
|
+
'multiple. Without it, each new file replaces the one chosen.');
|
|
16121
|
+
}
|
|
16122
|
+
}, [warnsMaxFiles]);
|
|
16123
|
+
const insideField = useInsideField();
|
|
16124
|
+
const insideControl = useInsideControl();
|
|
16125
|
+
const ownField = rendersOwnField('Dropzone', {
|
|
16126
|
+
insideField,
|
|
16127
|
+
insideControl,
|
|
16128
|
+
label,
|
|
16129
|
+
message,
|
|
16130
|
+
horizontal,
|
|
16131
|
+
fieldClassName,
|
|
16132
|
+
});
|
|
16133
|
+
const { controlId, fieldLabelProps } = useAutoLabelId({
|
|
16134
|
+
label,
|
|
16135
|
+
id: props.id,
|
|
16136
|
+
labelProps,
|
|
16137
|
+
rendersLabel: ownField,
|
|
16138
|
+
});
|
|
16139
|
+
const { classPrefix } = useConfig();
|
|
16140
|
+
const pcn = (...args) => prefixedClassNames(classPrefix, ...args);
|
|
16141
|
+
// The input takes `controlId`, so `id` stays out of the spread, where an
|
|
16142
|
+
// undefined `id` key would wipe the id the label points at.
|
|
16143
|
+
const { id: _id, ...inputProps } = props;
|
|
16144
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(inputProps);
|
|
16145
|
+
const baseId = React.useId();
|
|
16146
|
+
const hintId = `${baseId}-hint`;
|
|
16147
|
+
const refusalsId = `${baseId}-refusals`;
|
|
16148
|
+
const messageId = `${baseId}-message`;
|
|
16149
|
+
const describedBy = [
|
|
16150
|
+
ariaDescribedBy,
|
|
16151
|
+
hint ? hintId : undefined,
|
|
16152
|
+
refusals.length > 0 ? refusalsId : undefined,
|
|
16153
|
+
message ? messageId : undefined,
|
|
16154
|
+
]
|
|
16155
|
+
.filter(Boolean)
|
|
16156
|
+
.join(' ') || undefined;
|
|
16157
|
+
const rootClass = usePrefixedClassNames('dropzone', {
|
|
16158
|
+
'is-dragover': drag === 'over',
|
|
16159
|
+
'is-drag-reject': drag === 'reject',
|
|
16160
|
+
'is-disabled': disabled,
|
|
16161
|
+
});
|
|
16162
|
+
const zoneClass = usePrefixedClassNames('file', 'is-boxed', 'is-fullwidth', {
|
|
16163
|
+
[`is-${color}`]: !!color,
|
|
16164
|
+
[`is-${size}`]: !!size,
|
|
16165
|
+
});
|
|
16166
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
16167
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
16168
|
+
});
|
|
16169
|
+
// Each row's key, from the file's details, which the list keeps unique.
|
|
16170
|
+
const seen = new Map();
|
|
16171
|
+
const keyOf = (file) => {
|
|
16172
|
+
const key = `${file.name}:${file.size}:${file.lastModified}`;
|
|
16173
|
+
const count = seen.get(key) ?? 0;
|
|
16174
|
+
seen.set(key, count + 1);
|
|
16175
|
+
return count === 0 ? key : `${key}:${count}`;
|
|
16176
|
+
};
|
|
16177
|
+
const dropzoneElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), children: [jsxRuntime.jsx("div", { className: zoneClass, children: jsxRuntime.jsxs("label", { className: pcn('file-label'), children: [jsxRuntime.jsx("input", { ...rest, ref: setInputRef, className: pcn('file-input'), type: "file", id: controlId, accept: accept, multiple: multiple, disabled: disabled, "aria-describedby": describedBy, onChange: handleChange, onClick: handleClick, onDragEnter: handleDragOver, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, onPaste: handlePaste }), jsxRuntime.jsxs("span", { className: pcn('file-cta'), children: [icon && jsxRuntime.jsx("span", { className: pcn('file-icon'), children: icon }), jsxRuntime.jsx("span", { className: pcn('file-label'), children: buttonLabel ||
|
|
16178
|
+
(multiple ? 'Choose or drop files' : 'Choose or drop a file') })] })] }) }), hint && (jsxRuntime.jsx("p", { id: hintId, className: pcn('help', 'dropzone-hint'), children: hint })), showFileList && files.length > 0 && (jsxRuntime.jsx("ul", { ref: listRef, className: pcn('dropzone-list'), children: files.map((file, index) => (jsxRuntime.jsxs("li", { className: pcn('dropzone-item'), children: [jsxRuntime.jsx("span", { className: pcn('dropzone-name'), children: file.name }), jsxRuntime.jsx("span", { className: pcn('dropzone-size'), children: sizeText(file.size) }), !disabled && (jsxRuntime.jsx(Delete, { size: "small", ariaLabel: removeLabel ? removeLabel(file) : `Remove ${file.name}`, onClick: () => remove(index) }))] }, keyOf(file)))) })), refusals.length > 0 && (jsxRuntime.jsx("div", { id: refusalsId, className: pcn('dropzone-rejections'), children: refusals.map((refusal, index) => (jsxRuntime.jsx("p", { className: pcn('help', 'is-danger'), children: messageFor(refusal) }, index))) })), jsxRuntime.jsx(StatusRegion, { items: announcements, describe: describeAnnouncement })] }));
|
|
16179
|
+
const messageEl = message ? (jsxRuntime.jsx("p", { id: messageId, className: helpClass, children: message })) : null;
|
|
16180
|
+
if (ownField) {
|
|
16181
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [dropzoneElement, messageEl] }));
|
|
16182
|
+
}
|
|
16183
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [dropzoneElement, messageEl] }));
|
|
16184
|
+
});
|
|
16185
|
+
Dropzone.displayName = 'Dropzone';
|
|
16186
|
+
|
|
16187
|
+
// What an unlabeled Field passes on: no label names anything inside.
|
|
16188
|
+
const NO_LABEL = {};
|
|
16189
|
+
/**
|
|
16190
|
+
* The `Fieldset` component groups a section of a form in a native
|
|
16191
|
+
* `<fieldset>`, named by its `legend`, which a `disabled` switches off as a
|
|
16192
|
+
* whole.
|
|
16193
|
+
*
|
|
16194
|
+
* It starts its own Field scope: inside it, nothing reads as inside a
|
|
16195
|
+
* `Field` or `Control`, so the convenience inputs render their own `Field`,
|
|
16196
|
+
* and a labeled `Field` around the Fieldset names nothing in it.
|
|
16197
|
+
*
|
|
16198
|
+
* @function
|
|
16199
|
+
* @param {FieldsetProps} props - Props for the Fieldset component.
|
|
16200
|
+
* @returns {JSX.Element} The rendered fieldset.
|
|
14981
16201
|
*
|
|
14982
16202
|
* @example
|
|
14983
16203
|
* <Fieldset legend="Shipping address" message="We only ship within the EU.">
|
|
@@ -16993,11 +18213,11 @@ const validComboboxSizes = ['small', 'medium', 'large'];
|
|
|
16993
18213
|
// typing.
|
|
16994
18214
|
const typedNoMatches = [{ id: 'empty' }];
|
|
16995
18215
|
const nothingToAnnounce = [];
|
|
16996
|
-
const noop = () => { };
|
|
16997
|
-
const registerNothing = () => noop;
|
|
18216
|
+
const noop$1 = () => { };
|
|
18217
|
+
const registerNothing = () => noop$1;
|
|
16998
18218
|
// What a part rendered outside a Combobox reads: closed, with nothing to
|
|
16999
18219
|
// report to, so it renders its markup and does nothing.
|
|
17000
|
-
const standalone = {
|
|
18220
|
+
const standalone$1 = {
|
|
17001
18221
|
engine: {
|
|
17002
18222
|
open: false,
|
|
17003
18223
|
expanded: false,
|
|
@@ -17010,22 +18230,22 @@ const standalone = {
|
|
|
17010
18230
|
listboxRef: { current: null },
|
|
17011
18231
|
registerPopup: registerNothing,
|
|
17012
18232
|
registerItem: registerNothing,
|
|
17013
|
-
onKeyDown: noop,
|
|
17014
|
-
onChange: noop,
|
|
17015
|
-
onClick: noop,
|
|
17016
|
-
onBlur: noop,
|
|
17017
|
-
hover: noop,
|
|
17018
|
-
pick: noop,
|
|
17019
|
-
keepFocus: noop,
|
|
17020
|
-
markInside: noop,
|
|
18233
|
+
onKeyDown: noop$1,
|
|
18234
|
+
onChange: noop$1,
|
|
18235
|
+
onClick: noop$1,
|
|
18236
|
+
onBlur: noop$1,
|
|
18237
|
+
hover: noop$1,
|
|
18238
|
+
pick: noop$1,
|
|
18239
|
+
keepFocus: noop$1,
|
|
18240
|
+
markInside: noop$1,
|
|
17021
18241
|
},
|
|
17022
18242
|
inputId: undefined,
|
|
17023
|
-
reportInputId: noop,
|
|
18243
|
+
reportInputId: noop$1,
|
|
17024
18244
|
labelId: undefined,
|
|
17025
18245
|
ownLabelId: undefined,
|
|
17026
|
-
reportLoading: noop,
|
|
18246
|
+
reportLoading: noop$1,
|
|
17027
18247
|
reportEmpty: registerNothing,
|
|
17028
|
-
onFieldChange: noop,
|
|
18248
|
+
onFieldChange: noop$1,
|
|
17029
18249
|
reportPanel: registerNothing,
|
|
17030
18250
|
placement: { position: 'bottom-left' },
|
|
17031
18251
|
multiple: false,
|
|
@@ -17033,9 +18253,9 @@ const standalone = {
|
|
|
17033
18253
|
appendToBody: false,
|
|
17034
18254
|
};
|
|
17035
18255
|
const ComboboxContext = React.createContext(null);
|
|
17036
|
-
const useCombobox = () => React.useContext(ComboboxContext) ?? standalone;
|
|
18256
|
+
const useCombobox = () => React.useContext(ComboboxContext) ?? standalone$1;
|
|
17037
18257
|
/** Hands a node to a forwarded ref and to one of the component's own. */
|
|
17038
|
-
function setRefs(node, own, forwarded) {
|
|
18258
|
+
function setRefs$1(node, own, forwarded) {
|
|
17039
18259
|
own.current = node;
|
|
17040
18260
|
if (typeof forwarded === 'function')
|
|
17041
18261
|
forwarded(node);
|
|
@@ -17167,7 +18387,7 @@ const ComboboxParts = React.forwardRef(({ children, below, onSettle, open, defau
|
|
|
17167
18387
|
appendToBody,
|
|
17168
18388
|
};
|
|
17169
18389
|
const { rootRef } = engine;
|
|
17170
|
-
const rootRefs = React.useCallback((node) => setRefs(node, rootRef, ref), [rootRef, ref]);
|
|
18390
|
+
const rootRefs = React.useCallback((node) => setRefs$1(node, rootRef, ref), [rootRef, ref]);
|
|
17171
18391
|
const comboboxElement = (jsxRuntime.jsxs("div", { className: classNames(rootClass, bulmaHelperClasses, className), ...rest, ref: rootRefs, children: [jsxRuntime.jsx(ComboboxContext.Provider, { value: context, children: children }), jsxRuntime.jsx(StatusRegion, { items: announced, describe: describeEmpty })] }));
|
|
17172
18392
|
const fieldElement = below === undefined ? (comboboxElement) : (jsxRuntime.jsxs("div", { children: [comboboxElement, jsxRuntime.jsx("div", { onPointerDownCapture: engine.markInside, onMouseDown: engine.keepFocus, children: below })] }));
|
|
17173
18393
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
@@ -17200,7 +18420,7 @@ const ComboboxInput = React.forwardRef(({ id, isLoading, controlSize, onKeyDown,
|
|
|
17200
18420
|
return () => reportLoading(false);
|
|
17201
18421
|
}, [reportLoading, isLoading]);
|
|
17202
18422
|
const { inputRef } = engine;
|
|
17203
|
-
const inputRefs = React.useCallback((node) => setRefs(node, inputRef, ref), [inputRef, ref]);
|
|
18423
|
+
const inputRefs = React.useCallback((node) => setRefs$1(node, inputRef, ref), [inputRef, ref]);
|
|
17204
18424
|
const { expanded, highlighted } = engine;
|
|
17205
18425
|
// An `id` of the field's own reaches the label from a layout effect, so
|
|
17206
18426
|
// in server markup, and the first render, the label's `for` still names
|
|
@@ -17804,158 +19024,1162 @@ label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName,
|
|
|
17804
19024
|
const handleKeyDown = React.useCallback((e) => {
|
|
17805
19025
|
if (disabled || readonly || isComposing(e))
|
|
17806
19026
|
return;
|
|
17807
|
-
// Handle confirm keys
|
|
17808
|
-
if (confirmKeys.includes(e.key)) {
|
|
17809
|
-
e.preventDefault();
|
|
17810
|
-
if (highlightedIndex >= 0 && filteredData[highlightedIndex]) {
|
|
17811
|
-
addTags([filteredData[highlightedIndex]]);
|
|
17812
|
-
}
|
|
17813
|
-
else if (inputValue.trim()) {
|
|
17814
|
-
addTags([inputValue]);
|
|
17815
|
-
}
|
|
19027
|
+
// Handle confirm keys
|
|
19028
|
+
if (confirmKeys.includes(e.key)) {
|
|
19029
|
+
e.preventDefault();
|
|
19030
|
+
if (highlightedIndex >= 0 && filteredData[highlightedIndex]) {
|
|
19031
|
+
addTags([filteredData[highlightedIndex]]);
|
|
19032
|
+
}
|
|
19033
|
+
else if (inputValue.trim()) {
|
|
19034
|
+
addTags([inputValue]);
|
|
19035
|
+
}
|
|
19036
|
+
return;
|
|
19037
|
+
}
|
|
19038
|
+
switch (e.key) {
|
|
19039
|
+
case 'ArrowDown':
|
|
19040
|
+
e.preventDefault();
|
|
19041
|
+
if (!isActive && data.length > 0) {
|
|
19042
|
+
setIsActive(true);
|
|
19043
|
+
}
|
|
19044
|
+
else {
|
|
19045
|
+
setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
|
|
19046
|
+
}
|
|
19047
|
+
break;
|
|
19048
|
+
case 'ArrowUp':
|
|
19049
|
+
e.preventDefault();
|
|
19050
|
+
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
|
|
19051
|
+
break;
|
|
19052
|
+
case 'Backspace':
|
|
19053
|
+
if (removeOnKeys && !inputValue && tags.length > 0) {
|
|
19054
|
+
e.preventDefault();
|
|
19055
|
+
removeTag(tags.length - 1);
|
|
19056
|
+
}
|
|
19057
|
+
break;
|
|
19058
|
+
case 'Escape':
|
|
19059
|
+
// Closing the suggestions claims the key, so a Modal around the
|
|
19060
|
+
// field stays open. With none showing, the key goes on to it.
|
|
19061
|
+
if (listOpen)
|
|
19062
|
+
e.preventDefault();
|
|
19063
|
+
setIsActive(false);
|
|
19064
|
+
setHighlightedIndex(-1);
|
|
19065
|
+
break;
|
|
19066
|
+
}
|
|
19067
|
+
}, [
|
|
19068
|
+
disabled,
|
|
19069
|
+
readonly,
|
|
19070
|
+
confirmKeys,
|
|
19071
|
+
highlightedIndex,
|
|
19072
|
+
filteredData,
|
|
19073
|
+
addTags,
|
|
19074
|
+
inputValue,
|
|
19075
|
+
isActive,
|
|
19076
|
+
listOpen,
|
|
19077
|
+
data.length,
|
|
19078
|
+
removeOnKeys,
|
|
19079
|
+
tags.length,
|
|
19080
|
+
removeTag,
|
|
19081
|
+
]);
|
|
19082
|
+
// Combined ref
|
|
19083
|
+
const combinedRef = React.useCallback((node) => {
|
|
19084
|
+
inputRef.current =
|
|
19085
|
+
node;
|
|
19086
|
+
if (typeof ref === 'function') {
|
|
19087
|
+
ref(node);
|
|
19088
|
+
}
|
|
19089
|
+
else if (ref) {
|
|
19090
|
+
ref.current =
|
|
19091
|
+
node;
|
|
19092
|
+
}
|
|
19093
|
+
}, [ref]);
|
|
19094
|
+
// Focus input when container is clicked
|
|
19095
|
+
const handleContainerClick = React.useCallback(() => {
|
|
19096
|
+
inputRef.current?.focus();
|
|
19097
|
+
}, []);
|
|
19098
|
+
// Generate classes
|
|
19099
|
+
const taginputClasses = pcn('taginput', {
|
|
19100
|
+
'is-active': isActive,
|
|
19101
|
+
[`is-${size}`]: !!size,
|
|
19102
|
+
[`is-${color}`]: !!color,
|
|
19103
|
+
'is-disabled': disabled,
|
|
19104
|
+
'is-rounded': rounded,
|
|
19105
|
+
'is-ellipsis': ellipsis,
|
|
19106
|
+
});
|
|
19107
|
+
const containerClasses = pcn('taginput-container', {
|
|
19108
|
+
'is-attached': attached,
|
|
19109
|
+
'has-icons-left': !!icon,
|
|
19110
|
+
});
|
|
19111
|
+
const inputClasses = pcn('input', {
|
|
19112
|
+
[`is-${size}`]: !!size,
|
|
19113
|
+
});
|
|
19114
|
+
const tagClasses = pcn('tag', {
|
|
19115
|
+
[`is-${tagColor}`]: !!tagColor,
|
|
19116
|
+
[`is-${size}`]: !!size,
|
|
19117
|
+
});
|
|
19118
|
+
const combinedClasses = classNames(taginputClasses, bulmaHelperClasses, className);
|
|
19119
|
+
// Counter text
|
|
19120
|
+
const showCounter = hasCounter && (maxTags !== undefined || maxlength !== undefined);
|
|
19121
|
+
const write = formatNumber ?? String;
|
|
19122
|
+
const counterText = maxTags !== undefined
|
|
19123
|
+
? `${write(tags.length)} / ${write(maxTags)}`
|
|
19124
|
+
: maxlength !== undefined
|
|
19125
|
+
? `${write(inputValue.length)} / ${write(maxlength)}`
|
|
19126
|
+
: '';
|
|
19127
|
+
const counterClasses = pcn('help', 'counter');
|
|
19128
|
+
const tagsClasses = pcn('tags');
|
|
19129
|
+
const dropdownMenuClasses = pcn('dropdown-menu', 'is-active');
|
|
19130
|
+
const dropdownContentClasses = pcn('dropdown-content');
|
|
19131
|
+
const iconRightClass = pcn('icon', 'is-right');
|
|
19132
|
+
const loaderClass = pcn('loader', 'is-loading');
|
|
19133
|
+
const helpClass = usePrefixedClassNames('help', {
|
|
19134
|
+
[`is-${messageColor}`]: !!messageColor,
|
|
19135
|
+
});
|
|
19136
|
+
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
19137
|
+
// The combobox controls its listbox and points at the highlighted option,
|
|
19138
|
+
// the way Autocomplete's does. The listbox takes its name from the label
|
|
19139
|
+
// that names the combobox, the one this renders or a surrounding Field's,
|
|
19140
|
+
// and keeps a fallback name when no label names it or the one that does
|
|
19141
|
+
// has no id to point at. While no suggestion matches there's no listbox,
|
|
19142
|
+
// so the combobox is collapsed and points at nothing. It skips a
|
|
19143
|
+
// highlighted suggestion that's blank once trimmed, since that one can't
|
|
19144
|
+
// become a tag, and the option it points at is the one marked selected.
|
|
19145
|
+
const optionId = (index) => `${listboxId}-${index}`;
|
|
19146
|
+
const activeOptionId = listOpen && filteredData[highlightedIndex]?.trim()
|
|
19147
|
+
? optionId(highlightedIndex)
|
|
19148
|
+
: undefined;
|
|
19149
|
+
const comboboxProps = {
|
|
19150
|
+
role: 'combobox',
|
|
19151
|
+
'aria-expanded': listOpen,
|
|
19152
|
+
'aria-controls': listOpen ? listboxId : undefined,
|
|
19153
|
+
'aria-activedescendant': activeOptionId,
|
|
19154
|
+
'aria-haspopup': 'listbox',
|
|
19155
|
+
'aria-autocomplete': 'list',
|
|
19156
|
+
};
|
|
19157
|
+
const taginputElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, onClick: handleContainerClick, ...rest, children: [jsxRuntime.jsxs("div", { className: containerClasses, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: resolvedLibrary, variant: iconVariant, features: iconFeatures, containerClassName: pcn('icon', 'is-left') })), jsxRuntime.jsxs("div", { className: tagsClasses, children: [tags.map((tag, index) => {
|
|
19158
|
+
const displayVal = getDisplayValue(tag);
|
|
19159
|
+
return (jsxRuntime.jsxs("span", { className: tagClasses, children: [tagTemplate ? (tagTemplate(tag)) : ellipsis ? (jsxRuntime.jsx("span", { title: displayVal, children: displayVal })) : (displayVal), closable && !disabled && !readonly && (jsxRuntime.jsx("button", { type: "button", className: pcn('delete', 'is-small'), onClick: e => {
|
|
19160
|
+
e.stopPropagation();
|
|
19161
|
+
removeTag(index);
|
|
19162
|
+
}, "aria-label": ariaCloseLabel || `Remove ${displayVal}` }))] }, index));
|
|
19163
|
+
}), jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: tags.length === 0 ? placeholder : undefined, disabled: disabled,
|
|
19164
|
+
// Read-only at the tag limit rather than gone, so focus stays
|
|
19165
|
+
// put and a label keeps naming it, and Backspace still makes
|
|
19166
|
+
// room (#1103).
|
|
19167
|
+
readOnly: readonly || isMaxReached, maxLength: maxlength, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, onPaste: handlePaste, ...(hasSuggestions && comboboxProps), "aria-label": labelled ? undefined : 'Add tag', "aria-describedby": showCounter ? counterId : undefined })] }), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), listOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: dropdownContentClasses, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', children: filteredData.map((item, index) => (jsxRuntime.jsx("a", { id: optionId(index), className: pcn('dropdown-item', {
|
|
19168
|
+
'is-active': index === highlightedIndex,
|
|
19169
|
+
}), onClick: () => addTags([item]), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": optionId(index) === activeOptionId, children: item }, index))) }) })), showCounter && (jsxRuntime.jsx("small", { id: counterId, className: counterClasses, children: counterText })), name &&
|
|
19170
|
+
tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
19171
|
+
if (ownField) {
|
|
19172
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [taginputElement, messageEl] }));
|
|
19173
|
+
}
|
|
19174
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [taginputElement, messageEl] }));
|
|
19175
|
+
});
|
|
19176
|
+
Taginput.displayName = 'Taginput';
|
|
19177
|
+
/** @deprecated Use `Taginput` — same component. */
|
|
19178
|
+
const TagInput = Taginput;
|
|
19179
|
+
|
|
19180
|
+
// INTERNAL: deliberately not exported from src/index.ts. Reading, converting
|
|
19181
|
+
// and writing the CSS colors the color picker takes and gives.
|
|
19182
|
+
//
|
|
19183
|
+
// The picker works in HSV (hue, saturation and value, which the square
|
|
19184
|
+
// calls brightness), since its square is saturation by brightness. Every
|
|
19185
|
+
// color it reads becomes HSV, and every value it writes comes out of HSV in
|
|
19186
|
+
// the format asked for.
|
|
19187
|
+
const clamp$3 = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
19188
|
+
/** Rounds to a number of decimal places, so `0.1 + 0.2` writes as `0.3`. */
|
|
19189
|
+
const round = (value, places) => {
|
|
19190
|
+
const factor = 10 ** places;
|
|
19191
|
+
return Math.round(value * factor) / factor;
|
|
19192
|
+
};
|
|
19193
|
+
// ---------------------------------------------------------------------------
|
|
19194
|
+
// Conversions
|
|
19195
|
+
// ---------------------------------------------------------------------------
|
|
19196
|
+
/** HSV to RGB, each channel unrounded. */
|
|
19197
|
+
function hsvaToRgba({ h, s, v, a }) {
|
|
19198
|
+
const sat = s / 100;
|
|
19199
|
+
const val = v / 100;
|
|
19200
|
+
const at = (n) => {
|
|
19201
|
+
const k = (n + h / 60) % 6;
|
|
19202
|
+
return val - val * sat * Math.max(0, Math.min(k, 4 - k, 1));
|
|
19203
|
+
};
|
|
19204
|
+
return { r: at(5) * 255, g: at(3) * 255, b: at(1) * 255, a };
|
|
19205
|
+
}
|
|
19206
|
+
/** RGB to HSV. A gray has no hue, so it gets 0. */
|
|
19207
|
+
function rgbaToHsva({ r, g, b, a }) {
|
|
19208
|
+
const red = r / 255;
|
|
19209
|
+
const green = g / 255;
|
|
19210
|
+
const blue = b / 255;
|
|
19211
|
+
const max = Math.max(red, green, blue);
|
|
19212
|
+
const delta = max - Math.min(red, green, blue);
|
|
19213
|
+
let h = 0;
|
|
19214
|
+
if (delta > 0) {
|
|
19215
|
+
if (max === red)
|
|
19216
|
+
h = ((green - blue) / delta) % 6;
|
|
19217
|
+
else if (max === green)
|
|
19218
|
+
h = (blue - red) / delta + 2;
|
|
19219
|
+
else
|
|
19220
|
+
h = (red - green) / delta + 4;
|
|
19221
|
+
}
|
|
19222
|
+
return {
|
|
19223
|
+
h: (h * 60 + 360) % 360,
|
|
19224
|
+
s: max === 0 ? 0 : (delta / max) * 100,
|
|
19225
|
+
v: max * 100,
|
|
19226
|
+
a,
|
|
19227
|
+
};
|
|
19228
|
+
}
|
|
19229
|
+
/** HSL (saturation and lightness from 0 to 100) to HSV, keeping the hue. */
|
|
19230
|
+
function hslaToHsva(h, s, l, a) {
|
|
19231
|
+
const sat = s / 100;
|
|
19232
|
+
const light = l / 100;
|
|
19233
|
+
const v = light + sat * Math.min(light, 1 - light);
|
|
19234
|
+
return { h, s: v === 0 ? 0 : 2 * (1 - light / v) * 100, v: v * 100, a };
|
|
19235
|
+
}
|
|
19236
|
+
/** HSV to HSL, saturation and lightness from 0 to 100, keeping the hue. */
|
|
19237
|
+
function hsvaToHsla({ h, s, v, a }) {
|
|
19238
|
+
const sat = s / 100;
|
|
19239
|
+
const val = v / 100;
|
|
19240
|
+
const light = val * (1 - sat / 2);
|
|
19241
|
+
const lowest = Math.min(light, 1 - light);
|
|
19242
|
+
return {
|
|
19243
|
+
h,
|
|
19244
|
+
s: lowest === 0 ? 0 : ((val - light) / lowest) * 100,
|
|
19245
|
+
l: light * 100,
|
|
19246
|
+
a,
|
|
19247
|
+
};
|
|
19248
|
+
}
|
|
19249
|
+
// ---------------------------------------------------------------------------
|
|
19250
|
+
// Writing
|
|
19251
|
+
// ---------------------------------------------------------------------------
|
|
19252
|
+
const hexByte = (value) => Math.round(clamp$3(value, 0, 255))
|
|
19253
|
+
.toString(16)
|
|
19254
|
+
.padStart(2, '0');
|
|
19255
|
+
/** Alpha as CSS writes it, to two decimal places: `0.5`, `1`. */
|
|
19256
|
+
const writeAlpha = (a) => String(round(clamp$3(a, 0, 1), 2));
|
|
19257
|
+
/**
|
|
19258
|
+
* Writes a color in a format: `hsl(171, 100%, 41%)`, `#00d1b2` or
|
|
19259
|
+
* `rgb(0, 209, 178)`, with alpha as `hsla(…)`, `#00d1b2ff` and `rgba(…)`.
|
|
19260
|
+
* Channels are whole numbers, and the legacy comma form is used, which every
|
|
19261
|
+
* CSS parser and color library reads.
|
|
19262
|
+
*/
|
|
19263
|
+
function formatColor(color, format, alpha) {
|
|
19264
|
+
if (format === 'hsl') {
|
|
19265
|
+
const { h, s, l, a } = hsvaToHsla(color);
|
|
19266
|
+
const channels = `${Math.round(h) % 360}, ${Math.round(s)}%, ${Math.round(l)}%`;
|
|
19267
|
+
return alpha ? `hsla(${channels}, ${writeAlpha(a)})` : `hsl(${channels})`;
|
|
19268
|
+
}
|
|
19269
|
+
const { r, g, b, a } = hsvaToRgba(color);
|
|
19270
|
+
if (format === 'hex') {
|
|
19271
|
+
const hex = `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`;
|
|
19272
|
+
return alpha ? `${hex}${hexByte(a * 255)}` : hex;
|
|
19273
|
+
}
|
|
19274
|
+
const channels = `${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}`;
|
|
19275
|
+
return alpha ? `rgba(${channels}, ${writeAlpha(a)})` : `rgb(${channels})`;
|
|
19276
|
+
}
|
|
19277
|
+
/** The color as 8-bit RGB and alpha to two places, for telling colors apart. */
|
|
19278
|
+
const key = (color) => {
|
|
19279
|
+
const { r, g, b, a } = hsvaToRgba(color);
|
|
19280
|
+
return [r, g, b].map(c => Math.round(c)).join(',') + `,${writeAlpha(a)}`;
|
|
19281
|
+
};
|
|
19282
|
+
/** Whether two colors are the same once written as 8-bit RGB with alpha. */
|
|
19283
|
+
const sameColor = (one, other) => key(one) === key(other);
|
|
19284
|
+
/**
|
|
19285
|
+
* The relative luminance of a color, from 0 for black to 1 for white, as
|
|
19286
|
+
* WCAG defines it.
|
|
19287
|
+
*/
|
|
19288
|
+
function luminance(color) {
|
|
19289
|
+
const { r, g, b } = hsvaToRgba(color);
|
|
19290
|
+
const linear = (channel) => {
|
|
19291
|
+
const c = channel / 255;
|
|
19292
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
19293
|
+
};
|
|
19294
|
+
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
|
|
19295
|
+
}
|
|
19296
|
+
/**
|
|
19297
|
+
* Whether black reads better than white on a color, by WCAG contrast
|
|
19298
|
+
* ratio. Used for the check on a chosen swatch.
|
|
19299
|
+
*/
|
|
19300
|
+
function prefersDarkInk(color) {
|
|
19301
|
+
const l = luminance(color);
|
|
19302
|
+
return (l + 0.05) / 0.05 >= 1.05 / (l + 0.05);
|
|
19303
|
+
}
|
|
19304
|
+
// ---------------------------------------------------------------------------
|
|
19305
|
+
// Reading
|
|
19306
|
+
// ---------------------------------------------------------------------------
|
|
19307
|
+
// The CSS named colors, each name followed by its six hex digits.
|
|
19308
|
+
const NAMED = 'aliceblue:f0f8ff,antiquewhite:faebd7,aqua:00ffff,aquamarine:7fffd4,' +
|
|
19309
|
+
'azure:f0ffff,beige:f5f5dc,bisque:ffe4c4,black:000000,' +
|
|
19310
|
+
'blanchedalmond:ffebcd,blue:0000ff,blueviolet:8a2be2,brown:a52a2a,' +
|
|
19311
|
+
'burlywood:deb887,cadetblue:5f9ea0,chartreuse:7fff00,chocolate:d2691e,' +
|
|
19312
|
+
'coral:ff7f50,cornflowerblue:6495ed,cornsilk:fff8dc,crimson:dc143c,' +
|
|
19313
|
+
'cyan:00ffff,darkblue:00008b,darkcyan:008b8b,darkgoldenrod:b8860b,' +
|
|
19314
|
+
'darkgray:a9a9a9,darkgreen:006400,darkgrey:a9a9a9,darkkhaki:bdb76b,' +
|
|
19315
|
+
'darkmagenta:8b008b,darkolivegreen:556b2f,darkorange:ff8c00,' +
|
|
19316
|
+
'darkorchid:9932cc,darkred:8b0000,darksalmon:e9967a,' +
|
|
19317
|
+
'darkseagreen:8fbc8f,darkslateblue:483d8b,darkslategray:2f4f4f,' +
|
|
19318
|
+
'darkslategrey:2f4f4f,darkturquoise:00ced1,darkviolet:9400d3,' +
|
|
19319
|
+
'deeppink:ff1493,deepskyblue:00bfff,dimgray:696969,dimgrey:696969,' +
|
|
19320
|
+
'dodgerblue:1e90ff,firebrick:b22222,floralwhite:fffaf0,' +
|
|
19321
|
+
'forestgreen:228b22,fuchsia:ff00ff,gainsboro:dcdcdc,' +
|
|
19322
|
+
'ghostwhite:f8f8ff,gold:ffd700,goldenrod:daa520,gray:808080,' +
|
|
19323
|
+
'green:008000,greenyellow:adff2f,grey:808080,honeydew:f0fff0,' +
|
|
19324
|
+
'hotpink:ff69b4,indianred:cd5c5c,indigo:4b0082,ivory:fffff0,' +
|
|
19325
|
+
'khaki:f0e68c,lavender:e6e6fa,lavenderblush:fff0f5,' +
|
|
19326
|
+
'lawngreen:7cfc00,lemonchiffon:fffacd,lightblue:add8e6,' +
|
|
19327
|
+
'lightcoral:f08080,lightcyan:e0ffff,lightgoldenrodyellow:fafad2,' +
|
|
19328
|
+
'lightgray:d3d3d3,lightgreen:90ee90,lightgrey:d3d3d3,' +
|
|
19329
|
+
'lightpink:ffb6c1,lightsalmon:ffa07a,lightseagreen:20b2aa,' +
|
|
19330
|
+
'lightskyblue:87cefa,lightslategray:778899,lightslategrey:778899,' +
|
|
19331
|
+
'lightsteelblue:b0c4de,lightyellow:ffffe0,lime:00ff00,' +
|
|
19332
|
+
'limegreen:32cd32,linen:faf0e6,magenta:ff00ff,maroon:800000,' +
|
|
19333
|
+
'mediumaquamarine:66cdaa,mediumblue:0000cd,mediumorchid:ba55d3,' +
|
|
19334
|
+
'mediumpurple:9370db,mediumseagreen:3cb371,mediumslateblue:7b68ee,' +
|
|
19335
|
+
'mediumspringgreen:00fa9a,mediumturquoise:48d1cc,' +
|
|
19336
|
+
'mediumvioletred:c71585,midnightblue:191970,mintcream:f5fffa,' +
|
|
19337
|
+
'mistyrose:ffe4e1,moccasin:ffe4b5,navajowhite:ffdead,navy:000080,' +
|
|
19338
|
+
'oldlace:fdf5e6,olive:808000,olivedrab:6b8e23,orange:ffa500,' +
|
|
19339
|
+
'orangered:ff4500,orchid:da70d6,palegoldenrod:eee8aa,' +
|
|
19340
|
+
'palegreen:98fb98,paleturquoise:afeeee,palevioletred:db7093,' +
|
|
19341
|
+
'papayawhip:ffefd5,peachpuff:ffdab9,peru:cd853f,pink:ffc0cb,' +
|
|
19342
|
+
'plum:dda0dd,powderblue:b0e0e6,purple:800080,' +
|
|
19343
|
+
'rebeccapurple:663399,red:ff0000,rosybrown:bc8f8f,' +
|
|
19344
|
+
'royalblue:4169e1,saddlebrown:8b4513,salmon:fa8072,' +
|
|
19345
|
+
'sandybrown:f4a460,seagreen:2e8b57,seashell:fff5ee,sienna:a0522d,' +
|
|
19346
|
+
'silver:c0c0c0,skyblue:87ceeb,slateblue:6a5acd,slategray:708090,' +
|
|
19347
|
+
'slategrey:708090,snow:fffafa,springgreen:00ff7f,steelblue:4682b4,' +
|
|
19348
|
+
'tan:d2b48c,teal:008080,thistle:d8bfd8,tomato:ff6347,' +
|
|
19349
|
+
'turquoise:40e0d0,violet:ee82ee,wheat:f5deb3,white:ffffff,' +
|
|
19350
|
+
'whitesmoke:f5f5f5,yellow:ffff00,yellowgreen:9acd32';
|
|
19351
|
+
let namedColors;
|
|
19352
|
+
/** The hex digits of a CSS color name, or undefined for any other word. */
|
|
19353
|
+
const namedHex = (name) => {
|
|
19354
|
+
if (!namedColors) {
|
|
19355
|
+
namedColors = new Map(NAMED.split(',').map(entry => entry.split(':')));
|
|
19356
|
+
}
|
|
19357
|
+
return namedColors.get(name);
|
|
19358
|
+
};
|
|
19359
|
+
/** Reads `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`, the `#` optional. */
|
|
19360
|
+
function parseHex(text) {
|
|
19361
|
+
const digits = text.startsWith('#') ? text.slice(1) : text;
|
|
19362
|
+
if (!/^[0-9a-f]+$/.test(digits) || ![3, 4, 6, 8].includes(digits.length)) {
|
|
19363
|
+
return null;
|
|
19364
|
+
}
|
|
19365
|
+
const full = digits.length < 6
|
|
19366
|
+
? digits
|
|
19367
|
+
.split('')
|
|
19368
|
+
.map(d => d + d)
|
|
19369
|
+
.join('')
|
|
19370
|
+
: digits;
|
|
19371
|
+
const byte = (i) => parseInt(full.slice(i, i + 2), 16);
|
|
19372
|
+
return rgbaToHsva({
|
|
19373
|
+
r: byte(0),
|
|
19374
|
+
g: byte(2),
|
|
19375
|
+
b: byte(4),
|
|
19376
|
+
a: full.length === 8 ? byte(6) / 255 : 1,
|
|
19377
|
+
});
|
|
19378
|
+
}
|
|
19379
|
+
/**
|
|
19380
|
+
* The arguments of a CSS color function, in either syntax: commas
|
|
19381
|
+
* (`rgb(0, 209, 178)`) or spaces with the alpha after a slash
|
|
19382
|
+
* (`rgb(0 209 178 / 50%)`). Null when they're neither.
|
|
19383
|
+
*/
|
|
19384
|
+
function splitArguments(body) {
|
|
19385
|
+
if (body.includes(',')) {
|
|
19386
|
+
const parts = body.split(',').map(part => part.trim());
|
|
19387
|
+
return parts.includes('') ? null : parts;
|
|
19388
|
+
}
|
|
19389
|
+
const [channels, alpha, ...more] = body.split('/');
|
|
19390
|
+
const parts = channels.trim().split(/\s+/).filter(Boolean);
|
|
19391
|
+
if (more.length > 0 || parts.length !== 3)
|
|
19392
|
+
return null;
|
|
19393
|
+
if (alpha === undefined)
|
|
19394
|
+
return parts;
|
|
19395
|
+
const a = alpha.trim();
|
|
19396
|
+
return a && !/\s/.test(a) ? [...parts, a] : null;
|
|
19397
|
+
}
|
|
19398
|
+
/** A plain number or a percentage of `whole`, or NaN. */
|
|
19399
|
+
const numberOf = (part, whole) => {
|
|
19400
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?%?$/.test(part))
|
|
19401
|
+
return NaN;
|
|
19402
|
+
const value = parseFloat(part);
|
|
19403
|
+
return part.endsWith('%') ? (value / 100) * whole : value;
|
|
19404
|
+
};
|
|
19405
|
+
/** An alpha, `0.5` or `50%`, clamped to 0 to 1, or NaN. */
|
|
19406
|
+
const alphaOf = (part) => part === undefined ? 1 : clamp$3(numberOf(part, 1), 0, 1);
|
|
19407
|
+
const ANGLE_UNITS = {
|
|
19408
|
+
deg: 1,
|
|
19409
|
+
grad: 360 / 400,
|
|
19410
|
+
rad: 180 / Math.PI,
|
|
19411
|
+
turn: 360,
|
|
19412
|
+
};
|
|
19413
|
+
/** A hue in degrees from 0 to 360, from a number or an angle, or NaN. */
|
|
19414
|
+
const hueOf = (part) => {
|
|
19415
|
+
const match = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(deg|grad|rad|turn)?$/.exec(part);
|
|
19416
|
+
if (!match)
|
|
19417
|
+
return NaN;
|
|
19418
|
+
const degrees = parseFloat(match[1]) * ANGLE_UNITS[match[2] ?? 'deg'];
|
|
19419
|
+
return ((degrees % 360) + 360) % 360;
|
|
19420
|
+
};
|
|
19421
|
+
/** Reads `rgb()`, `rgba()`, `hsl()` and `hsla()` in either syntax. */
|
|
19422
|
+
function parseFunction(text) {
|
|
19423
|
+
const match = /^(rgba?|hsla?)\((.*)\)$/.exec(text);
|
|
19424
|
+
if (!match)
|
|
19425
|
+
return null;
|
|
19426
|
+
const parts = splitArguments(match[2]);
|
|
19427
|
+
if (!parts || parts.length < 3 || parts.length > 4)
|
|
19428
|
+
return null;
|
|
19429
|
+
const [first, second, third, fourth] = parts;
|
|
19430
|
+
const a = alphaOf(fourth);
|
|
19431
|
+
if (match[1].startsWith('rgb')) {
|
|
19432
|
+
const [r, g, b] = [first, second, third].map(part => clamp$3(numberOf(part, 255), 0, 255));
|
|
19433
|
+
if ([r, g, b, a].some(Number.isNaN))
|
|
19434
|
+
return null;
|
|
19435
|
+
return rgbaToHsva({ r, g, b, a });
|
|
19436
|
+
}
|
|
19437
|
+
const h = hueOf(first);
|
|
19438
|
+
// Modern syntax lets saturation and lightness drop the percent sign.
|
|
19439
|
+
const [s, l] = [second, third].map(part => clamp$3(numberOf(part.replace(/%$/, ''), 100), 0, 100));
|
|
19440
|
+
if ([h, s, l, a].some(Number.isNaN))
|
|
19441
|
+
return null;
|
|
19442
|
+
return hslaToHsva(h, s, l, a);
|
|
19443
|
+
}
|
|
19444
|
+
/**
|
|
19445
|
+
* Whether a color's text names its own hue, as `hsl()` and `hsla()` do even
|
|
19446
|
+
* for a gray, where hex, `rgb()` and the color names leave it out.
|
|
19447
|
+
*/
|
|
19448
|
+
const namesHue = (text) => /^\s*hsla?\(/i.test(text);
|
|
19449
|
+
/**
|
|
19450
|
+
* Reads a CSS color: hex (with or without `#`), `rgb()`, `rgba()`, `hsl()`
|
|
19451
|
+
* and `hsla()` in either syntax, the named colors, and `transparent`. Null
|
|
19452
|
+
* for anything else, such as `oklch()` or `var(--x)`, which only the
|
|
19453
|
+
* browser can resolve.
|
|
19454
|
+
*/
|
|
19455
|
+
function parseColor(input) {
|
|
19456
|
+
if (!input)
|
|
19457
|
+
return null;
|
|
19458
|
+
const text = input.trim().toLowerCase();
|
|
19459
|
+
if (text === 'transparent')
|
|
19460
|
+
return { h: 0, s: 0, v: 0, a: 0 };
|
|
19461
|
+
const named = namedHex(text);
|
|
19462
|
+
if (named)
|
|
19463
|
+
return parseHex(named);
|
|
19464
|
+
if (text.includes('('))
|
|
19465
|
+
return parseFunction(text);
|
|
19466
|
+
return parseHex(text);
|
|
19467
|
+
}
|
|
19468
|
+
|
|
19469
|
+
function useNativeMobilePicker(options = {}) {
|
|
19470
|
+
const { smallViewportMaxPx = 768, force } = options;
|
|
19471
|
+
const [state, setState] = React.useState({
|
|
19472
|
+
isCoarsePointer: false,
|
|
19473
|
+
isSmallViewport: false,
|
|
19474
|
+
shouldUseNative: force === true,
|
|
19475
|
+
});
|
|
19476
|
+
React.useEffect(() => {
|
|
19477
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
|
|
19478
|
+
if (!isBrowser)
|
|
19479
|
+
return undefined;
|
|
19480
|
+
const coarse = window.matchMedia('(pointer: coarse)');
|
|
19481
|
+
const small = window.matchMedia(`(max-width: ${smallViewportMaxPx}px)`);
|
|
19482
|
+
const update = () => {
|
|
19483
|
+
const isCoarsePointer = coarse.matches;
|
|
19484
|
+
const isSmallViewport = small.matches;
|
|
19485
|
+
const shouldUseNative = force === true
|
|
19486
|
+
? true
|
|
19487
|
+
: force === false
|
|
19488
|
+
? false
|
|
19489
|
+
: isCoarsePointer && isSmallViewport;
|
|
19490
|
+
setState({ isCoarsePointer, isSmallViewport, shouldUseNative });
|
|
19491
|
+
};
|
|
19492
|
+
update();
|
|
19493
|
+
const subscribe = (mql, fn) => {
|
|
19494
|
+
if (mql.addEventListener) {
|
|
19495
|
+
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener -- cleanup returned below and run in the effect teardown
|
|
19496
|
+
mql.addEventListener('change', fn);
|
|
19497
|
+
return () => mql.removeEventListener('change', fn);
|
|
19498
|
+
}
|
|
19499
|
+
// Older Safari fallback
|
|
19500
|
+
mql.addListener(fn);
|
|
19501
|
+
return () => mql.removeListener(fn);
|
|
19502
|
+
};
|
|
19503
|
+
const unsubCoarse = subscribe(coarse, update);
|
|
19504
|
+
const unsubSmall = subscribe(small, update);
|
|
19505
|
+
return () => {
|
|
19506
|
+
unsubCoarse();
|
|
19507
|
+
unsubSmall();
|
|
19508
|
+
};
|
|
19509
|
+
}, [smallViewportMaxPx, force]);
|
|
19510
|
+
return state;
|
|
19511
|
+
}
|
|
19512
|
+
|
|
19513
|
+
// INTERNAL: deliberately not exported from src/index.ts. The color a picker
|
|
19514
|
+
// holds and the context its parts read, shared by ColorPicker and the panel
|
|
19515
|
+
// ColorInput opens, so the panel needs no Field or Control of its own.
|
|
19516
|
+
const DEFAULT_LABELS = {
|
|
19517
|
+
area: 'Saturation and brightness',
|
|
19518
|
+
areaRoleDescription: '2D slider',
|
|
19519
|
+
saturation: 'Saturation',
|
|
19520
|
+
brightness: 'Brightness',
|
|
19521
|
+
hue: 'Hue',
|
|
19522
|
+
alpha: 'Alpha',
|
|
19523
|
+
degrees: 'degrees',
|
|
19524
|
+
value: 'Color value',
|
|
19525
|
+
swatches: 'Swatches',
|
|
19526
|
+
eyeDropper: 'Pick a color from the screen',
|
|
19527
|
+
};
|
|
19528
|
+
/** The labels with each one left out, or empty, at its default. */
|
|
19529
|
+
const mergeColorLabels = (labels) => {
|
|
19530
|
+
const merged = { ...DEFAULT_LABELS };
|
|
19531
|
+
for (const [key, value] of Object.entries(labels ?? {})) {
|
|
19532
|
+
if (value)
|
|
19533
|
+
merged[key] = value;
|
|
19534
|
+
}
|
|
19535
|
+
return merged;
|
|
19536
|
+
};
|
|
19537
|
+
const BLACK = { h: 0, s: 0, v: 0, a: 1 };
|
|
19538
|
+
/**
|
|
19539
|
+
* A color read from outside the picker, written as `source`, with the hue
|
|
19540
|
+
* and saturation the picker had where the new color has none, so the thumbs
|
|
19541
|
+
* don't jump to 0. A gray written as hex, `rgb()` or a name has no hue, so
|
|
19542
|
+
* it keeps the picker's; one written as `hsl()` names its hue, which wins.
|
|
19543
|
+
* Black has no saturation in the square either way, and keeps the picker's,
|
|
19544
|
+
* which writes no differently.
|
|
19545
|
+
*/
|
|
19546
|
+
const keepingChannels = (next, previous, source) => ({
|
|
19547
|
+
...next,
|
|
19548
|
+
h: (next.s === 0 || next.v === 0) && !namesHue(source) ? previous.h : next.h,
|
|
19549
|
+
s: next.v === 0 ? previous.s : next.s,
|
|
19550
|
+
});
|
|
19551
|
+
const ColorPickerContext = React.createContext(null);
|
|
19552
|
+
/** Does nothing, for a part outside a picker and a cancelled eye dropper. */
|
|
19553
|
+
const noop = () => { };
|
|
19554
|
+
// What a part outside a ColorPicker reads, so its hooks run the same; it
|
|
19555
|
+
// then renders nothing.
|
|
19556
|
+
const standalone = {
|
|
19557
|
+
color: BLACK,
|
|
19558
|
+
text: '',
|
|
19559
|
+
format: 'hsl',
|
|
19560
|
+
alpha: false,
|
|
19561
|
+
disabled: true,
|
|
19562
|
+
labels: DEFAULT_LABELS,
|
|
19563
|
+
swatches: undefined,
|
|
19564
|
+
eyeDropper: false,
|
|
19565
|
+
touch: false,
|
|
19566
|
+
percent: String,
|
|
19567
|
+
degrees: String,
|
|
19568
|
+
change: noop,
|
|
19569
|
+
end: noop,
|
|
19570
|
+
};
|
|
19571
|
+
/** The picker around a part, or null with a development warning. */
|
|
19572
|
+
function usePicker(part) {
|
|
19573
|
+
const picker = React.useContext(ColorPickerContext);
|
|
19574
|
+
const outside = !picker;
|
|
19575
|
+
React.useEffect(() => {
|
|
19576
|
+
if (outside) {
|
|
19577
|
+
warnOnce(`ColorPicker:${part}:outside`, `[bestax-bulma] <ColorPicker.${part}> needs a <ColorPicker> around ` +
|
|
19578
|
+
'it, which holds the color, and renders nothing without one.');
|
|
19579
|
+
}
|
|
19580
|
+
}, [outside, part]);
|
|
19581
|
+
return picker;
|
|
19582
|
+
}
|
|
19583
|
+
/** A percentage, in the provider's locale when there is one. */
|
|
19584
|
+
const writePercent = (formatNumber) => (value) => formatNumber
|
|
19585
|
+
? formatNumber(value / 100, { style: 'percent' })
|
|
19586
|
+
: `${value}%`;
|
|
19587
|
+
/** Degrees, in the provider's locale when there is one, else with `unit`. */
|
|
19588
|
+
const writeDegrees = (formatNumber, unit) => (value) => formatNumber
|
|
19589
|
+
? formatNumber(value, {
|
|
19590
|
+
style: 'unit',
|
|
19591
|
+
unit: 'degree',
|
|
19592
|
+
unitDisplay: 'long',
|
|
19593
|
+
})
|
|
19594
|
+
: `${value} ${unit}`;
|
|
19595
|
+
/**
|
|
19596
|
+
* Holds a picker's color and builds what its parts read. Controlled, the
|
|
19597
|
+
* color shown follows `value`; a value the picker handed out comes back as
|
|
19598
|
+
* the exact color it was, thumbs and all.
|
|
19599
|
+
*/
|
|
19600
|
+
function useColorPicker({ value, defaultValue, onChange, onChangeEnd, format, alpha, disabled, swatches, eyeDropper, labels, }) {
|
|
19601
|
+
const isControlled = value !== undefined;
|
|
19602
|
+
// The color the picker last set or started with, and its text. While
|
|
19603
|
+
// `value` is that text, this is the color shown.
|
|
19604
|
+
const [own, setOwn] = React.useState(() => {
|
|
19605
|
+
const start = isControlled ? value : defaultValue;
|
|
19606
|
+
return { text: start ?? '', color: parseColor(start) ?? BLACK };
|
|
19607
|
+
});
|
|
19608
|
+
// A controlled value from outside becomes the color held, during this
|
|
19609
|
+
// render, keeping the hue and saturation a gray or black has none of.
|
|
19610
|
+
// One the picker can't read changes nothing, so the last good color
|
|
19611
|
+
// stays on screen.
|
|
19612
|
+
if (isControlled && value !== own.text) {
|
|
19613
|
+
const parsed = parseColor(value);
|
|
19614
|
+
if (parsed) {
|
|
19615
|
+
setOwn({
|
|
19616
|
+
text: value,
|
|
19617
|
+
color: keepingChannels(parsed, own.color, value),
|
|
19618
|
+
});
|
|
19619
|
+
}
|
|
19620
|
+
}
|
|
19621
|
+
const color = alpha ? own.color : { ...own.color, a: 1 };
|
|
19622
|
+
const text = formatColor(color, format, alpha);
|
|
19623
|
+
// The latest text handed out, so a step that writes the same text
|
|
19624
|
+
// doesn't call onChange again, and a drag's end reports the last one.
|
|
19625
|
+
const lastTextRef = React.useRef(text);
|
|
19626
|
+
useClientLayoutEffect(() => {
|
|
19627
|
+
lastTextRef.current = text;
|
|
19628
|
+
});
|
|
19629
|
+
const change = React.useCallback((next, finished = false) => {
|
|
19630
|
+
if (disabled)
|
|
19631
|
+
return;
|
|
19632
|
+
const solid = alpha ? next : { ...next, a: 1 };
|
|
19633
|
+
const nextText = formatColor(solid, format, alpha);
|
|
19634
|
+
setOwn({ text: nextText, color: solid });
|
|
19635
|
+
if (nextText !== lastTextRef.current) {
|
|
19636
|
+
lastTextRef.current = nextText;
|
|
19637
|
+
onChange?.(nextText);
|
|
19638
|
+
}
|
|
19639
|
+
if (finished)
|
|
19640
|
+
onChangeEnd?.(nextText);
|
|
19641
|
+
}, [disabled, alpha, format, onChange, onChangeEnd]);
|
|
19642
|
+
const end = React.useCallback(() => onChangeEnd?.(lastTextRef.current), [onChangeEnd]);
|
|
19643
|
+
const formatNumber = useNumberFormat();
|
|
19644
|
+
const { isCoarsePointer } = useNativeMobilePicker();
|
|
19645
|
+
const merged = mergeColorLabels(labels);
|
|
19646
|
+
return {
|
|
19647
|
+
color,
|
|
19648
|
+
text,
|
|
19649
|
+
format,
|
|
19650
|
+
alpha,
|
|
19651
|
+
disabled,
|
|
19652
|
+
labels: merged,
|
|
19653
|
+
swatches,
|
|
19654
|
+
eyeDropper,
|
|
19655
|
+
touch: isCoarsePointer,
|
|
19656
|
+
percent: writePercent(formatNumber),
|
|
19657
|
+
degrees: writeDegrees(formatNumber, merged.degrees),
|
|
19658
|
+
change,
|
|
19659
|
+
end,
|
|
19660
|
+
};
|
|
19661
|
+
}
|
|
19662
|
+
|
|
19663
|
+
// The theme colors `swatches="bulma"` shows, read off the page.
|
|
19664
|
+
const BULMA_SWATCHES = [
|
|
19665
|
+
{ color: 'var(--bulma-primary)', label: 'Primary' },
|
|
19666
|
+
{ color: 'var(--bulma-link)', label: 'Link' },
|
|
19667
|
+
{ color: 'var(--bulma-info)', label: 'Info' },
|
|
19668
|
+
{ color: 'var(--bulma-success)', label: 'Success' },
|
|
19669
|
+
{ color: 'var(--bulma-warning)', label: 'Warning' },
|
|
19670
|
+
{ color: 'var(--bulma-danger)', label: 'Danger' },
|
|
19671
|
+
];
|
|
19672
|
+
const clamp$2 = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
19673
|
+
/** Whether the element is laid out right to left. */
|
|
19674
|
+
const isRtl = (element) => !!element && getComputedStyle(element).direction === 'rtl';
|
|
19675
|
+
/** Where a pointer is across an element, from 0 to 1 each way. */
|
|
19676
|
+
const pointIn = (element, event) => {
|
|
19677
|
+
const rect = element.getBoundingClientRect();
|
|
19678
|
+
const across = rect.width > 0 ? clamp$2((event.clientX - rect.left) / rect.width, 0, 1) : 0;
|
|
19679
|
+
const down = rect.height > 0 ? clamp$2((event.clientY - rect.top) / rect.height, 0, 1) : 0;
|
|
19680
|
+
return { x: isRtl(element) ? 1 - across : across, y: down };
|
|
19681
|
+
};
|
|
19682
|
+
/** The color's red, green and blue, rounded, as a CSS `rgb()` takes them. */
|
|
19683
|
+
const rgbChannels = (color) => {
|
|
19684
|
+
const { r, g, b } = hsvaToRgba(color);
|
|
19685
|
+
return `${Math.round(r)} ${Math.round(g)} ${Math.round(b)}`;
|
|
19686
|
+
};
|
|
19687
|
+
/** Whether a press should start a drag: any touch or pen, a mouse's main button. */
|
|
19688
|
+
const startsDrag = (event) => event.pointerType !== 'mouse' || event.button === 0;
|
|
19689
|
+
/** Hands a node to a forwarded ref and to one of the part's own. */
|
|
19690
|
+
function setRefs(node, own, forwarded) {
|
|
19691
|
+
own.current = node;
|
|
19692
|
+
if (typeof forwarded === 'function')
|
|
19693
|
+
forwarded(node);
|
|
19694
|
+
else if (forwarded)
|
|
19695
|
+
forwarded.current = node;
|
|
19696
|
+
}
|
|
19697
|
+
/**
|
|
19698
|
+
* The saturation and brightness square, operated by pointer or by keyboard as one 2D slider.
|
|
19699
|
+
*
|
|
19700
|
+
* @function
|
|
19701
|
+
* @param {ColorPickerAreaProps} props - Props for ColorPicker.Area.
|
|
19702
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the square's `<div>`.
|
|
19703
|
+
* @returns {JSX.Element | null} The square, or nothing outside a ColorPicker.
|
|
19704
|
+
*/
|
|
19705
|
+
const ColorPickerArea = React.forwardRef(({ className, style, ...props }, ref) => {
|
|
19706
|
+
const picker = usePicker('Area');
|
|
19707
|
+
const live = picker ?? standalone;
|
|
19708
|
+
const { color, change, end, disabled, labels, touch, percent } = live;
|
|
19709
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19710
|
+
const rootRef = React.useRef(null);
|
|
19711
|
+
const xRef = React.useRef(null);
|
|
19712
|
+
const yRef = React.useRef(null);
|
|
19713
|
+
// The axis whose input holds the tab stop: the one focused or moved last.
|
|
19714
|
+
const [axis, setAxis] = React.useState('x');
|
|
19715
|
+
// Once a key has moved the square, both inputs are exposed until focus
|
|
19716
|
+
// leaves it, so the one that moved can be read.
|
|
19717
|
+
const [keyed, setKeyed] = React.useState(false);
|
|
19718
|
+
const draggingRef = React.useRef(false);
|
|
19719
|
+
const areaClass = usePrefixedClassNames('color-area', {
|
|
19720
|
+
'is-disabled': disabled,
|
|
19721
|
+
});
|
|
19722
|
+
const thumbClass = usePrefixedClassNames('color-area-thumb');
|
|
19723
|
+
const inputClass = usePrefixedClassNames('color-area-input');
|
|
19724
|
+
const s = Math.round(color.s);
|
|
19725
|
+
const v = Math.round(color.v);
|
|
19726
|
+
const saturationText = `${labels.saturation} ${percent(s)}`;
|
|
19727
|
+
const brightnessText = `${labels.brightness} ${percent(v)}`;
|
|
19728
|
+
const focusAxis = (which) => (which === 'x' ? xRef : yRef).current?.focus({ preventScroll: true });
|
|
19729
|
+
// The axis a key moved to, focused once the render that shows its input
|
|
19730
|
+
// is on the page, so it's no longer aria-hidden as it takes focus.
|
|
19731
|
+
const focusNextRef = React.useRef(null);
|
|
19732
|
+
useClientLayoutEffect(() => {
|
|
19733
|
+
if (!focusNextRef.current)
|
|
19734
|
+
return;
|
|
19735
|
+
focusAxis(focusNextRef.current);
|
|
19736
|
+
focusNextRef.current = null;
|
|
19737
|
+
});
|
|
19738
|
+
const moveTo = (event) => {
|
|
19739
|
+
const { x, y } = pointIn(event.currentTarget, event);
|
|
19740
|
+
change({ ...color, s: x * 100, v: (1 - y) * 100 });
|
|
19741
|
+
};
|
|
19742
|
+
const onPointerDown = (event) => {
|
|
19743
|
+
if (disabled || !startsDrag(event))
|
|
19744
|
+
return;
|
|
19745
|
+
// No text selection, and focus goes where the square puts it.
|
|
19746
|
+
event.preventDefault();
|
|
19747
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
19748
|
+
draggingRef.current = true;
|
|
19749
|
+
moveTo(event);
|
|
19750
|
+
focusAxis(axis);
|
|
19751
|
+
};
|
|
19752
|
+
const onPointerMove = (event) => {
|
|
19753
|
+
if (draggingRef.current)
|
|
19754
|
+
moveTo(event);
|
|
19755
|
+
};
|
|
19756
|
+
const endDrag = (event) => {
|
|
19757
|
+
if (!draggingRef.current)
|
|
19758
|
+
return;
|
|
19759
|
+
draggingRef.current = false;
|
|
19760
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
19761
|
+
end();
|
|
19762
|
+
};
|
|
19763
|
+
const onKeyDown = (event) => {
|
|
19764
|
+
const by = event.shiftKey ? 10 : 1;
|
|
19765
|
+
// The horizontal arrows follow the screen: right to left, saturation
|
|
19766
|
+
// grows to the left.
|
|
19767
|
+
const right = isRtl(rootRef.current) ? -by : by;
|
|
19768
|
+
const moves = {
|
|
19769
|
+
ArrowRight: [right, 0],
|
|
19770
|
+
ArrowLeft: [-right, 0],
|
|
19771
|
+
ArrowUp: [0, by],
|
|
19772
|
+
ArrowDown: [0, -by],
|
|
19773
|
+
PageUp: [0, 10],
|
|
19774
|
+
PageDown: [0, -10],
|
|
19775
|
+
Home: [-10, 0],
|
|
19776
|
+
End: [10, 0],
|
|
19777
|
+
};
|
|
19778
|
+
const move = moves[event.key];
|
|
19779
|
+
if (!move)
|
|
19780
|
+
return;
|
|
19781
|
+
event.preventDefault();
|
|
19782
|
+
const [dx, dy] = move;
|
|
19783
|
+
change({
|
|
19784
|
+
...color,
|
|
19785
|
+
s: clamp$2(s + dx, 0, 100),
|
|
19786
|
+
v: clamp$2(v + dy, 0, 100),
|
|
19787
|
+
}, true);
|
|
19788
|
+
const moved = dx !== 0 ? 'x' : 'y';
|
|
19789
|
+
setAxis(moved);
|
|
19790
|
+
setKeyed(true);
|
|
19791
|
+
if (moved !== axis)
|
|
19792
|
+
focusNextRef.current = moved;
|
|
19793
|
+
};
|
|
19794
|
+
// A touch screen reader, or a browser's own stepping, sets an input's
|
|
19795
|
+
// value without a key the square handled.
|
|
19796
|
+
const onInput = (which) => (event) => {
|
|
19797
|
+
const value = Number(event.target.value);
|
|
19798
|
+
change(which === 'x' ? { ...color, s: value } : { ...color, v: value }, true);
|
|
19799
|
+
};
|
|
19800
|
+
const onBlur = (event) => {
|
|
19801
|
+
const next = event.relatedTarget;
|
|
19802
|
+
if (next !== xRef.current && next !== yRef.current)
|
|
19803
|
+
setKeyed(false);
|
|
19804
|
+
};
|
|
19805
|
+
// What both inputs share; each adds its axis, and the handlers.
|
|
19806
|
+
const shared = {
|
|
19807
|
+
type: 'range',
|
|
19808
|
+
className: inputClass,
|
|
19809
|
+
min: 0,
|
|
19810
|
+
max: 100,
|
|
19811
|
+
step: 1,
|
|
19812
|
+
disabled,
|
|
19813
|
+
'aria-label': labels.area,
|
|
19814
|
+
'aria-roledescription': labels.areaRoleDescription,
|
|
19815
|
+
};
|
|
19816
|
+
// The one not holding the tab stop is hidden until a key moves the
|
|
19817
|
+
// square, so a screen reader lists one control; both show on touch.
|
|
19818
|
+
const tabIndexOf = (which) => (touch || axis === which ? 0 : -1);
|
|
19819
|
+
const hiddenOf = (which) => touch || keyed || axis === which ? undefined : true;
|
|
19820
|
+
if (!picker)
|
|
19821
|
+
return null;
|
|
19822
|
+
return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(areaClass, bulmaHelperClasses, className), style: {
|
|
19823
|
+
...style,
|
|
19824
|
+
'--color-picker-hue': Math.round(color.h),
|
|
19825
|
+
'--color-picker-rgb': rgbChannels(color),
|
|
19826
|
+
'--color-area-x': `${color.s}%`,
|
|
19827
|
+
'--color-area-y': `${100 - color.v}%`,
|
|
19828
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: jsxRuntime.jsxs("div", { className: thumbClass, role: "presentation", children: [jsxRuntime.jsx("input", { ref: xRef, ...shared, value: s, "aria-valuetext": `${saturationText}, ${brightnessText}`, "aria-orientation": "horizontal", tabIndex: tabIndexOf('x'), "aria-hidden": hiddenOf('x'), onChange: onInput('x'), onKeyDown: onKeyDown, onFocus: () => setAxis('x'), onBlur: onBlur }), jsxRuntime.jsx("input", { ref: yRef, ...shared, value: v, "aria-valuetext": `${brightnessText}, ${saturationText}`, "aria-orientation": "vertical", tabIndex: tabIndexOf('y'), "aria-hidden": hiddenOf('y'), onChange: onInput('y'), onKeyDown: onKeyDown, onFocus: () => setAxis('y'), onBlur: onBlur })] }) }));
|
|
19829
|
+
});
|
|
19830
|
+
ColorPickerArea.displayName = 'ColorPicker.Area';
|
|
19831
|
+
/** The hue or the alpha slider: a hidden native range under a drawn track. */
|
|
19832
|
+
const ChannelSlider = React.forwardRef(({ channel, className, style, ...props }, ref) => {
|
|
19833
|
+
const picker = usePicker(channel === 'hue' ? 'HueSlider' : 'AlphaSlider');
|
|
19834
|
+
const live = picker ?? standalone;
|
|
19835
|
+
const { color, change, end, disabled, labels, percent, degrees } = live;
|
|
19836
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19837
|
+
const rootRef = React.useRef(null);
|
|
19838
|
+
const draggingRef = React.useRef(false);
|
|
19839
|
+
const isHue = channel === 'hue';
|
|
19840
|
+
const max = isHue ? 360 : 100;
|
|
19841
|
+
const value = Math.round(isHue ? color.h : color.a * 100);
|
|
19842
|
+
const withValue = (next) => isHue ? { ...color, h: next } : { ...color, a: next / 100 };
|
|
19843
|
+
const sliderClass = usePrefixedClassNames('color-slider', {
|
|
19844
|
+
'is-hue': isHue,
|
|
19845
|
+
'is-alpha': !isHue,
|
|
19846
|
+
'is-disabled': disabled,
|
|
19847
|
+
});
|
|
19848
|
+
const trackClass = usePrefixedClassNames('color-slider-track');
|
|
19849
|
+
const thumbClass = usePrefixedClassNames('color-slider-thumb');
|
|
19850
|
+
const inputClass = usePrefixedClassNames('color-slider-input');
|
|
19851
|
+
const moveTo = (event) => {
|
|
19852
|
+
const { x } = pointIn(event.currentTarget, event);
|
|
19853
|
+
change(withValue(x * max));
|
|
19854
|
+
};
|
|
19855
|
+
const onPointerDown = (event) => {
|
|
19856
|
+
if (disabled || !startsDrag(event))
|
|
19857
|
+
return;
|
|
19858
|
+
event.preventDefault();
|
|
19859
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
19860
|
+
draggingRef.current = true;
|
|
19861
|
+
moveTo(event);
|
|
19862
|
+
rootRef.current?.querySelector('input')?.focus({ preventScroll: true });
|
|
19863
|
+
};
|
|
19864
|
+
const onPointerMove = (event) => {
|
|
19865
|
+
if (draggingRef.current)
|
|
19866
|
+
moveTo(event);
|
|
19867
|
+
};
|
|
19868
|
+
const endDrag = (event) => {
|
|
19869
|
+
if (!draggingRef.current)
|
|
19870
|
+
return;
|
|
19871
|
+
draggingRef.current = false;
|
|
19872
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
19873
|
+
end();
|
|
19874
|
+
};
|
|
19875
|
+
const onKeyDown = (event) => {
|
|
19876
|
+
const by = event.shiftKey ? 10 : 1;
|
|
19877
|
+
const right = isRtl(rootRef.current) ? -by : by;
|
|
19878
|
+
const steps = {
|
|
19879
|
+
ArrowRight: right,
|
|
19880
|
+
ArrowLeft: -right,
|
|
19881
|
+
ArrowUp: by,
|
|
19882
|
+
ArrowDown: -by,
|
|
19883
|
+
PageUp: 10,
|
|
19884
|
+
PageDown: -10,
|
|
19885
|
+
Home: -max,
|
|
19886
|
+
End: max,
|
|
19887
|
+
};
|
|
19888
|
+
const step = steps[event.key];
|
|
19889
|
+
if (step === undefined)
|
|
19890
|
+
return;
|
|
19891
|
+
event.preventDefault();
|
|
19892
|
+
change(withValue(clamp$2(value + step, 0, max)), true);
|
|
19893
|
+
};
|
|
19894
|
+
if (!picker)
|
|
19895
|
+
return null;
|
|
19896
|
+
return (jsxRuntime.jsxs("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(sliderClass, bulmaHelperClasses, className), style: {
|
|
19897
|
+
...style,
|
|
19898
|
+
'--color-picker-hue': Math.round(color.h),
|
|
19899
|
+
'--color-picker-rgb': rgbChannels(color),
|
|
19900
|
+
'--color-slider-position': `${(value / max) * 100}%`,
|
|
19901
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, ...rest, children: [jsxRuntime.jsx("div", { className: trackClass }), jsxRuntime.jsx("div", { className: thumbClass, role: "presentation", children: jsxRuntime.jsx("input", { type: "range", className: inputClass, min: 0, max: max, step: 1, value: value, disabled: disabled,
|
|
19902
|
+
// Spelled out, as the square's is, so Safari's Tab reaches it
|
|
19903
|
+
// with its default settings, which skip a range input without
|
|
19904
|
+
// one.
|
|
19905
|
+
tabIndex: 0, "aria-label": isHue ? labels.hue : labels.alpha, "aria-valuetext": isHue ? degrees(value) : percent(value), onChange: event => change(withValue(Number(event.target.value)), true), onKeyDown: onKeyDown }) })] }));
|
|
19906
|
+
});
|
|
19907
|
+
ChannelSlider.displayName = 'ChannelSlider';
|
|
19908
|
+
/**
|
|
19909
|
+
* The hue slider, from red round the spectrum and back to red.
|
|
19910
|
+
*
|
|
19911
|
+
* @function
|
|
19912
|
+
* @param {ColorPickerSliderProps} props - Props for ColorPicker.HueSlider.
|
|
19913
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
|
|
19914
|
+
* @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
|
|
19915
|
+
*/
|
|
19916
|
+
const ColorPickerHueSlider = React.forwardRef((props, ref) => jsxRuntime.jsx(ChannelSlider, { ref: ref, ...props, channel: "hue" }));
|
|
19917
|
+
ColorPickerHueSlider.displayName = 'ColorPicker.HueSlider';
|
|
19918
|
+
/**
|
|
19919
|
+
* The alpha slider, from see-through to solid.
|
|
19920
|
+
*
|
|
19921
|
+
* @function
|
|
19922
|
+
* @param {ColorPickerSliderProps} props - Props for ColorPicker.AlphaSlider.
|
|
19923
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the slider's `<div>`.
|
|
19924
|
+
* @returns {JSX.Element | null} The slider, or nothing outside a ColorPicker.
|
|
19925
|
+
*/
|
|
19926
|
+
const ColorPickerAlphaSlider = React.forwardRef((props, ref) => jsxRuntime.jsx(ChannelSlider, { ref: ref, ...props, channel: "alpha" }));
|
|
19927
|
+
ColorPickerAlphaSlider.displayName = 'ColorPicker.AlphaSlider';
|
|
19928
|
+
/** Arrow keys and how far they move along the swatches. */
|
|
19929
|
+
const SWATCH_STEPS = {
|
|
19930
|
+
ArrowRight: 1,
|
|
19931
|
+
ArrowDown: 1,
|
|
19932
|
+
ArrowLeft: -1,
|
|
19933
|
+
ArrowUp: -1,
|
|
19934
|
+
};
|
|
19935
|
+
/**
|
|
19936
|
+
* The swatches, a radio group of preset colors that sets the picker's color.
|
|
19937
|
+
*
|
|
19938
|
+
* @function
|
|
19939
|
+
* @param {ColorPickerSwatchesProps} props - Props for ColorPicker.Swatches.
|
|
19940
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the group's `<div>`.
|
|
19941
|
+
* @returns {JSX.Element | null} The swatches, or nothing outside a ColorPicker or with none to show.
|
|
19942
|
+
*/
|
|
19943
|
+
const ColorPickerSwatches = React.forwardRef(({ className, swatches: ownSwatches, ...props }, ref) => {
|
|
19944
|
+
const picker = usePicker('Swatches');
|
|
19945
|
+
const live = picker ?? standalone;
|
|
19946
|
+
const { color, change, disabled, labels, format, alpha } = live;
|
|
19947
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
19948
|
+
const classPrefix = useClassPrefix();
|
|
19949
|
+
const groupClass = usePrefixedClassNames('color-picker-swatches');
|
|
19950
|
+
const rootRef = React.useRef(null);
|
|
19951
|
+
const given = ownSwatches ?? live.swatches;
|
|
19952
|
+
const list = React.useMemo(() => (given === 'bulma' ? BULMA_SWATCHES : (given ?? [])).map(entry => typeof entry === 'string' ? { color: entry } : entry), [given]);
|
|
19953
|
+
// A color the picker can't read as written, such as a var(), is read off
|
|
19954
|
+
// the page, as the text the swatch's own custom property resolves to.
|
|
19955
|
+
const chipsRef = React.useRef([]);
|
|
19956
|
+
const textOf = React.useCallback((i) => {
|
|
19957
|
+
const chip = chipsRef.current[i];
|
|
19958
|
+
if (parseColor(list[i].color) || !chip)
|
|
19959
|
+
return list[i].color;
|
|
19960
|
+
return getComputedStyle(chip)
|
|
19961
|
+
.getPropertyValue('--color-swatch-color')
|
|
19962
|
+
.trim();
|
|
19963
|
+
}, [list]);
|
|
19964
|
+
// Those texts, read after every render rather than once: a Theme can
|
|
19965
|
+
// change the variables behind them while the picker is open, and the
|
|
19966
|
+
// chosen swatch has to follow. A read that changes nothing renders
|
|
19967
|
+
// nothing more.
|
|
19968
|
+
const [resolved, setResolved] = React.useState([]);
|
|
19969
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose; the updater keeps the state when nothing changed, so it settles
|
|
19970
|
+
React.useEffect(() => {
|
|
19971
|
+
const next = list.map((_, i) => textOf(i));
|
|
19972
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- a var() color can only be read off the page once its swatch is on it
|
|
19973
|
+
setResolved(previous => previous.length === next.length &&
|
|
19974
|
+
previous.every((text, i) => text === next[i])
|
|
19975
|
+
? previous
|
|
19976
|
+
: next);
|
|
19977
|
+
});
|
|
19978
|
+
const colorOf = (i) => parseColor(list[i].color) ?? parseColor(resolved[i]);
|
|
19979
|
+
// The same color, as far as the picker's value can tell: the same 8-bit
|
|
19980
|
+
// color, or the same text, since an HSL value is rounded.
|
|
19981
|
+
const shownText = formatColor(color, format, alpha);
|
|
19982
|
+
const checked = list.findIndex((_, i) => {
|
|
19983
|
+
const swatch = colorOf(i);
|
|
19984
|
+
if (!swatch)
|
|
19985
|
+
return false;
|
|
19986
|
+
const solid = alpha ? swatch : { ...swatch, a: 1 };
|
|
19987
|
+
return (sameColor(solid, color) || formatColor(solid, format, alpha) === shownText);
|
|
19988
|
+
});
|
|
19989
|
+
const [focused, setFocused] = React.useState(null);
|
|
19990
|
+
const tabStop = focused ?? (checked >= 0 ? checked : 0);
|
|
19991
|
+
// Read afresh, so a theme that changed since the last render counts.
|
|
19992
|
+
const pick = (i) => {
|
|
19993
|
+
const text = textOf(i);
|
|
19994
|
+
const swatch = parseColor(text);
|
|
19995
|
+
if (swatch)
|
|
19996
|
+
change(keepingChannels(swatch, color, text), true);
|
|
19997
|
+
};
|
|
19998
|
+
const onKeyDown = (i) => (event) => {
|
|
19999
|
+
const buttons = Array.from(rootRef.current.querySelectorAll('[role="radio"]'));
|
|
20000
|
+
let next;
|
|
20001
|
+
if (event.key === 'Home')
|
|
20002
|
+
next = 0;
|
|
20003
|
+
else if (event.key === 'End')
|
|
20004
|
+
next = buttons.length - 1;
|
|
20005
|
+
else if (SWATCH_STEPS[event.key]) {
|
|
20006
|
+
// Left and Right follow the screen, so right to left they swap.
|
|
20007
|
+
const sideways = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
|
|
20008
|
+
const flip = sideways && isRtl(rootRef.current) ? -1 : 1;
|
|
20009
|
+
next =
|
|
20010
|
+
(i + SWATCH_STEPS[event.key] * flip + buttons.length) % buttons.length;
|
|
20011
|
+
}
|
|
20012
|
+
if (next === undefined)
|
|
20013
|
+
return;
|
|
20014
|
+
event.preventDefault();
|
|
20015
|
+
buttons[next]?.focus();
|
|
20016
|
+
pick(next);
|
|
20017
|
+
};
|
|
20018
|
+
if (!picker || list.length === 0)
|
|
20019
|
+
return null;
|
|
20020
|
+
return (jsxRuntime.jsx("div", { ref: node => setRefs(node, rootRef, ref), className: classNames(groupClass, bulmaHelperClasses, className), role: "radiogroup", "aria-label": labels.swatches, "aria-disabled": disabled || undefined, ...rest, children: list.map((entry, i) => {
|
|
20021
|
+
const swatch = colorOf(i);
|
|
20022
|
+
const isChecked = i === checked;
|
|
20023
|
+
// The check is drawn in black or white with a ring of the other, so
|
|
20024
|
+
// it shows on any color. On a see-through swatch the page shows
|
|
20025
|
+
// through, so it takes the scheme's text color and background,
|
|
20026
|
+
// which follow dark mode, in place of the swatch's own.
|
|
20027
|
+
const seeThrough = !!swatch && swatch.a < 1;
|
|
20028
|
+
return (jsxRuntime.jsx("button", { type: "button", role: "radio", "aria-checked": isChecked, "aria-label": entry.label || entry.color, title: entry.label, tabIndex: i === tabStop ? 0 : -1, disabled: disabled, className: prefixedClassNames(classPrefix, 'color-picker-swatch', {
|
|
20029
|
+
'is-checked': isChecked,
|
|
20030
|
+
'is-see-through': seeThrough,
|
|
20031
|
+
'is-dark-ink': !!swatch && !seeThrough && prefersDarkInk(swatch),
|
|
20032
|
+
}), onClick: () => pick(i), onKeyDown: onKeyDown(i), onFocus: () => setFocused(i), onBlur: () => setFocused(null), children: jsxRuntime.jsx(ColorSwatch, { ref: node => {
|
|
20033
|
+
chipsRef.current[i] = node;
|
|
20034
|
+
}, color: entry.color, "aria-hidden": "true", children: isChecked && (jsxRuntime.jsx(CheckIcon, { haloClass: prefixedClassNames(classPrefix, 'color-picker-check-halo') })) }) }, `${entry.color}-${i}`));
|
|
20035
|
+
}) }));
|
|
20036
|
+
});
|
|
20037
|
+
ColorPickerSwatches.displayName = 'ColorPicker.Swatches';
|
|
20038
|
+
/**
|
|
20039
|
+
* The check on the chosen swatch: a ring in `haloClass`, and the check over
|
|
20040
|
+
* it in the current color.
|
|
20041
|
+
*/
|
|
20042
|
+
const CheckIcon = ({ haloClass }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: ['halo', 'ink'].map(part => (jsxRuntime.jsx("path", { className: part === 'halo' ? haloClass : undefined, d: "M3.5 8.5l3 3 6-7", fill: "none", stroke: "currentColor", strokeWidth: part === 'halo' ? 4.5 : 2, strokeLinecap: "round", strokeLinejoin: "round" }, part))) }));
|
|
20043
|
+
/**
|
|
20044
|
+
* The value as text you can type in, beside a preview swatch, with the eye dropper when the picker has `eyeDropper`.
|
|
20045
|
+
*
|
|
20046
|
+
* @function
|
|
20047
|
+
* @param {ColorPickerInputProps} props - Props for ColorPicker.Input.
|
|
20048
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.
|
|
20049
|
+
* @returns {JSX.Element | null} The row, or nothing outside a ColorPicker.
|
|
20050
|
+
*/
|
|
20051
|
+
const ColorPickerInput = React.forwardRef(({ className, ...props }, ref) => {
|
|
20052
|
+
const picker = usePicker('Input');
|
|
20053
|
+
const live = picker ?? standalone;
|
|
20054
|
+
const { color, text, change, disabled, labels, eyeDropper } = live;
|
|
20055
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
20056
|
+
const rowClass = usePrefixedClassNames('color-picker-value');
|
|
20057
|
+
const previewClass = usePrefixedClassNames('color-picker-preview');
|
|
20058
|
+
// What's been typed and not yet committed, or null while the field
|
|
20059
|
+
// shows the value.
|
|
20060
|
+
const [draft, setDraft] = React.useState(null);
|
|
20061
|
+
const invalid = draft !== null && !parseColor(draft);
|
|
20062
|
+
const inputClass = usePrefixedClassNames('input', {
|
|
20063
|
+
'is-small': true,
|
|
20064
|
+
'is-danger': invalid,
|
|
20065
|
+
});
|
|
20066
|
+
const buttonClass = usePrefixedClassNames('button', { 'is-small': true });
|
|
20067
|
+
const hydrated = useIsHydrated();
|
|
20068
|
+
const EyeDropper = hydrated
|
|
20069
|
+
? window.EyeDropper
|
|
20070
|
+
: undefined;
|
|
20071
|
+
const commit = () => {
|
|
20072
|
+
if (draft === null)
|
|
17816
20073
|
return;
|
|
20074
|
+
const parsed = parseColor(draft);
|
|
20075
|
+
setDraft(null);
|
|
20076
|
+
if (parsed)
|
|
20077
|
+
change(keepingChannels(parsed, color, draft), true);
|
|
20078
|
+
};
|
|
20079
|
+
const onKeyDown = (event) => {
|
|
20080
|
+
if (draft === null)
|
|
20081
|
+
return;
|
|
20082
|
+
if (event.key === 'Enter') {
|
|
20083
|
+
// Commits rather than submitting a form; an unreadable value stays
|
|
20084
|
+
// to be fixed.
|
|
20085
|
+
event.preventDefault();
|
|
20086
|
+
if (!invalid)
|
|
20087
|
+
commit();
|
|
17817
20088
|
}
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
}
|
|
17824
|
-
else {
|
|
17825
|
-
setHighlightedIndex(prev => prev < filteredData.length - 1 ? prev + 1 : prev);
|
|
17826
|
-
}
|
|
17827
|
-
break;
|
|
17828
|
-
case 'ArrowUp':
|
|
17829
|
-
e.preventDefault();
|
|
17830
|
-
setHighlightedIndex(prev => (prev > 0 ? prev - 1 : prev));
|
|
17831
|
-
break;
|
|
17832
|
-
case 'Backspace':
|
|
17833
|
-
if (removeOnKeys && !inputValue && tags.length > 0) {
|
|
17834
|
-
e.preventDefault();
|
|
17835
|
-
removeTag(tags.length - 1);
|
|
17836
|
-
}
|
|
17837
|
-
break;
|
|
17838
|
-
case 'Escape':
|
|
17839
|
-
// Closing the suggestions claims the key, so a Modal around the
|
|
17840
|
-
// field stays open. With none showing, the key goes on to it.
|
|
17841
|
-
if (listOpen)
|
|
17842
|
-
e.preventDefault();
|
|
17843
|
-
setIsActive(false);
|
|
17844
|
-
setHighlightedIndex(-1);
|
|
17845
|
-
break;
|
|
17846
|
-
}
|
|
17847
|
-
}, [
|
|
17848
|
-
disabled,
|
|
17849
|
-
readonly,
|
|
17850
|
-
confirmKeys,
|
|
17851
|
-
highlightedIndex,
|
|
17852
|
-
filteredData,
|
|
17853
|
-
addTags,
|
|
17854
|
-
inputValue,
|
|
17855
|
-
isActive,
|
|
17856
|
-
listOpen,
|
|
17857
|
-
data.length,
|
|
17858
|
-
removeOnKeys,
|
|
17859
|
-
tags.length,
|
|
17860
|
-
removeTag,
|
|
17861
|
-
]);
|
|
17862
|
-
// Combined ref
|
|
17863
|
-
const combinedRef = React.useCallback((node) => {
|
|
17864
|
-
inputRef.current =
|
|
17865
|
-
node;
|
|
17866
|
-
if (typeof ref === 'function') {
|
|
17867
|
-
ref(node);
|
|
17868
|
-
}
|
|
17869
|
-
else if (ref) {
|
|
17870
|
-
ref.current =
|
|
17871
|
-
node;
|
|
20089
|
+
else if (event.key === 'Escape') {
|
|
20090
|
+
// Puts the value back, and keeps the key from closing a popover too.
|
|
20091
|
+
event.preventDefault();
|
|
20092
|
+
event.stopPropagation();
|
|
20093
|
+
setDraft(null);
|
|
17872
20094
|
}
|
|
17873
|
-
}
|
|
17874
|
-
//
|
|
17875
|
-
const
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
20095
|
+
};
|
|
20096
|
+
// Escape, or a click away, cancels it, which rejects.
|
|
20097
|
+
const pickFromScreen = (Dropper) => new Dropper().open().then(({ sRGBHex }) => {
|
|
20098
|
+
const parsed = parseColor(sRGBHex);
|
|
20099
|
+
// The screen gives a solid color as hex; the alpha stays as it was,
|
|
20100
|
+
// and a gray keeps the hue the picker had.
|
|
20101
|
+
if (parsed) {
|
|
20102
|
+
change(keepingChannels({ ...parsed, a: color.a }, color, sRGBHex), true);
|
|
20103
|
+
}
|
|
20104
|
+
}, noop);
|
|
20105
|
+
if (!picker)
|
|
20106
|
+
return null;
|
|
20107
|
+
return (jsxRuntime.jsxs("div", { ref: ref, className: classNames(rowClass, bulmaHelperClasses, className), ...rest, children: [jsxRuntime.jsx(ColorSwatch, { color: text, className: previewClass, "aria-hidden": "true" }), jsxRuntime.jsx("input", { type: "text", className: inputClass, value: draft ?? text, disabled: disabled, "aria-label": labels.value, "aria-invalid": invalid || undefined, spellCheck: false, autoComplete: "off", autoCapitalize: "off", onChange: event => setDraft(event.target.value), onKeyDown: onKeyDown, onBlur: commit }), eyeDropper && EyeDropper && (jsxRuntime.jsx("button", { type: "button", className: buttonClass, "aria-label": labels.eyeDropper, title: labels.eyeDropper, disabled: disabled, onClick: () => pickFromScreen(EyeDropper), children: jsxRuntime.jsx(EyeDropperIcon, {}) }))] }));
|
|
20108
|
+
});
|
|
20109
|
+
ColorPickerInput.displayName = 'ColorPicker.Input';
|
|
20110
|
+
/** A pipette. */
|
|
20111
|
+
const EyeDropperIcon = () => (jsxRuntime.jsx("svg", { viewBox: "0 0 16 16", width: "1em", height: "1em", "aria-hidden": "true", children: jsxRuntime.jsx("path", { d: "M10.5 2.5l3 3M9 4l3 3-6.5 6.5H3v-3z", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinejoin: "round" }) }));
|
|
20112
|
+
/**
|
|
20113
|
+
* The `ColorPicker` component is a panel for choosing a color: a saturation and brightness square, a hue slider, and the value as text, with alpha and swatches when asked for.
|
|
20114
|
+
*
|
|
20115
|
+
* Every part is operable by keyboard, and the square is two native range
|
|
20116
|
+
* inputs, one per axis, with one tab stop. The value is a CSS color string
|
|
20117
|
+
* in `format`, HSL by default.
|
|
20118
|
+
*
|
|
20119
|
+
* @function
|
|
20120
|
+
* @param {ColorPickerProps} props - Props for the ColorPicker component.
|
|
20121
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the picker's `<div>`.
|
|
20122
|
+
* @returns {JSX.Element} The rendered picker.
|
|
20123
|
+
*
|
|
20124
|
+
* @example
|
|
20125
|
+
* <ColorPicker label="Brand color" defaultValue="#1e6b99" swatches="bulma" />
|
|
20126
|
+
*/
|
|
20127
|
+
const ColorPickerRoot = React.forwardRef(({ className, label, labelSize, labelProps, horizontal, message, messageColor, fieldClassName, value, defaultValue, onChange, onChangeEnd, format = 'hsl', alpha = false, swatches, eyeDropper = false, disabled: disabledProp, name, form, labels, children, ...props }, ref) => {
|
|
20128
|
+
const disabled = useFieldsetDisabled(disabledProp);
|
|
20129
|
+
const insideField = useInsideField();
|
|
20130
|
+
const insideControl = useInsideControl();
|
|
20131
|
+
const ownField = rendersOwnField('ColorPicker', {
|
|
20132
|
+
insideField,
|
|
20133
|
+
insideControl,
|
|
20134
|
+
label,
|
|
20135
|
+
message,
|
|
20136
|
+
horizontal,
|
|
20137
|
+
fieldClassName,
|
|
17890
20138
|
});
|
|
17891
|
-
const
|
|
17892
|
-
|
|
20139
|
+
const { ariaLabelledBy, fieldLabelProps } = useAutoLabelledBy({
|
|
20140
|
+
label,
|
|
20141
|
+
labelProps,
|
|
20142
|
+
rendersLabel: ownField,
|
|
20143
|
+
callerProps: props,
|
|
17893
20144
|
});
|
|
17894
|
-
const
|
|
17895
|
-
|
|
17896
|
-
|
|
20145
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses(props);
|
|
20146
|
+
const pickerClass = usePrefixedClassNames('color-picker', {
|
|
20147
|
+
'is-disabled': disabled,
|
|
17897
20148
|
});
|
|
17898
|
-
const combinedClasses = classNames(taginputClasses, bulmaHelperClasses, className);
|
|
17899
|
-
// Counter text
|
|
17900
|
-
const showCounter = hasCounter && (maxTags !== undefined || maxlength !== undefined);
|
|
17901
|
-
const write = formatNumber ?? String;
|
|
17902
|
-
const counterText = maxTags !== undefined
|
|
17903
|
-
? `${write(tags.length)} / ${write(maxTags)}`
|
|
17904
|
-
: maxlength !== undefined
|
|
17905
|
-
? `${write(inputValue.length)} / ${write(maxlength)}`
|
|
17906
|
-
: '';
|
|
17907
|
-
const counterClasses = pcn('help', 'counter');
|
|
17908
|
-
const tagsClasses = pcn('tags');
|
|
17909
|
-
const dropdownMenuClasses = pcn('dropdown-menu', 'is-active');
|
|
17910
|
-
const dropdownContentClasses = pcn('dropdown-content');
|
|
17911
|
-
const iconRightClass = pcn('icon', 'is-right');
|
|
17912
|
-
const loaderClass = pcn('loader', 'is-loading');
|
|
17913
20149
|
const helpClass = usePrefixedClassNames('help', {
|
|
17914
20150
|
[`is-${messageColor}`]: !!messageColor,
|
|
17915
20151
|
});
|
|
20152
|
+
const context = useColorPicker({
|
|
20153
|
+
value,
|
|
20154
|
+
defaultValue,
|
|
20155
|
+
onChange,
|
|
20156
|
+
onChangeEnd,
|
|
20157
|
+
format,
|
|
20158
|
+
alpha,
|
|
20159
|
+
disabled,
|
|
20160
|
+
swatches,
|
|
20161
|
+
eyeDropper,
|
|
20162
|
+
labels,
|
|
20163
|
+
});
|
|
20164
|
+
const picker = (jsxRuntime.jsxs("div", { ref: ref, className: classNames(pickerClass, bulmaHelperClasses, className), role: "group", "aria-labelledby": ariaLabelledBy, ...rest, children: [jsxRuntime.jsx(ColorPickerContext.Provider, { value: context, children: children ?? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ColorPickerArea, {}), jsxRuntime.jsx(ColorPickerHueSlider, {}), alpha && jsxRuntime.jsx(ColorPickerAlphaSlider, {}), jsxRuntime.jsx(ColorPickerInput, {}), swatches && jsxRuntime.jsx(ColorPickerSwatches, {})] })) }), name && (jsxRuntime.jsx("input", { type: "hidden", name: name, form: form, value: context.text, disabled: disabled }))] }));
|
|
20165
|
+
let content = picker;
|
|
20166
|
+
if (rendersOwnControl('ColorPicker', { insideControl })) {
|
|
20167
|
+
content = jsxRuntime.jsx(Control, { children: content });
|
|
20168
|
+
}
|
|
17916
20169
|
const messageEl = message ? jsxRuntime.jsx("p", { className: helpClass, children: message }) : null;
|
|
17917
|
-
// The combobox controls its listbox and points at the highlighted option,
|
|
17918
|
-
// the way Autocomplete's does. The listbox takes its name from the label
|
|
17919
|
-
// that names the combobox, the one this renders or a surrounding Field's,
|
|
17920
|
-
// and keeps a fallback name when no label names it or the one that does
|
|
17921
|
-
// has no id to point at. While no suggestion matches there's no listbox,
|
|
17922
|
-
// so the combobox is collapsed and points at nothing. It skips a
|
|
17923
|
-
// highlighted suggestion that's blank once trimmed, since that one can't
|
|
17924
|
-
// become a tag, and the option it points at is the one marked selected.
|
|
17925
|
-
const optionId = (index) => `${listboxId}-${index}`;
|
|
17926
|
-
const activeOptionId = listOpen && filteredData[highlightedIndex]?.trim()
|
|
17927
|
-
? optionId(highlightedIndex)
|
|
17928
|
-
: undefined;
|
|
17929
|
-
const comboboxProps = {
|
|
17930
|
-
role: 'combobox',
|
|
17931
|
-
'aria-expanded': listOpen,
|
|
17932
|
-
'aria-controls': listOpen ? listboxId : undefined,
|
|
17933
|
-
'aria-activedescendant': activeOptionId,
|
|
17934
|
-
'aria-haspopup': 'listbox',
|
|
17935
|
-
'aria-autocomplete': 'list',
|
|
17936
|
-
};
|
|
17937
|
-
const taginputElement = (jsxRuntime.jsxs("div", { ref: containerRef, className: combinedClasses, onClick: handleContainerClick, ...rest, children: [jsxRuntime.jsxs("div", { className: containerClasses, children: [icon && (jsxRuntime.jsx(Icon, { name: icon, library: resolvedLibrary, variant: iconVariant, features: iconFeatures, containerClassName: pcn('icon', 'is-left') })), jsxRuntime.jsxs("div", { className: tagsClasses, children: [tags.map((tag, index) => {
|
|
17938
|
-
const displayVal = getDisplayValue(tag);
|
|
17939
|
-
return (jsxRuntime.jsxs("span", { className: tagClasses, children: [tagTemplate ? (tagTemplate(tag)) : ellipsis ? (jsxRuntime.jsx("span", { title: displayVal, children: displayVal })) : (displayVal), closable && !disabled && !readonly && (jsxRuntime.jsx("button", { type: "button", className: pcn('delete', 'is-small'), onClick: e => {
|
|
17940
|
-
e.stopPropagation();
|
|
17941
|
-
removeTag(index);
|
|
17942
|
-
}, "aria-label": ariaCloseLabel || `Remove ${displayVal}` }))] }, index));
|
|
17943
|
-
}), jsxRuntime.jsx("input", { ref: combinedRef, type: "text", className: inputClasses, id: controlId, value: inputValue, placeholder: tags.length === 0 ? placeholder : undefined, disabled: disabled,
|
|
17944
|
-
// Read-only at the tag limit rather than gone, so focus stays
|
|
17945
|
-
// put and a label keeps naming it, and Backspace still makes
|
|
17946
|
-
// room (#1103).
|
|
17947
|
-
readOnly: readonly || isMaxReached, maxLength: maxlength, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, onPaste: handlePaste, ...(hasSuggestions && comboboxProps), "aria-label": labelled ? undefined : 'Add tag', "aria-describedby": showCounter ? counterId : undefined })] }), loading && (jsxRuntime.jsx("span", { className: iconRightClass, children: jsxRuntime.jsx("span", { className: loaderClass }) }))] }), listOpen && (jsxRuntime.jsx("div", { className: dropdownMenuClasses, children: jsxRuntime.jsx("div", { ref: dropdownRef, id: listboxId, className: dropdownContentClasses, role: "listbox", "aria-labelledby": listLabelId, "aria-label": listLabelId ? undefined : 'Suggestions', children: filteredData.map((item, index) => (jsxRuntime.jsx("a", { id: optionId(index), className: pcn('dropdown-item', {
|
|
17948
|
-
'is-active': index === highlightedIndex,
|
|
17949
|
-
}), onClick: () => addTags([item]), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": optionId(index) === activeOptionId, children: item }, index))) }) })), showCounter && (jsxRuntime.jsx("small", { id: counterId, className: counterClasses, children: counterText })), name &&
|
|
17950
|
-
tags.map((tag, i) => (jsxRuntime.jsx("input", { type: "hidden", name: name, value: getDisplayValue(tag), form: form }, `tag-input-${i}`)))] }));
|
|
17951
20170
|
if (ownField) {
|
|
17952
|
-
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [
|
|
20171
|
+
return (jsxRuntime.jsxs(Field, { label: label, labelSize: labelSize, labelProps: fieldLabelProps, horizontal: horizontal, className: fieldClassName, children: [content, messageEl] }));
|
|
17953
20172
|
}
|
|
17954
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
20173
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, messageEl] }));
|
|
20174
|
+
});
|
|
20175
|
+
ColorPickerRoot.displayName = 'ColorPicker';
|
|
20176
|
+
const ColorPicker = withSubComponents(ColorPickerRoot, {
|
|
20177
|
+
Area: ColorPickerArea,
|
|
20178
|
+
HueSlider: ColorPickerHueSlider,
|
|
20179
|
+
AlphaSlider: ColorPickerAlphaSlider,
|
|
20180
|
+
Swatches: ColorPickerSwatches,
|
|
20181
|
+
Input: ColorPickerInput,
|
|
17955
20182
|
});
|
|
17956
|
-
Taginput.displayName = 'Taginput';
|
|
17957
|
-
/** @deprecated Use `Taginput` — same component. */
|
|
17958
|
-
const TagInput = Taginput;
|
|
17959
20183
|
|
|
17960
20184
|
/**
|
|
17961
20185
|
* Bulma Select component with full Bulma helper class support.
|
|
@@ -19666,50 +21890,6 @@ const PickerPopover = ({ isOpen, onClose, anchorRef, children, position = 'botto
|
|
|
19666
21890
|
};
|
|
19667
21891
|
PickerPopover.displayName = 'PickerPopover';
|
|
19668
21892
|
|
|
19669
|
-
function useNativeMobilePicker(options = {}) {
|
|
19670
|
-
const { smallViewportMaxPx = 768, force } = options;
|
|
19671
|
-
const [state, setState] = React.useState({
|
|
19672
|
-
isCoarsePointer: false,
|
|
19673
|
-
isSmallViewport: false,
|
|
19674
|
-
shouldUseNative: force === true,
|
|
19675
|
-
});
|
|
19676
|
-
React.useEffect(() => {
|
|
19677
|
-
const isBrowser = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
|
|
19678
|
-
if (!isBrowser)
|
|
19679
|
-
return undefined;
|
|
19680
|
-
const coarse = window.matchMedia('(pointer: coarse)');
|
|
19681
|
-
const small = window.matchMedia(`(max-width: ${smallViewportMaxPx}px)`);
|
|
19682
|
-
const update = () => {
|
|
19683
|
-
const isCoarsePointer = coarse.matches;
|
|
19684
|
-
const isSmallViewport = small.matches;
|
|
19685
|
-
const shouldUseNative = force === true
|
|
19686
|
-
? true
|
|
19687
|
-
: force === false
|
|
19688
|
-
? false
|
|
19689
|
-
: isCoarsePointer && isSmallViewport;
|
|
19690
|
-
setState({ isCoarsePointer, isSmallViewport, shouldUseNative });
|
|
19691
|
-
};
|
|
19692
|
-
update();
|
|
19693
|
-
const subscribe = (mql, fn) => {
|
|
19694
|
-
if (mql.addEventListener) {
|
|
19695
|
-
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener -- cleanup returned below and run in the effect teardown
|
|
19696
|
-
mql.addEventListener('change', fn);
|
|
19697
|
-
return () => mql.removeEventListener('change', fn);
|
|
19698
|
-
}
|
|
19699
|
-
// Older Safari fallback
|
|
19700
|
-
mql.addListener(fn);
|
|
19701
|
-
return () => mql.removeListener(fn);
|
|
19702
|
-
};
|
|
19703
|
-
const unsubCoarse = subscribe(coarse, update);
|
|
19704
|
-
const unsubSmall = subscribe(small, update);
|
|
19705
|
-
return () => {
|
|
19706
|
-
unsubCoarse();
|
|
19707
|
-
unsubSmall();
|
|
19708
|
-
};
|
|
19709
|
-
}, [smallViewportMaxPx, force]);
|
|
19710
|
-
return state;
|
|
19711
|
-
}
|
|
19712
|
-
|
|
19713
21893
|
/**
|
|
19714
21894
|
* Segment-map utility for manual keyboard entry on a date / time picker input.
|
|
19715
21895
|
* Given a token-format string like `'YYYY-MM-DD'`, `'HH:mm:ss'`, `'hh:mm A'`,
|
|
@@ -24936,6 +27116,13 @@ exports.ClientOnly = ClientOnly;
|
|
|
24936
27116
|
exports.Code = Code;
|
|
24937
27117
|
exports.Collapse = Collapse;
|
|
24938
27118
|
exports.Collapses = Collapses;
|
|
27119
|
+
exports.ColorPicker = ColorPicker;
|
|
27120
|
+
exports.ColorPickerAlphaSlider = ColorPickerAlphaSlider;
|
|
27121
|
+
exports.ColorPickerArea = ColorPickerArea;
|
|
27122
|
+
exports.ColorPickerHueSlider = ColorPickerHueSlider;
|
|
27123
|
+
exports.ColorPickerInput = ColorPickerInput;
|
|
27124
|
+
exports.ColorPickerSwatches = ColorPickerSwatches;
|
|
27125
|
+
exports.ColorSwatch = ColorSwatch;
|
|
24939
27126
|
exports.Column = Column;
|
|
24940
27127
|
exports.Columns = Columns;
|
|
24941
27128
|
exports.Combobox = Combobox;
|
|
@@ -24957,6 +27144,7 @@ exports.Container = Container;
|
|
|
24957
27144
|
exports.Content = Content;
|
|
24958
27145
|
exports.ContextMenu = ContextMenu;
|
|
24959
27146
|
exports.Control = Control;
|
|
27147
|
+
exports.DataTable = DataTable;
|
|
24960
27148
|
exports.DateInput = DateInput;
|
|
24961
27149
|
exports.DateInputBase = DateInputBase;
|
|
24962
27150
|
exports.DateRangeInput = DateRangeInput;
|
|
@@ -24974,6 +27162,7 @@ exports.Divider = Divider;
|
|
|
24974
27162
|
exports.Dropdown = Dropdown;
|
|
24975
27163
|
exports.DropdownDivider = DropdownDivider;
|
|
24976
27164
|
exports.DropdownItem = DropdownItem;
|
|
27165
|
+
exports.Dropzone = Dropzone;
|
|
24977
27166
|
exports.Emphasis = Emphasis;
|
|
24978
27167
|
exports.EmptyState = EmptyState;
|
|
24979
27168
|
exports.EmptyStateActions = EmptyStateActions;
|
|
@@ -25134,6 +27323,7 @@ exports.useClassPrefix = useClassPrefix;
|
|
|
25134
27323
|
exports.useColorClasses = useColorClasses;
|
|
25135
27324
|
exports.useColorStyles = useColorStyles;
|
|
25136
27325
|
exports.useConfig = useConfig;
|
|
27326
|
+
exports.useDataTable = useDataTable;
|
|
25137
27327
|
exports.useFlexboxClasses = useFlexboxClasses;
|
|
25138
27328
|
exports.useFocusTrap = useFocusTrap;
|
|
25139
27329
|
exports.useIconLibrary = useIconLibrary;
|