@allxsmith/bestax-bulma 5.31.0 → 5.33.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/Avatars.js +4 -21
- package/dist/esm/components/Avatars.js.map +1 -1
- package/dist/esm/components/Collapses.js +1 -20
- package/dist/esm/components/Collapses.js.map +1 -1
- package/dist/esm/components/SegmentedControl.js +230 -0
- package/dist/esm/components/SegmentedControl.js.map +1 -0
- package/dist/esm/elements/Kbd.js +86 -0
- package/dist/esm/elements/Kbd.js.map +1 -0
- package/dist/esm/elements/ToggleButton.js +2 -1
- package/dist/esm/elements/ToggleButton.js.map +1 -1
- package/dist/esm/helpers/flattenChildren.js +35 -0
- package/dist/esm/helpers/flattenChildren.js.map +1 -0
- package/dist/extras.css +1 -1
- package/dist/extras.css.map +1 -1
- package/dist/index.cjs +535 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +2 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/types/components/SegmentedControl.d.ts +116 -0
- package/dist/types/elements/Kbd.d.ts +100 -0
- package/dist/types/elements/ToggleButton.d.ts +2 -1
- package/dist/types/helpers/flattenChildren.d.ts +16 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types-cjs/components/SegmentedControl.d.ts +116 -0
- package/dist/types-cjs/elements/Kbd.d.ts +100 -0
- package/dist/types-cjs/elements/ToggleButton.d.ts +2 -1
- package/dist/types-cjs/helpers/flattenChildren.d.ts +16 -0
- package/dist/types-cjs/index.d.ts +2 -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/elements/_index.scss +1 -0
- package/src/scss/elements/_kbd.scss +86 -0
|
@@ -6,6 +6,7 @@ import { withSubComponents } from '../helpers/withSubComponents.js';
|
|
|
6
6
|
import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
|
|
7
7
|
import { Collapse } from './Collapse.js';
|
|
8
8
|
import { CollapsesItemContext } from './collapsesContext.js';
|
|
9
|
+
import { flattenChildren } from '../helpers/flattenChildren.js';
|
|
9
10
|
|
|
10
11
|
/** Either mode's value as the list of open indexes. */
|
|
11
12
|
function toIndexes(value) {
|
|
@@ -13,26 +14,6 @@ function toIndexes(value) {
|
|
|
13
14
|
return value;
|
|
14
15
|
return typeof value === 'number' ? [value] : [];
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Flattens Fragment children (recursively) so each item inside one gets its
|
|
18
|
-
* own index rather than sharing the fragment's. Children.toArray keys restart
|
|
19
|
-
* at ".0" inside each fragment, so those children are re-keyed with the
|
|
20
|
-
* fragment's own key as a prefix, the way Avatars does it.
|
|
21
|
-
*/
|
|
22
|
-
function flattenChildren(children, keyPrefix = '') {
|
|
23
|
-
return React.Children.toArray(children).flatMap(child => {
|
|
24
|
-
if (!React.isValidElement(child))
|
|
25
|
-
return [child];
|
|
26
|
-
if (child.type === React.Fragment) {
|
|
27
|
-
return flattenChildren(child.props.children, keyPrefix + child.key);
|
|
28
|
-
}
|
|
29
|
-
return [
|
|
30
|
-
keyPrefix
|
|
31
|
-
? React.cloneElement(child, { key: keyPrefix + child.key })
|
|
32
|
-
: child,
|
|
33
|
-
];
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
17
|
/**
|
|
37
18
|
* The `Collapses` component groups `Collapse` items into an accordion that keeps one item open at a time, or any number with `multiple`.
|
|
38
19
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Collapses.js","sources":["../../../src/components/Collapses.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { withSubComponents } from '../helpers/withSubComponents';\nimport { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';\nimport { Collapse } from './Collapse';\nimport { CollapsesItemContext } from './collapsesContext';\n\n/**\n * Props shared by both modes of the Collapses component.\n */\ninterface CollapsesBaseProps\n extends\n Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'color' | 'defaultValue' | 'onChange'\n >,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** Joins the items into one block, with no gap between them and only the outer corners rounded. The joins show on `bordered` items. */\n seamless?: boolean;\n /** Additional CSS classes to apply. */\n className?: string;\n /** The `Collapse` items. Each child element is one item, indexed from 0 in order, with the children of a fragment counted one by one. A child that sets its own `open` keeps it: the group leaves that item alone. */\n children?: React.ReactNode;\n}\n\n/**\n * Props for Collapses in single mode, the accordion: opening one item closes the others.\n */\nexport interface CollapsesSingleProps extends CollapsesBaseProps {\n /**\n * Lets items open independently. Without it, opening one item closes the others.\n * @defaultValue false\n */\n multiple?: false;\n /** Controlled open items: the index of the open item, or `null` for none. With `multiple`, an array of indexes. An index is a position among the children, so rendering an item conditionally or reordering them moves the open state onto a neighbour: render every item and hide one with `visibility=\"hidden\"`, or keep `value` in step yourself. */\n value?: number | null;\n /** Open items for uncontrolled use, as an index or `null` (the default, none open). With `multiple`, an array of indexes, empty by default. Indexes are positions among the children, as for `value`. */\n defaultValue?: number | null;\n /** Called with the new open items each time a trigger asks for a change, whether or not `value` is set: an index or `null`, or with `multiple` an array of indexes in ascending order. Indexes are positions among the children, as for `value`. */\n onChange?: (value: number | null) => void;\n}\n\n/**\n * Props for Collapses with `multiple`: items open and close independently.\n */\nexport interface CollapsesMultipleProps extends CollapsesBaseProps {\n /**\n * Lets items open independently. Without it, opening one item closes the others.\n * @defaultValue false\n */\n multiple: true;\n /** Controlled open items: the index of the open item, or `null` for none. With `multiple`, an array of indexes. An index is a position among the children, so rendering an item conditionally or reordering them moves the open state onto a neighbour: render every item and hide one with `visibility=\"hidden\"`, or keep `value` in step yourself. */\n value?: number[];\n /** Open items for uncontrolled use, as an index or `null` (the default, none open). With `multiple`, an array of indexes, empty by default. Indexes are positions among the children, as for `value`. */\n defaultValue?: number[];\n /** Called with the new open items each time a trigger asks for a change, whether or not `value` is set: an index or `null`, or with `multiple` an array of indexes in ascending order. Indexes are positions among the children, as for `value`. */\n onChange?: (value: number[]) => void;\n}\n\n/**\n * Props for the Collapses component, a discriminated union on `multiple`: the open items are an index (or `null`) in single mode and an array of indexes with `multiple`.\n */\nexport type CollapsesProps = CollapsesSingleProps | CollapsesMultipleProps;\n\n/** Either mode's value as the list of open indexes. */\nfunction toIndexes(value: number | number[] | null | undefined): number[] {\n if (Array.isArray(value)) return value;\n return typeof value === 'number' ? [value] : [];\n}\n\n/**\n * Flattens Fragment children (recursively) so each item inside one gets its\n * own index rather than sharing the fragment's. Children.toArray keys restart\n * at \".0\" inside each fragment, so those children are re-keyed with the\n * fragment's own key as a prefix, the way Avatars does it.\n */\nfunction flattenChildren(\n children: React.ReactNode,\n keyPrefix = ''\n): React.ReactNode[] {\n return React.Children.toArray(children).flatMap(child => {\n if (!React.isValidElement(child)) return [child];\n if (child.type === React.Fragment) {\n return flattenChildren(\n (child.props as { children?: React.ReactNode }).children,\n keyPrefix + child.key\n );\n }\n return [\n keyPrefix\n ? React.cloneElement(child, { key: keyPrefix + child.key })\n : child,\n ];\n });\n}\n\n/**\n * The `Collapses` component groups `Collapse` items into an accordion that keeps one item open at a time, or any number with `multiple`.\n *\n * @function\n * @param {CollapsesProps} props - Props for the Collapses component.\n * @returns {JSX.Element} The rendered group.\n *\n * @example\n * // Accordion: one item open at a time, the first to start with\n * <Collapses defaultValue={0}>\n * <Collapse bordered trigger={<strong>Shipping</strong>}>…</Collapse>\n * <Collapse bordered trigger={<strong>Returns</strong>}>…</Collapse>\n * </Collapses>\n *\n * @example\n * // Controlled, with items that open independently\n * const [open, setOpen] = useState<number[]>([]);\n * <Collapses multiple value={open} onChange={setOpen}>…</Collapses>\n */\nconst CollapsesComponent: React.FC<CollapsesProps> = ({\n multiple = false,\n value,\n defaultValue,\n onChange,\n seamless = false,\n className,\n children,\n ...props\n}) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });\n\n // Controlled vs uncontrolled, as in Tabs. State is held as a list of open\n // indexes in both modes; `multiple` only decides how a change is computed\n // and which shape `onChange` receives.\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState(() =>\n toIndexes(defaultValue)\n );\n const openIndexes = isControlled ? toIndexes(value) : internalOpen;\n\n // Each mode types `onChange` for its own shape, and the value built below\n // always matches `multiple`, the union's discriminant.\n const reportChange = onChange as\n ((next: number | number[] | null) => void) | undefined;\n\n const setItemOpen = (index: number, open: boolean) => {\n const others = openIndexes.filter(i => i !== index);\n // Single mode keeps at most one item open, so opening one drops the rest.\n // Ascending order, whatever order the items were opened in, so the value\n // reads and compares the same way however the user got there.\n const next = (\n open ? (multiple ? [...others, index] : [index]) : others\n ).sort((a, b) => a - b);\n if (!isControlled) setInternalOpen(next);\n reportChange?.(multiple ? next : (next[0] ?? null));\n };\n\n const groupClasses = usePrefixedClassNames('collapses', {\n 'is-seamless': seamless,\n });\n const combinedClasses = classNames(\n groupClasses,\n bulmaHelperClasses,\n className\n );\n\n const items: React.ReactNode[] = [];\n let index = 0;\n for (const child of flattenChildren(children)) {\n if (!React.isValidElement(child)) {\n items.push(child);\n continue;\n }\n const itemIndex = index;\n index += 1;\n items.push(\n <CollapsesItemContext.Provider\n key={child.key}\n value={{\n open: openIndexes.includes(itemIndex),\n setOpen: open => setItemOpen(itemIndex, open),\n }}\n >\n {child}\n </CollapsesItemContext.Provider>\n );\n }\n\n return (\n <div className={combinedClasses} {...rest}>\n {items}\n </div>\n );\n};\n\nexport const Collapses = withSubComponents(\n CollapsesComponent,\n { Collapse },\n 'Collapses'\n);\n\nexport default Collapses;\n"],"names":["_jsx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Collapses.js","sources":["../../../src/components/Collapses.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { withSubComponents } from '../helpers/withSubComponents';\nimport { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';\nimport { Collapse } from './Collapse';\nimport { CollapsesItemContext } from './collapsesContext';\nimport { flattenChildren } from '../helpers/flattenChildren';\n\n/**\n * Props shared by both modes of the Collapses component.\n */\ninterface CollapsesBaseProps\n extends\n Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'color' | 'defaultValue' | 'onChange'\n >,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** Joins the items into one block, with no gap between them and only the outer corners rounded. The joins show on `bordered` items. */\n seamless?: boolean;\n /** Additional CSS classes to apply. */\n className?: string;\n /** The `Collapse` items. Each child element is one item, indexed from 0 in order, with the children of a fragment counted one by one. A child that sets its own `open` keeps it: the group leaves that item alone. */\n children?: React.ReactNode;\n}\n\n/**\n * Props for Collapses in single mode, the accordion: opening one item closes the others.\n */\nexport interface CollapsesSingleProps extends CollapsesBaseProps {\n /**\n * Lets items open independently. Without it, opening one item closes the others.\n * @defaultValue false\n */\n multiple?: false;\n /** Controlled open items: the index of the open item, or `null` for none. With `multiple`, an array of indexes. An index is a position among the children, so rendering an item conditionally or reordering them moves the open state onto a neighbour: render every item and hide one with `visibility=\"hidden\"`, or keep `value` in step yourself. */\n value?: number | null;\n /** Open items for uncontrolled use, as an index or `null` (the default, none open). With `multiple`, an array of indexes, empty by default. Indexes are positions among the children, as for `value`. */\n defaultValue?: number | null;\n /** Called with the new open items each time a trigger asks for a change, whether or not `value` is set: an index or `null`, or with `multiple` an array of indexes in ascending order. Indexes are positions among the children, as for `value`. */\n onChange?: (value: number | null) => void;\n}\n\n/**\n * Props for Collapses with `multiple`: items open and close independently.\n */\nexport interface CollapsesMultipleProps extends CollapsesBaseProps {\n /**\n * Lets items open independently. Without it, opening one item closes the others.\n * @defaultValue false\n */\n multiple: true;\n /** Controlled open items: the index of the open item, or `null` for none. With `multiple`, an array of indexes. An index is a position among the children, so rendering an item conditionally or reordering them moves the open state onto a neighbour: render every item and hide one with `visibility=\"hidden\"`, or keep `value` in step yourself. */\n value?: number[];\n /** Open items for uncontrolled use, as an index or `null` (the default, none open). With `multiple`, an array of indexes, empty by default. Indexes are positions among the children, as for `value`. */\n defaultValue?: number[];\n /** Called with the new open items each time a trigger asks for a change, whether or not `value` is set: an index or `null`, or with `multiple` an array of indexes in ascending order. Indexes are positions among the children, as for `value`. */\n onChange?: (value: number[]) => void;\n}\n\n/**\n * Props for the Collapses component, a discriminated union on `multiple`: the open items are an index (or `null`) in single mode and an array of indexes with `multiple`.\n */\nexport type CollapsesProps = CollapsesSingleProps | CollapsesMultipleProps;\n\n/** Either mode's value as the list of open indexes. */\nfunction toIndexes(value: number | number[] | null | undefined): number[] {\n if (Array.isArray(value)) return value;\n return typeof value === 'number' ? [value] : [];\n}\n\n/**\n * The `Collapses` component groups `Collapse` items into an accordion that keeps one item open at a time, or any number with `multiple`.\n *\n * @function\n * @param {CollapsesProps} props - Props for the Collapses component.\n * @returns {JSX.Element} The rendered group.\n *\n * @example\n * // Accordion: one item open at a time, the first to start with\n * <Collapses defaultValue={0}>\n * <Collapse bordered trigger={<strong>Shipping</strong>}>…</Collapse>\n * <Collapse bordered trigger={<strong>Returns</strong>}>…</Collapse>\n * </Collapses>\n *\n * @example\n * // Controlled, with items that open independently\n * const [open, setOpen] = useState<number[]>([]);\n * <Collapses multiple value={open} onChange={setOpen}>…</Collapses>\n */\nconst CollapsesComponent: React.FC<CollapsesProps> = ({\n multiple = false,\n value,\n defaultValue,\n onChange,\n seamless = false,\n className,\n children,\n ...props\n}) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });\n\n // Controlled vs uncontrolled, as in Tabs. State is held as a list of open\n // indexes in both modes; `multiple` only decides how a change is computed\n // and which shape `onChange` receives.\n const isControlled = value !== undefined;\n const [internalOpen, setInternalOpen] = useState(() =>\n toIndexes(defaultValue)\n );\n const openIndexes = isControlled ? toIndexes(value) : internalOpen;\n\n // Each mode types `onChange` for its own shape, and the value built below\n // always matches `multiple`, the union's discriminant.\n const reportChange = onChange as\n ((next: number | number[] | null) => void) | undefined;\n\n const setItemOpen = (index: number, open: boolean) => {\n const others = openIndexes.filter(i => i !== index);\n // Single mode keeps at most one item open, so opening one drops the rest.\n // Ascending order, whatever order the items were opened in, so the value\n // reads and compares the same way however the user got there.\n const next = (\n open ? (multiple ? [...others, index] : [index]) : others\n ).sort((a, b) => a - b);\n if (!isControlled) setInternalOpen(next);\n reportChange?.(multiple ? next : (next[0] ?? null));\n };\n\n const groupClasses = usePrefixedClassNames('collapses', {\n 'is-seamless': seamless,\n });\n const combinedClasses = classNames(\n groupClasses,\n bulmaHelperClasses,\n className\n );\n\n const items: React.ReactNode[] = [];\n let index = 0;\n for (const child of flattenChildren(children)) {\n if (!React.isValidElement(child)) {\n items.push(child);\n continue;\n }\n const itemIndex = index;\n index += 1;\n items.push(\n <CollapsesItemContext.Provider\n key={child.key}\n value={{\n open: openIndexes.includes(itemIndex),\n setOpen: open => setItemOpen(itemIndex, open),\n }}\n >\n {child}\n </CollapsesItemContext.Provider>\n );\n }\n\n return (\n <div className={combinedClasses} {...rest}>\n {items}\n </div>\n );\n};\n\nexport const Collapses = withSubComponents(\n CollapsesComponent,\n { Collapse },\n 'Collapses'\n);\n\nexport default Collapses;\n"],"names":["_jsx"],"mappings":";;;;;;;;;;AAiEA;AACA,SAAS,SAAS,CAAC,KAA2C,EAAA;AAC5D,IAAA,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AACtC,IAAA,OAAO,OAAO,KAAK,KAAK,QAAQ,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;AACjD;AAEA;;;;;;;;;;;;;;;;;;AAkBG;AACH,MAAM,kBAAkB,GAA6B,CAAC,EACpD,QAAQ,GAAG,KAAK,EAChB,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,QAAQ,GAAG,KAAK,EAChB,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,KAAI;AACH,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;;;;AAKlE,IAAA,MAAM,YAAY,GAAG,KAAK,KAAK,SAAS;AACxC,IAAA,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,MAC/C,SAAS,CAAC,YAAY,CAAC,CACxB;AACD,IAAA,MAAM,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,YAAY;;;IAIlE,MAAM,YAAY,GAAG,QACmC;AAExD,IAAA,MAAM,WAAW,GAAG,CAAC,KAAa,EAAE,IAAa,KAAI;AACnD,QAAA,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC;;;;QAInD,MAAM,IAAI,GAAG,CACX,IAAI,IAAI,QAAQ,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,MAAM,EACzD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACvB,QAAA,IAAI,CAAC,YAAY;YAAE,eAAe,CAAC,IAAI,CAAC;AACxC,QAAA,YAAY,GAAG,QAAQ,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;AACrD,IAAA,CAAC;AAED,IAAA,MAAM,YAAY,GAAG,qBAAqB,CAAC,WAAW,EAAE;AACtD,QAAA,aAAa,EAAE,QAAQ;AACxB,KAAA,CAAC;IACF,MAAM,eAAe,GAAG,UAAU,CAChC,YAAY,EACZ,kBAAkB,EAClB,SAAS,CACV;IAED,MAAM,KAAK,GAAsB,EAAE;IACnC,IAAI,KAAK,GAAG,CAAC;IACb,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE;QAC7C,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE;AAChC,YAAA,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;YACjB;QACF;QACA,MAAM,SAAS,GAAG,KAAK;QACvB,KAAK,IAAI,CAAC;QACV,KAAK,CAAC,IAAI,CACRA,GAAA,CAAC,oBAAoB,CAAC,QAAQ,EAAA,EAE5B,KAAK,EAAE;AACL,gBAAA,IAAI,EAAE,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC;gBACrC,OAAO,EAAE,IAAI,IAAI,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC;AAC9C,aAAA,EAAA,QAAA,EAEA,KAAK,EAAA,EAND,KAAK,CAAC,GAAG,CAOgB,CACjC;IACH;IAEA,QACEA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAE,eAAe,EAAA,GAAM,IAAI,EAAA,QAAA,EACtC,KAAK,EAAA,CACF;AAEV,CAAC;AAEM,MAAM,SAAS,GAAG,iBAAiB,CACxC,kBAAkB,EAClB,EAAE,QAAQ,EAAE,EACZ,WAAW;;"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { createContext, forwardRef, useState, useCallback, useEffect, useContext, useRef } from 'react';
|
|
4
|
+
import { Button } from '../elements/Button.js';
|
|
5
|
+
import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
|
|
6
|
+
import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
|
|
7
|
+
import { withSubComponents } from '../helpers/withSubComponents.js';
|
|
8
|
+
import { warnOnce } from '../helpers/devWarnings.js';
|
|
9
|
+
import { useClientLayoutEffect } from '../helpers/useClientLayoutEffect.js';
|
|
10
|
+
|
|
11
|
+
// Not exported, so it stays out of the public API that `index.ts` re-exports.
|
|
12
|
+
const SegmentedControlContext = createContext(null);
|
|
13
|
+
/** Sorts mounted segments into the order they sit in the page. */
|
|
14
|
+
const inDocumentOrder = (a, b) => a.node.compareDocumentPosition(b.node) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
15
|
+
? -1
|
|
16
|
+
: 1;
|
|
17
|
+
/** How far each arrow key moves along the row. */
|
|
18
|
+
const STEPS = {
|
|
19
|
+
ArrowRight: 1,
|
|
20
|
+
ArrowDown: 1,
|
|
21
|
+
ArrowLeft: -1,
|
|
22
|
+
ArrowUp: -1,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The segment a key moves to, found among the segments of the same group in
|
|
26
|
+
* DOM order, skipping disabled ones and wrapping at either end. `undefined`
|
|
27
|
+
* for a key the group leaves alone, when no enabled segment is left, or when
|
|
28
|
+
* the segment isn't inside its row in the page, as a portaled one isn't.
|
|
29
|
+
*/
|
|
30
|
+
function segmentFor(current, key) {
|
|
31
|
+
const group = current.closest('[role="radiogroup"]');
|
|
32
|
+
if (!group)
|
|
33
|
+
return undefined;
|
|
34
|
+
const segments = Array.from(group.querySelectorAll('[role="radio"]'));
|
|
35
|
+
const enabled = segments.filter(segment => !segment.disabled);
|
|
36
|
+
if (key === 'Home')
|
|
37
|
+
return enabled[0];
|
|
38
|
+
if (key === 'End')
|
|
39
|
+
return enabled[enabled.length - 1];
|
|
40
|
+
const step = STEPS[key];
|
|
41
|
+
if (!step)
|
|
42
|
+
return undefined;
|
|
43
|
+
const at = segments.indexOf(current);
|
|
44
|
+
for (let i = 1; i <= segments.length; i += 1) {
|
|
45
|
+
const candidate = segments[(at + step * i + segments.length) % segments.length];
|
|
46
|
+
if (!candidate.disabled)
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The `SegmentedControl` component renders a row of buttons where only one can be on, and tells a screen reader it's a radio group.
|
|
53
|
+
*
|
|
54
|
+
* It's Bulma's `.buttons.has-addons` row, with the checked segment filled and
|
|
55
|
+
* marked `is-selected`, the way Bulma's button docs show it. To a screen
|
|
56
|
+
* reader it's `role="radiogroup"`, with `role="radio"` and `aria-checked` on
|
|
57
|
+
* each segment, which is how the APG toolbar example marks its alignment
|
|
58
|
+
* buttons. The keys follow the APG radio group: the row is one tab stop, the
|
|
59
|
+
* arrow keys move to the next or previous enabled segment and check it,
|
|
60
|
+
* wrapping at either end, Home and End go to the first and last, and Space
|
|
61
|
+
* checks the focused segment. Once a segment is checked, one always is.
|
|
62
|
+
*
|
|
63
|
+
* It carries no form value. A choice that's sent with a form is `Radios`.
|
|
64
|
+
*
|
|
65
|
+
* @function
|
|
66
|
+
* @param {SegmentedControlProps} props - Props for the SegmentedControl component.
|
|
67
|
+
* @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.
|
|
68
|
+
* @returns {JSX.Element} The rendered segmented control.
|
|
69
|
+
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/radio/ | APG radio group pattern}
|
|
70
|
+
* @see {@link https://bulma.io/documentation/elements/button/#list-of-buttons | Bulma list of buttons}
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* <SegmentedControl aria-label="Text alignment" defaultValue="left">
|
|
74
|
+
* <SegmentedControl.Segment value="left">Left</SegmentedControl.Segment>
|
|
75
|
+
* <SegmentedControl.Segment value="center">Center</SegmentedControl.Segment>
|
|
76
|
+
* <SegmentedControl.Segment value="right">Right</SegmentedControl.Segment>
|
|
77
|
+
* </SegmentedControl>
|
|
78
|
+
*/
|
|
79
|
+
const SegmentedControlRoot = forwardRef(function SegmentedControl({ value, defaultValue, onChange, color = 'link', size, hasAddons = true, isCentered, isRight, className, children, ...props }, ref) {
|
|
80
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });
|
|
81
|
+
// Controlled vs uncontrolled, as in Tabs and Collapses.
|
|
82
|
+
const isControlled = value !== undefined;
|
|
83
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
84
|
+
const checked = isControlled ? (value ?? undefined) : internalValue;
|
|
85
|
+
const select = (next) => {
|
|
86
|
+
if (next === checked)
|
|
87
|
+
return;
|
|
88
|
+
if (!isControlled)
|
|
89
|
+
setInternalValue(next);
|
|
90
|
+
onChange?.(next);
|
|
91
|
+
};
|
|
92
|
+
// The segments mount and register from an effect, so the group knows
|
|
93
|
+
// which can hold the tab stop wherever in the tree they sit.
|
|
94
|
+
const [segments, setSegments] = useState([]);
|
|
95
|
+
const register = useCallback((record) => {
|
|
96
|
+
setSegments(prev => [...prev, record]);
|
|
97
|
+
return () => setSegments(prev => prev.filter(r => r !== record));
|
|
98
|
+
}, []);
|
|
99
|
+
const [focused, setFocused] = useState();
|
|
100
|
+
// The first enabled segment in the page, sorted when it's read rather
|
|
101
|
+
// than when a segment registers, so reordered segments are seen too. It's
|
|
102
|
+
// read after the commit: a render runs before React moves the nodes, so
|
|
103
|
+
// sorting there would find the old order.
|
|
104
|
+
const [firstEnabled, setFirstEnabled] = useState();
|
|
105
|
+
useClientLayoutEffect(() => {
|
|
106
|
+
const inPage = segments.filter(s => !s.disabled).sort(inDocumentOrder);
|
|
107
|
+
setFirstEnabled(inPage[0]?.value);
|
|
108
|
+
}, [segments, children]);
|
|
109
|
+
// One tab stop for the row (roving tabindex). While focus is in the row
|
|
110
|
+
// the stop follows it, so Tab leaves from wherever focus is even when a
|
|
111
|
+
// controlled group hasn't taken the new value. Otherwise it's the checked
|
|
112
|
+
// segment, or the first enabled one when none is checked or the checked
|
|
113
|
+
// one is disabled. Before the segments register (the server render and
|
|
114
|
+
// the first client render) the checked one holds it, so a row with
|
|
115
|
+
// nothing checked has no stop until the client has rendered.
|
|
116
|
+
const canHoldStop = (candidate) => segments.some(s => !s.disabled && s.value === candidate);
|
|
117
|
+
let tabStop;
|
|
118
|
+
if (canHoldStop(focused))
|
|
119
|
+
tabStop = focused;
|
|
120
|
+
else if (segments.length === 0 || canHoldStop(checked))
|
|
121
|
+
tabStop = checked;
|
|
122
|
+
else
|
|
123
|
+
tabStop = firstEnabled;
|
|
124
|
+
const named = Boolean(rest['aria-label'] || rest['aria-labelledby']);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!named) {
|
|
127
|
+
warnOnce('SegmentedControl:unnamed', '[bestax-bulma] <SegmentedControl> has no accessible name. Pass ' +
|
|
128
|
+
'aria-label or aria-labelledby, so a screen reader can say what ' +
|
|
129
|
+
'the choice is.');
|
|
130
|
+
}
|
|
131
|
+
}, [named]);
|
|
132
|
+
const rowClasses = usePrefixedClassNames('buttons', {
|
|
133
|
+
'has-addons': hasAddons,
|
|
134
|
+
'is-centered': isCentered,
|
|
135
|
+
'is-right': isRight,
|
|
136
|
+
[`are-${size}`]: size,
|
|
137
|
+
});
|
|
138
|
+
// The props are spread first, so the role a screen reader relies on wins
|
|
139
|
+
// over anything a JavaScript caller or a spread object passes along.
|
|
140
|
+
return (jsx(SegmentedControlContext.Provider, { value: { checked, tabStop, color, select, setFocused, register }, children: jsx("div", { ...rest, ref: ref, role: "radiogroup", className: classNames(rowClasses, bulmaHelperClasses, className), children: children }) }));
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* One option in the row: a `<button role="radio">` that's checked while the group's value is its `value`, filled with the group's `color` and marked `is-selected`.
|
|
144
|
+
*
|
|
145
|
+
* @function
|
|
146
|
+
* @param {SegmentedControlSegmentProps} props - Props for the segment.
|
|
147
|
+
* @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.
|
|
148
|
+
* @returns {JSX.Element} The rendered segment.
|
|
149
|
+
*/
|
|
150
|
+
const Segment = forwardRef(function Segment({ value, isDisabled, disabled, className, onClick, onKeyDown, onFocus, onBlur, ...props }, ref) {
|
|
151
|
+
const ctx = useContext(SegmentedControlContext);
|
|
152
|
+
const checked = ctx !== null && ctx.checked === value;
|
|
153
|
+
const off = Boolean(isDisabled || disabled);
|
|
154
|
+
// Prefixed like Button's own classes, which a hand-written
|
|
155
|
+
// `className="is-selected"` could not be under ConfigProvider classPrefix.
|
|
156
|
+
// Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.
|
|
157
|
+
const selectedClass = usePrefixedClassNames({ 'is-selected': checked });
|
|
158
|
+
const nodeRef = useRef(null);
|
|
159
|
+
const setRefs = useCallback((node) => {
|
|
160
|
+
nodeRef.current = node;
|
|
161
|
+
if (typeof ref === 'function')
|
|
162
|
+
ref(node);
|
|
163
|
+
else if (ref)
|
|
164
|
+
ref.current = node;
|
|
165
|
+
}, [ref]);
|
|
166
|
+
const register = ctx?.register;
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!register) {
|
|
169
|
+
warnOnce('SegmentedControl.Segment:outside', '[bestax-bulma] <SegmentedControl.Segment> is outside a ' +
|
|
170
|
+
'<SegmentedControl>, so nothing can check it. Put it inside one.');
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
return register({
|
|
174
|
+
value,
|
|
175
|
+
disabled: off,
|
|
176
|
+
node: nodeRef.current,
|
|
177
|
+
});
|
|
178
|
+
}, [register, value, off]);
|
|
179
|
+
// The caller's onClick runs first, so a `preventDefault()` there keeps
|
|
180
|
+
// the segment from being checked. A disabled button gets no click.
|
|
181
|
+
const handleClick = (event) => {
|
|
182
|
+
onClick?.(event);
|
|
183
|
+
if (event.defaultPrevented)
|
|
184
|
+
return;
|
|
185
|
+
ctx?.select(value);
|
|
186
|
+
};
|
|
187
|
+
const handleKeyDown = (event) => {
|
|
188
|
+
onKeyDown?.(event);
|
|
189
|
+
if (!ctx ||
|
|
190
|
+
event.defaultPrevented ||
|
|
191
|
+
event.altKey ||
|
|
192
|
+
event.ctrlKey ||
|
|
193
|
+
event.metaKey) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const target = segmentFor(event.currentTarget, event.key);
|
|
197
|
+
if (!target)
|
|
198
|
+
return;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
// The row has taken this key, so nothing above it acts on it too: a
|
|
201
|
+
// Carousel moves its slide on the arrows from a document listener that
|
|
202
|
+
// doesn't read `defaultPrevented`. Keys the row leaves alone still bubble.
|
|
203
|
+
event.stopPropagation();
|
|
204
|
+
target.focus();
|
|
205
|
+
// The check follows focus, APG's rule for a radio group outside a
|
|
206
|
+
// toolbar. It goes through a click on the segment, as a native radio's
|
|
207
|
+
// does, so its onClick sees it. Inside a toolbar the arrows would move
|
|
208
|
+
// focus alone; that is the line a toolbar would change.
|
|
209
|
+
target.click();
|
|
210
|
+
};
|
|
211
|
+
const handleFocus = (event) => {
|
|
212
|
+
onFocus?.(event);
|
|
213
|
+
ctx?.setFocused(value);
|
|
214
|
+
};
|
|
215
|
+
// The segment focus moves to, if any, records itself straight after.
|
|
216
|
+
const handleBlur = (event) => {
|
|
217
|
+
onBlur?.(event);
|
|
218
|
+
ctx?.setFocused(undefined);
|
|
219
|
+
};
|
|
220
|
+
// The props are spread first, so what a radio depends on wins over
|
|
221
|
+
// anything a JavaScript caller or a spread object passes along: it stays
|
|
222
|
+
// a button, never submits, reports whether it's checked, and the group
|
|
223
|
+
// decides whether Tab reaches it.
|
|
224
|
+
return (jsx(Button, { ...props, ref: setRefs, as: "button", type: "button", role: "radio", "aria-checked": checked, tabIndex: ctx ? (ctx.tabStop === value ? 0 : -1) : undefined, isDisabled: off, color: checked ? ctx.color : undefined, className: classNames(selectedClass, className), onClick: handleClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur }));
|
|
225
|
+
});
|
|
226
|
+
Segment.displayName = 'SegmentedControl.Segment';
|
|
227
|
+
const SegmentedControl = withSubComponents(SegmentedControlRoot, { Segment }, 'SegmentedControl');
|
|
228
|
+
|
|
229
|
+
export { SegmentedControl };
|
|
230
|
+
//# sourceMappingURL=SegmentedControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SegmentedControl.js","sources":["../../../src/components/SegmentedControl.tsx"],"sourcesContent":["import React, {\n createContext,\n forwardRef,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { Button, type ButtonOwnProps } from '../elements/Button';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport { useBulmaClasses, BulmaClassesProps } from '../helpers/useBulmaClasses';\nimport { withSubComponents } from '../helpers/withSubComponents';\nimport { warnOnce } from '../helpers/devWarnings';\nimport { useClientLayoutEffect } from '../helpers/useClientLayoutEffect';\n\n// Spelled out so the API page can list the values. The tests hold it to\n// ToggleButton's, since the checked segment fills the way a pressed toggle\n// does and for the same reasons.\n/**\n * The fills the checked segment takes: `Button`'s colors, minus `text` and\n * `ghost`, which don't fill, and `white`, `light`, `dark` and `black`, which in\n * one theme or the other fill about as light or dark as an unchecked segment\n * already is. The same set `ToggleButton` takes.\n */\nexport type SegmentedControlColor =\n 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\n/** A mounted segment, as the group needs to know it for the tab stop. */\ninterface SegmentRecord {\n value: string;\n disabled: boolean;\n node: HTMLButtonElement;\n}\n\ninterface SegmentedControlContextValue {\n /** The checked segment's value, or `undefined` while none is. */\n checked: string | undefined;\n /** The segment that holds `tabIndex={0}`, or `undefined` for none. */\n tabStop: string | undefined;\n /** The fill the checked segment takes. */\n color: SegmentedControlColor;\n /** Checks a segment, unless it's checked already. */\n select: (value: string) => void;\n /** Records which segment has focus, so the tab stop can follow it. */\n setFocused: (value: string | undefined) => void;\n register: (record: SegmentRecord) => () => void;\n}\n\n// Not exported, so it stays out of the public API that `index.ts` re-exports.\nconst SegmentedControlContext =\n createContext<SegmentedControlContextValue | null>(null);\n\n/** Sorts mounted segments into the order they sit in the page. */\nconst inDocumentOrder = (a: SegmentRecord, b: SegmentRecord) =>\n a.node.compareDocumentPosition(b.node) & Node.DOCUMENT_POSITION_FOLLOWING\n ? -1\n : 1;\n\n/** How far each arrow key moves along the row. */\nconst STEPS: Record<string, number> = {\n ArrowRight: 1,\n ArrowDown: 1,\n ArrowLeft: -1,\n ArrowUp: -1,\n};\n\n/**\n * The segment a key moves to, found among the segments of the same group in\n * DOM order, skipping disabled ones and wrapping at either end. `undefined`\n * for a key the group leaves alone, when no enabled segment is left, or when\n * the segment isn't inside its row in the page, as a portaled one isn't.\n */\nfunction segmentFor(\n current: HTMLButtonElement,\n key: string\n): HTMLButtonElement | undefined {\n const group = current.closest('[role=\"radiogroup\"]');\n if (!group) return undefined;\n const segments = Array.from(\n group.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')\n );\n const enabled = segments.filter(segment => !segment.disabled);\n\n if (key === 'Home') return enabled[0];\n if (key === 'End') return enabled[enabled.length - 1];\n\n const step = STEPS[key];\n if (!step) return undefined;\n const at = segments.indexOf(current);\n for (let i = 1; i <= segments.length; i += 1) {\n const candidate =\n segments[(at + step * i + segments.length) % segments.length];\n if (!candidate.disabled) return candidate;\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// SegmentedControl (root)\n// ---------------------------------------------------------------------------\n\n/**\n * Props for the SegmentedControl component. The helper props, the other `<div>`\n * attributes and the `ref` all land on the row.\n *\n * @extraProp {string} [aria-label] - Names the group for a screen reader, such as \"Text alignment\". A radio group needs a name, so pass this or `aria-labelledby`, and a development build warns when it has neither.\n * @extraProp {string} [aria-labelledby] - The id of a visible heading or label that names the group, in place of `aria-label`.\n * @extraProp {React.Ref<HTMLDivElement>} [ref] - Ref forwarded to the row's `<div>`.\n */\nexport interface SegmentedControlProps\n extends\n Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'onChange' | 'defaultValue' | 'color' | 'role'\n >,\n Omit<\n BulmaClassesProps,\n 'color' | 'colorShade' | 'backgroundColor' | 'backgroundColorShade'\n > {\n /**\n * The checked segment's `value`, when you hold the state. Setting it makes\n * the group controlled: picking a segment only calls `onChange`, and nothing\n * changes until you pass the new value. `null` checks nothing, with the\n * tab stop `defaultValue` describes for that. Pick this or `defaultValue`\n * for the group's lifetime.\n */\n value?: string | null;\n /**\n * The segment checked to begin with, when the group holds its own state.\n * Leave it out and nothing is checked until someone picks one, and Tab\n * lands on the first enabled segment meanwhile. That fallback takes hold\n * once the row has rendered in the browser: server markup with nothing\n * checked has no tab stop until then, while a checked segment is the stop\n * from the server markup on.\n */\n defaultValue?: string;\n /**\n * Called with the newly checked segment's `value`, whether or not `value`\n * is set. The arrow keys, Home and End check the segment they move to, so\n * each of those calls it too: the check follows focus, as in a native radio\n * group. Pressing the checked segment again doesn't call it, since a\n * segmented control can't be cleared: one stays checked once one is.\n */\n onChange?: (value: string) => void;\n /**\n * The checked segment's fill. The others are plain buttons, so the fill is\n * the difference people see. Takes the same colors as `ToggleButton`, for\n * the same reason: `white`, `light`, `dark` and `black` fill about as light\n * or dark as a plain button already is in one theme or the other.\n */\n color?: SegmentedControlColor;\n /** Size of every segment, through Bulma's `are-<size>` on the row. */\n size?: 'small' | 'medium' | 'large';\n /**\n * Joins the segments into one row with no gap between them, Bulma's\n * `has-addons`. Set it to `false` for separate buttons.\n */\n hasAddons?: boolean;\n /** Centers the row. */\n isCentered?: boolean;\n /** Aligns the row to the right. */\n isRight?: boolean;\n /** Additional CSS classes to apply. */\n className?: string;\n /** The `SegmentedControl.Segment`s, each with a `value` of its own. */\n children?: React.ReactNode;\n}\n\n/**\n * The `SegmentedControl` component renders a row of buttons where only one can be on, and tells a screen reader it's a radio group.\n *\n * It's Bulma's `.buttons.has-addons` row, with the checked segment filled and\n * marked `is-selected`, the way Bulma's button docs show it. To a screen\n * reader it's `role=\"radiogroup\"`, with `role=\"radio\"` and `aria-checked` on\n * each segment, which is how the APG toolbar example marks its alignment\n * buttons. The keys follow the APG radio group: the row is one tab stop, the\n * arrow keys move to the next or previous enabled segment and check it,\n * wrapping at either end, Home and End go to the first and last, and Space\n * checks the focused segment. Once a segment is checked, one always is.\n *\n * It carries no form value. A choice that's sent with a form is `Radios`.\n *\n * @function\n * @param {SegmentedControlProps} props - Props for the SegmentedControl component.\n * @param {React.Ref<HTMLDivElement>} ref - Forwarded ref to the row's `<div>`.\n * @returns {JSX.Element} The rendered segmented control.\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/radio/ | APG radio group pattern}\n * @see {@link https://bulma.io/documentation/elements/button/#list-of-buttons | Bulma list of buttons}\n *\n * @example\n * <SegmentedControl aria-label=\"Text alignment\" defaultValue=\"left\">\n * <SegmentedControl.Segment value=\"left\">Left</SegmentedControl.Segment>\n * <SegmentedControl.Segment value=\"center\">Center</SegmentedControl.Segment>\n * <SegmentedControl.Segment value=\"right\">Right</SegmentedControl.Segment>\n * </SegmentedControl>\n */\nconst SegmentedControlRoot = forwardRef<HTMLDivElement, SegmentedControlProps>(\n function SegmentedControl(\n {\n value,\n defaultValue,\n onChange,\n color = 'link',\n size,\n hasAddons = true,\n isCentered,\n isRight,\n className,\n children,\n ...props\n },\n ref\n ) {\n const { bulmaHelperClasses, rest } = useBulmaClasses({ ...props });\n\n // Controlled vs uncontrolled, as in Tabs and Collapses.\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState(defaultValue);\n const checked = isControlled ? (value ?? undefined) : internalValue;\n\n const select = (next: string) => {\n if (next === checked) return;\n if (!isControlled) setInternalValue(next);\n onChange?.(next);\n };\n\n // The segments mount and register from an effect, so the group knows\n // which can hold the tab stop wherever in the tree they sit.\n const [segments, setSegments] = useState<SegmentRecord[]>([]);\n const register = useCallback((record: SegmentRecord) => {\n setSegments(prev => [...prev, record]);\n return () => setSegments(prev => prev.filter(r => r !== record));\n }, []);\n const [focused, setFocused] = useState<string | undefined>();\n\n // The first enabled segment in the page, sorted when it's read rather\n // than when a segment registers, so reordered segments are seen too. It's\n // read after the commit: a render runs before React moves the nodes, so\n // sorting there would find the old order.\n const [firstEnabled, setFirstEnabled] = useState<string | undefined>();\n useClientLayoutEffect(() => {\n const inPage = segments.filter(s => !s.disabled).sort(inDocumentOrder);\n setFirstEnabled(inPage[0]?.value);\n }, [segments, children]);\n\n // One tab stop for the row (roving tabindex). While focus is in the row\n // the stop follows it, so Tab leaves from wherever focus is even when a\n // controlled group hasn't taken the new value. Otherwise it's the checked\n // segment, or the first enabled one when none is checked or the checked\n // one is disabled. Before the segments register (the server render and\n // the first client render) the checked one holds it, so a row with\n // nothing checked has no stop until the client has rendered.\n const canHoldStop = (candidate: string | undefined) =>\n segments.some(s => !s.disabled && s.value === candidate);\n let tabStop: string | undefined;\n if (canHoldStop(focused)) tabStop = focused;\n else if (segments.length === 0 || canHoldStop(checked)) tabStop = checked;\n else tabStop = firstEnabled;\n\n const named = Boolean(rest['aria-label'] || rest['aria-labelledby']);\n useEffect(() => {\n if (!named) {\n warnOnce(\n 'SegmentedControl:unnamed',\n '[bestax-bulma] <SegmentedControl> has no accessible name. Pass ' +\n 'aria-label or aria-labelledby, so a screen reader can say what ' +\n 'the choice is.'\n );\n }\n }, [named]);\n\n const rowClasses = usePrefixedClassNames('buttons', {\n 'has-addons': hasAddons,\n 'is-centered': isCentered,\n 'is-right': isRight,\n [`are-${size}`]: size,\n });\n\n // The props are spread first, so the role a screen reader relies on wins\n // over anything a JavaScript caller or a spread object passes along.\n return (\n <SegmentedControlContext.Provider\n value={{ checked, tabStop, color, select, setFocused, register }}\n >\n <div\n {...rest}\n ref={ref}\n role=\"radiogroup\"\n className={classNames(rowClasses, bulmaHelperClasses, className)}\n >\n {children}\n </div>\n </SegmentedControlContext.Provider>\n );\n }\n);\n\n// ---------------------------------------------------------------------------\n// SegmentedControl.Segment\n// ---------------------------------------------------------------------------\n\n/**\n * Props for SegmentedControl.Segment. The helper props, the other button\n * attributes and the `ref` all land on the `<button>`.\n *\n * Some of `Button`'s props are left out on purpose. `as`, `type`, `role` and\n * `tabIndex`, since a segment is always a `<button type=\"button\" role=\"radio\">`\n * and the group decides which segment Tab reaches. `color`, which the group\n * sets for the checked segment, and the props that change the fill\n * (`isActive`, `isLight`, `isOutlined`, `isInverted`, `textColor`, `bgColor`\n * and their shades), since each would color it whether checked or not. `size`\n * and the width and shape props, which the row sets for every segment. And\n * `isStatic` and `isLoading`, a segment you couldn't pick.\n *\n * @extraProp {React.Ref<HTMLButtonElement>} [ref] - Ref forwarded to the `<button>`.\n */\nexport interface SegmentedControlSegmentProps\n extends\n Omit<\n ButtonOwnProps,\n | 'color'\n | 'size'\n | 'isActive'\n | 'isStatic'\n | 'isLoading'\n | 'isLight'\n | 'isOutlined'\n | 'isInverted'\n | 'isRounded'\n | 'isFullwidth'\n | 'isFullWidth'\n | 'textColor'\n | 'bgColor'\n | 'colorShade'\n | 'backgroundColorShade'\n >,\n Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n | keyof ButtonOwnProps\n | 'type'\n | 'value'\n | 'role'\n | 'tabIndex'\n | 'aria-checked'\n | 'aria-pressed'\n > {\n /**\n * **Required.** What `onChange` reports when this segment is checked, and\n * what the group's `value` and `defaultValue` name it by. Unique within the\n * group.\n */\n value: string;\n /**\n * The label. An icon on its own needs an `aria-label`, so a screen reader\n * has something to say.\n */\n children?: React.ReactNode;\n /**\n * Disables the segment, the same as `disabled`: it can't be clicked, the\n * arrow keys, Home and End skip it, and it never holds the row's tab stop.\n * A checked one keeps its fill under Bulma's disabled look.\n */\n isDisabled?: boolean;\n /**\n * Not accepted: `aria-checked` follows the group's value. Declared because\n * JSX lets a hyphenated attribute through when the props don't name it.\n * @internal\n */\n 'aria-checked'?: never;\n /**\n * Not accepted: a segment is a radio, which says checked, not pressed.\n * @internal\n */\n 'aria-pressed'?: never;\n}\n\n/**\n * One option in the row: a `<button role=\"radio\">` that's checked while the group's value is its `value`, filled with the group's `color` and marked `is-selected`.\n *\n * @function\n * @param {SegmentedControlSegmentProps} props - Props for the segment.\n * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.\n * @returns {JSX.Element} The rendered segment.\n */\nconst Segment = forwardRef<HTMLButtonElement, SegmentedControlSegmentProps>(\n function Segment(\n {\n value,\n isDisabled,\n disabled,\n className,\n onClick,\n onKeyDown,\n onFocus,\n onBlur,\n ...props\n },\n ref\n ) {\n const ctx = useContext(SegmentedControlContext);\n const checked = ctx !== null && ctx.checked === value;\n const off = Boolean(isDisabled || disabled);\n\n // Prefixed like Button's own classes, which a hand-written\n // `className=\"is-selected\"` could not be under ConfigProvider classPrefix.\n // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.\n const selectedClass = usePrefixedClassNames({ 'is-selected': checked });\n\n const nodeRef = useRef<HTMLButtonElement | null>(null);\n const setRefs = useCallback(\n (node: HTMLButtonElement | null) => {\n nodeRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n },\n [ref]\n );\n\n const register = ctx?.register;\n useEffect(() => {\n if (!register) {\n warnOnce(\n 'SegmentedControl.Segment:outside',\n '[bestax-bulma] <SegmentedControl.Segment> is outside a ' +\n '<SegmentedControl>, so nothing can check it. Put it inside one.'\n );\n return undefined;\n }\n return register({\n value,\n disabled: off,\n node: nodeRef.current as HTMLButtonElement,\n });\n }, [register, value, off]);\n\n // The caller's onClick runs first, so a `preventDefault()` there keeps\n // the segment from being checked. A disabled button gets no click.\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n if (event.defaultPrevented) return;\n ctx?.select(value);\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n onKeyDown?.(event);\n if (\n !ctx ||\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n ) {\n return;\n }\n const target = segmentFor(event.currentTarget, event.key);\n if (!target) return;\n event.preventDefault();\n // The row has taken this key, so nothing above it acts on it too: a\n // Carousel moves its slide on the arrows from a document listener that\n // doesn't read `defaultPrevented`. Keys the row leaves alone still bubble.\n event.stopPropagation();\n target.focus();\n // The check follows focus, APG's rule for a radio group outside a\n // toolbar. It goes through a click on the segment, as a native radio's\n // does, so its onClick sees it. Inside a toolbar the arrows would move\n // focus alone; that is the line a toolbar would change.\n target.click();\n };\n\n const handleFocus = (event: React.FocusEvent<HTMLButtonElement>) => {\n onFocus?.(event);\n ctx?.setFocused(value);\n };\n\n // The segment focus moves to, if any, records itself straight after.\n const handleBlur = (event: React.FocusEvent<HTMLButtonElement>) => {\n onBlur?.(event);\n ctx?.setFocused(undefined);\n };\n\n // The props are spread first, so what a radio depends on wins over\n // anything a JavaScript caller or a spread object passes along: it stays\n // a button, never submits, reports whether it's checked, and the group\n // decides whether Tab reaches it.\n return (\n <Button\n {...props}\n ref={setRefs}\n as=\"button\"\n type=\"button\"\n role=\"radio\"\n aria-checked={checked}\n tabIndex={ctx ? (ctx.tabStop === value ? 0 : -1) : undefined}\n isDisabled={off}\n color={checked ? ctx.color : undefined}\n className={classNames(selectedClass, className)}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onFocus={handleFocus}\n onBlur={handleBlur}\n />\n );\n }\n);\n\nSegment.displayName = 'SegmentedControl.Segment';\n\nexport const SegmentedControl = withSubComponents(\n SegmentedControlRoot,\n { Segment },\n 'SegmentedControl'\n);\n\nexport default SegmentedControl;\n"],"names":["_jsx"],"mappings":";;;;;;;;;;AAqDA;AACA,MAAM,uBAAuB,GAC3B,aAAa,CAAsC,IAAI,CAAC;AAE1D;AACA,MAAM,eAAe,GAAG,CAAC,CAAgB,EAAE,CAAgB,KACzD,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;MAC1C;MACA,CAAC;AAEP;AACA,MAAM,KAAK,GAA2B;AACpC,IAAA,UAAU,EAAE,CAAC;AACb,IAAA,SAAS,EAAE,CAAC;IACZ,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;CACZ;AAED;;;;;AAKG;AACH,SAAS,UAAU,CACjB,OAA0B,EAC1B,GAAW,EAAA;IAEX,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC;AACpD,IAAA,IAAI,CAAC,KAAK;AAAE,QAAA,OAAO,SAAS;AAC5B,IAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CACzB,KAAK,CAAC,gBAAgB,CAAoB,gBAAgB,CAAC,CAC5D;AACD,IAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAE7D,IAAI,GAAG,KAAK,MAAM;AAAE,QAAA,OAAO,OAAO,CAAC,CAAC,CAAC;IACrC,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;AAErD,IAAA,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC;AACvB,IAAA,IAAI,CAAC,IAAI;AAAE,QAAA,OAAO,SAAS;IAC3B,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC;AACpC,IAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE;QAC5C,MAAM,SAAS,GACb,QAAQ,CAAC,CAAC,EAAE,GAAG,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;QAC/D,IAAI,CAAC,SAAS,CAAC,QAAQ;AAAE,YAAA,OAAO,SAAS;IAC3C;AACA,IAAA,OAAO,SAAS;AAClB;AAyEA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;AACH,MAAM,oBAAoB,GAAG,UAAU,CACrC,SAAS,gBAAgB,CACvB,EACE,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,KAAK,GAAG,MAAM,EACd,IAAI,EACJ,SAAS,GAAG,IAAI,EAChB,UAAU,EACV,OAAO,EACP,SAAS,EACT,QAAQ,EACR,GAAG,KAAK,EACT,EACD,GAAG,EAAA;AAEH,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;;AAGlE,IAAA,MAAM,YAAY,GAAG,KAAK,KAAK,SAAS;IACxC,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC;AAChE,IAAA,MAAM,OAAO,GAAG,YAAY,IAAI,KAAK,IAAI,SAAS,IAAI,aAAa;AAEnE,IAAA,MAAM,MAAM,GAAG,CAAC,IAAY,KAAI;QAC9B,IAAI,IAAI,KAAK,OAAO;YAAE;AACtB,QAAA,IAAI,CAAC,YAAY;YAAE,gBAAgB,CAAC,IAAI,CAAC;AACzC,QAAA,QAAQ,GAAG,IAAI,CAAC;AAClB,IAAA,CAAC;;;IAID,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAkB,EAAE,CAAC;AAC7D,IAAA,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,MAAqB,KAAI;AACrD,QAAA,WAAW,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,OAAO,MAAM,WAAW,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,CAAC;IAClE,CAAC,EAAE,EAAE,CAAC;IACN,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,EAAsB;;;;;IAM5D,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,EAAsB;IACtE,qBAAqB,CAAC,MAAK;QACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC;QACtE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC;AACnC,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;;;;;;;;IASxB,MAAM,WAAW,GAAG,CAAC,SAA6B,KAChD,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC;AAC1D,IAAA,IAAI,OAA2B;IAC/B,IAAI,WAAW,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,OAAO;SACtC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC;QAAE,OAAO,GAAG,OAAO;;QACpE,OAAO,GAAG,YAAY;AAE3B,IAAA,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACpE,SAAS,CAAC,MAAK;QACb,IAAI,CAAC,KAAK,EAAE;YACV,QAAQ,CACN,0BAA0B,EAC1B,iEAAiE;gBAC/D,iEAAiE;AACjE,gBAAA,gBAAgB,CACnB;QACH;AACF,IAAA,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AAEX,IAAA,MAAM,UAAU,GAAG,qBAAqB,CAAC,SAAS,EAAE;AAClD,QAAA,YAAY,EAAE,SAAS;AACvB,QAAA,aAAa,EAAE,UAAU;AACzB,QAAA,UAAU,EAAE,OAAO;AACnB,QAAA,CAAC,CAAA,IAAA,EAAO,IAAI,CAAA,CAAE,GAAG,IAAI;AACtB,KAAA,CAAC;;;IAIF,QACEA,IAAC,uBAAuB,CAAC,QAAQ,EAAA,EAC/B,KAAK,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAA,QAAA,EAEhEA,GAAA,CAAA,KAAA,EAAA,EAAA,GACM,IAAI,EACR,GAAG,EAAE,GAAG,EACR,IAAI,EAAC,YAAY,EACjB,SAAS,EAAE,UAAU,CAAC,UAAU,EAAE,kBAAkB,EAAE,SAAS,CAAC,YAE/D,QAAQ,EAAA,CACL,EAAA,CAC2B;AAEvC,CAAC,CACF;AAiFD;;;;;;;AAOG;AACH,MAAM,OAAO,GAAG,UAAU,CACxB,SAAS,OAAO,CACd,EACE,KAAK,EACL,UAAU,EACV,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,EACT,OAAO,EACP,MAAM,EACN,GAAG,KAAK,EACT,EACD,GAAG,EAAA;AAEH,IAAA,MAAM,GAAG,GAAG,UAAU,CAAC,uBAAuB,CAAC;IAC/C,MAAM,OAAO,GAAG,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK;IACrD,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC;;;;IAK3C,MAAM,aAAa,GAAG,qBAAqB,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AAEvE,IAAA,MAAM,OAAO,GAAG,MAAM,CAA2B,IAAI,CAAC;AACtD,IAAA,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,IAA8B,KAAI;AACjC,QAAA,OAAO,CAAC,OAAO,GAAG,IAAI;QACtB,IAAI,OAAO,GAAG,KAAK,UAAU;YAAE,GAAG,CAAC,IAAI,CAAC;AACnC,aAAA,IAAI,GAAG;AAAE,YAAA,GAAG,CAAC,OAAO,GAAG,IAAI;AAClC,IAAA,CAAC,EACD,CAAC,GAAG,CAAC,CACN;AAED,IAAA,MAAM,QAAQ,GAAG,GAAG,EAAE,QAAQ;IAC9B,SAAS,CAAC,MAAK;QACb,IAAI,CAAC,QAAQ,EAAE;YACb,QAAQ,CACN,kCAAkC,EAClC,yDAAyD;AACvD,gBAAA,iEAAiE,CACpE;AACD,YAAA,OAAO,SAAS;QAClB;AACA,QAAA,OAAO,QAAQ,CAAC;YACd,KAAK;AACL,YAAA,QAAQ,EAAE,GAAG;YACb,IAAI,EAAE,OAAO,CAAC,OAA4B;AAC3C,SAAA,CAAC;IACJ,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;;;AAI1B,IAAA,MAAM,WAAW,GAAG,CAAC,KAA0C,KAAI;AACjE,QAAA,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,KAAK,CAAC,gBAAgB;YAAE;AAC5B,QAAA,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC;AACpB,IAAA,CAAC;AAED,IAAA,MAAM,aAAa,GAAG,CAAC,KAA6C,KAAI;AACtE,QAAA,SAAS,GAAG,KAAK,CAAC;AAClB,QAAA,IACE,CAAC,GAAG;AACJ,YAAA,KAAK,CAAC,gBAAgB;AACtB,YAAA,KAAK,CAAC,MAAM;AACZ,YAAA,KAAK,CAAC,OAAO;YACb,KAAK,CAAC,OAAO,EACb;YACA;QACF;AACA,QAAA,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC;AACzD,QAAA,IAAI,CAAC,MAAM;YAAE;QACb,KAAK,CAAC,cAAc,EAAE;;;;QAItB,KAAK,CAAC,eAAe,EAAE;QACvB,MAAM,CAAC,KAAK,EAAE;;;;;QAKd,MAAM,CAAC,KAAK,EAAE;AAChB,IAAA,CAAC;AAED,IAAA,MAAM,WAAW,GAAG,CAAC,KAA0C,KAAI;AACjE,QAAA,OAAO,GAAG,KAAK,CAAC;AAChB,QAAA,GAAG,EAAE,UAAU,CAAC,KAAK,CAAC;AACxB,IAAA,CAAC;;AAGD,IAAA,MAAM,UAAU,GAAG,CAAC,KAA0C,KAAI;AAChE,QAAA,MAAM,GAAG,KAAK,CAAC;AACf,QAAA,GAAG,EAAE,UAAU,CAAC,SAAS,CAAC;AAC5B,IAAA,CAAC;;;;;IAMD,QACEA,IAAC,MAAM,EAAA,EAAA,GACD,KAAK,EACT,GAAG,EAAE,OAAO,EACZ,EAAE,EAAC,QAAQ,EACX,IAAI,EAAC,QAAQ,EACb,IAAI,EAAC,OAAO,EAAA,cAAA,EACE,OAAO,EACrB,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,SAAS,EAC5D,UAAU,EAAE,GAAG,EACf,KAAK,EAAE,OAAO,GAAG,GAAG,CAAC,KAAK,GAAG,SAAS,EACtC,SAAS,EAAE,UAAU,CAAC,aAAa,EAAE,SAAS,CAAC,EAC/C,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,UAAU,EAAA,CAClB;AAEN,CAAC,CACF;AAED,OAAO,CAAC,WAAW,GAAG,0BAA0B;AAEzC,MAAM,gBAAgB,GAAG,iBAAiB,CAC/C,oBAAoB,EACpB,EAAE,OAAO,EAAE,EACX,kBAAkB;;"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
|
|
5
|
+
import { useBulmaClasses } from '../helpers/useBulmaClasses.js';
|
|
6
|
+
import { flattenChildren } from '../helpers/flattenChildren.js';
|
|
7
|
+
import { visuallyHidden } from '../helpers/statusRegion.js';
|
|
8
|
+
import { withSubComponents } from '../helpers/withSubComponents.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Keys pressed together or one after another, in the outer `<kbd>` HTML gives a combination, with the separator between each pair as text.
|
|
12
|
+
*
|
|
13
|
+
* It doesn't wrap in the middle and isn't drawn as a key itself. It sets no
|
|
14
|
+
* font size, so the keys in it are the size of a key on its own.
|
|
15
|
+
*
|
|
16
|
+
* @function
|
|
17
|
+
* @param {KbdGroupProps} props - Props for Kbd.Group.
|
|
18
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the outer `<kbd>`.
|
|
19
|
+
* @returns {JSX.Element} The rendered combination.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* <Kbd.Group>
|
|
23
|
+
* <Kbd>Ctrl</Kbd>
|
|
24
|
+
* <Kbd>Shift</Kbd>
|
|
25
|
+
* <Kbd>P</Kbd>
|
|
26
|
+
* </Kbd.Group>
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <Kbd.Group separator="then">
|
|
30
|
+
* <Kbd>G</Kbd>
|
|
31
|
+
* <Kbd>I</Kbd>
|
|
32
|
+
* </Kbd.Group>
|
|
33
|
+
*/
|
|
34
|
+
const KbdGroup = forwardRef(({ className, textColor, separator = '+', children, ...props }, ref) => {
|
|
35
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
36
|
+
color: textColor,
|
|
37
|
+
...props,
|
|
38
|
+
});
|
|
39
|
+
const groupClass = usePrefixedClassNames('kbd-group');
|
|
40
|
+
const separatorClass = usePrefixedClassNames('kbd-separator');
|
|
41
|
+
// Null, undefined and booleans drop out, and Fragments are opened up, so
|
|
42
|
+
// an empty Fragment leaves no separator behind and the keys in one are
|
|
43
|
+
// separated like the rest. The spaces are part of the text, so a screen
|
|
44
|
+
// reader reads "Ctrl plus S" rather than one run-together word, whatever
|
|
45
|
+
// the CSS does.
|
|
46
|
+
const keys = flattenChildren(children);
|
|
47
|
+
return (jsx("kbd", { ref: ref, className: classNames(groupClass, bulmaHelperClasses, className), ...rest, children: keys.map((key, index) => (jsxs(React.Fragment, { children: [index > 0 && (jsx("span", { className: separatorClass, children: ` ${separator} ` })), key] }, React.isValidElement(key) && key.key != null ? key.key : index))) }));
|
|
48
|
+
});
|
|
49
|
+
KbdGroup.displayName = 'Kbd.Group';
|
|
50
|
+
/**
|
|
51
|
+
* The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.
|
|
52
|
+
*
|
|
53
|
+
* It only shows the key: it doesn't listen for it. The key takes its colors
|
|
54
|
+
* from the text it sits in, and its size in `em`, so it fits a paragraph, a
|
|
55
|
+
* title or a filled button as it is, and `textSize` sizes it outright. For a
|
|
56
|
+
* combination or a sequence, put the keys in a `Kbd.Group`. What tells
|
|
57
|
+
* assistive tech a control has a shortcut is `aria-keyshortcuts` on that
|
|
58
|
+
* control, with `Kbd` as the hint people see next to it.
|
|
59
|
+
*
|
|
60
|
+
* @function
|
|
61
|
+
* @param {KbdProps} props - Props for the Kbd component.
|
|
62
|
+
* @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<kbd>`.
|
|
63
|
+
* @returns {JSX.Element} The rendered key.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* <Kbd>Esc</Kbd>
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* <Kbd label="Command">⌘</Kbd>
|
|
70
|
+
*/
|
|
71
|
+
const KbdRoot = forwardRef(({ className, textColor, bgColor, label, children, ...props }, ref) => {
|
|
72
|
+
const { bulmaHelperClasses, rest } = useBulmaClasses({
|
|
73
|
+
color: textColor,
|
|
74
|
+
backgroundColor: bgColor,
|
|
75
|
+
...props,
|
|
76
|
+
});
|
|
77
|
+
const kbdClass = usePrefixedClassNames('kbd');
|
|
78
|
+
// The name is text rather than an aria-label, which ARIA in HTML
|
|
79
|
+
// prohibits on kbd. Its styles are inline, so it stays hidden in a
|
|
80
|
+
// modular build that leaves out Bulma's helper classes.
|
|
81
|
+
return (jsx("kbd", { ref: ref, className: classNames(kbdClass, bulmaHelperClasses, className), ...rest, children: label ? (jsxs(Fragment, { children: [jsx("span", { "aria-hidden": "true", children: children }), jsx("span", { style: visuallyHidden, children: label })] })) : (children) }));
|
|
82
|
+
});
|
|
83
|
+
const Kbd = withSubComponents(KbdRoot, { Group: KbdGroup }, 'Kbd');
|
|
84
|
+
|
|
85
|
+
export { Kbd, KbdGroup };
|
|
86
|
+
//# sourceMappingURL=Kbd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Kbd.js","sources":["../../../src/elements/Kbd.tsx"],"sourcesContent":["import React, { forwardRef } from 'react';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\nimport {\n useBulmaClasses,\n BulmaClassesProps,\n validColors,\n} from '../helpers/useBulmaClasses';\nimport { flattenChildren } from '../helpers/flattenChildren';\nimport { visuallyHidden } from '../helpers/statusRegion';\nimport { withSubComponents } from '../helpers/withSubComponents';\n\n/**\n * Props for the Kbd component. The helper props, the other attributes and\n * the `ref` land on the `<kbd>`.\n *\n * @extraProp {React.Ref<HTMLElement>} [ref] - Ref forwarded to the `<kbd>`.\n */\nexport interface KbdProps\n extends\n Omit<React.HTMLAttributes<HTMLElement>, 'color'>,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** Additional CSS classes to apply. */\n className?: string;\n /**\n * The key as it's printed on the keyboard: a word (`Ctrl`, `Esc`), a\n * letter, or a symbol (`⌘`).\n */\n children?: React.ReactNode;\n /**\n * What a screen reader says in place of a symbol, in your app's language:\n * `label=\"Command\"` on `⌘`. The symbol is hidden from assistive tech with\n * `aria-hidden`, and the label is read in its place, from text that's\n * visually hidden. Use it for a glyph that isn't a word, such as `⌘`, `⌥`,\n * `⇧`, `⌃` or `⏎`, and leave it off a key written as a word. Copying the\n * key copies the label along with the glyph. Don't reach for `aria-label`\n * instead: ARIA in HTML prohibits naming a `<kbd>`, so nothing obliges a\n * screen reader to read it.\n */\n label?: string;\n /**\n * Text color helper. The key's edge and fill are mixed from its text\n * color, so they follow it.\n */\n textColor?: (typeof validColors)[number] | 'inherit' | 'current';\n /**\n * Background color helper. It replaces the key's faint fill, and the text\n * still follows the text around it, which turns light in dark mode, so pair\n * it with a `textColor` that reads on that color in both schemes, such as\n * `textColor=\"black\"` on `bgColor=\"warning\"`.\n */\n bgColor?: (typeof validColors)[number] | 'inherit' | 'current';\n}\n\n/**\n * Props for Kbd.Group, keys pressed together or one after another. The\n * group renders the outer `<kbd>` of a combination, and the helper props,\n * the other attributes and the `ref` land on it.\n *\n * @extraProp {React.Ref<HTMLElement>} [ref] - Ref forwarded to the outer `<kbd>`.\n */\nexport interface KbdGroupProps\n extends\n Omit<React.HTMLAttributes<HTMLElement>, 'color'>,\n Omit<BulmaClassesProps, 'color' | 'backgroundColor'> {\n /** Additional CSS classes to apply. */\n className?: string;\n /**\n * The keys, a `Kbd` each, in the order they're pressed. The group renders\n * the outer `<kbd>` around them, so it doesn't go inside a `Kbd` of its\n * own, and it puts the separator between them, so don't type one: a `+`\n * of your own doubles up. A `null` or `false` child, or an empty Fragment,\n * gets no separator, and the keys in a Fragment are separated like the\n * rest. A component that renders nothing still gets one, since the group\n * can't see what a child renders, so write a conditional key inline\n * (`{isMac && <Kbd label=\"Command\">⌘</Kbd>}`), not in a component of its\n * own.\n */\n children?: React.ReactNode;\n /**\n * What goes between the keys, shown and read aloud, with a space either\n * side. Use `'then'` for a sequence pressed one key after another. It's a\n * string because a screen reader has to read it.\n */\n separator?: string;\n /** Text color helper. The keys and separators inside follow it. */\n textColor?: (typeof validColors)[number] | 'inherit' | 'current';\n}\n\n/**\n * Keys pressed together or one after another, in the outer `<kbd>` HTML gives a combination, with the separator between each pair as text.\n *\n * It doesn't wrap in the middle and isn't drawn as a key itself. It sets no\n * font size, so the keys in it are the size of a key on its own.\n *\n * @function\n * @param {KbdGroupProps} props - Props for Kbd.Group.\n * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the outer `<kbd>`.\n * @returns {JSX.Element} The rendered combination.\n *\n * @example\n * <Kbd.Group>\n * <Kbd>Ctrl</Kbd>\n * <Kbd>Shift</Kbd>\n * <Kbd>P</Kbd>\n * </Kbd.Group>\n *\n * @example\n * <Kbd.Group separator=\"then\">\n * <Kbd>G</Kbd>\n * <Kbd>I</Kbd>\n * </Kbd.Group>\n */\nexport const KbdGroup = forwardRef<HTMLElement, KbdGroupProps>(\n ({ className, textColor, separator = '+', children, ...props }, ref) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses({\n color: textColor,\n ...props,\n });\n const groupClass = usePrefixedClassNames('kbd-group');\n const separatorClass = usePrefixedClassNames('kbd-separator');\n\n // Null, undefined and booleans drop out, and Fragments are opened up, so\n // an empty Fragment leaves no separator behind and the keys in one are\n // separated like the rest. The spaces are part of the text, so a screen\n // reader reads \"Ctrl plus S\" rather than one run-together word, whatever\n // the CSS does.\n const keys = flattenChildren(children);\n\n return (\n <kbd\n ref={ref}\n className={classNames(groupClass, bulmaHelperClasses, className)}\n {...rest}\n >\n {keys.map((key, index) => (\n // Each wrapper takes its child's own React key, so a child that\n // moves keeps its node. Text has none, so it goes by its place.\n <React.Fragment\n key={React.isValidElement(key) && key.key != null ? key.key : index}\n >\n {index > 0 && (\n <span className={separatorClass}>{` ${separator} `}</span>\n )}\n {key}\n </React.Fragment>\n ))}\n </kbd>\n );\n }\n);\n\nKbdGroup.displayName = 'Kbd.Group';\n\n/**\n * The `Kbd` component renders a `<kbd>` drawn as a keycap, for a key the reader presses.\n *\n * It only shows the key: it doesn't listen for it. The key takes its colors\n * from the text it sits in, and its size in `em`, so it fits a paragraph, a\n * title or a filled button as it is, and `textSize` sizes it outright. For a\n * combination or a sequence, put the keys in a `Kbd.Group`. What tells\n * assistive tech a control has a shortcut is `aria-keyshortcuts` on that\n * control, with `Kbd` as the hint people see next to it.\n *\n * @function\n * @param {KbdProps} props - Props for the Kbd component.\n * @param {React.Ref<HTMLElement>} ref - Forwarded ref to the `<kbd>`.\n * @returns {JSX.Element} The rendered key.\n *\n * @example\n * <Kbd>Esc</Kbd>\n *\n * @example\n * <Kbd label=\"Command\">⌘</Kbd>\n */\nconst KbdRoot = forwardRef<HTMLElement, KbdProps>(\n ({ className, textColor, bgColor, label, children, ...props }, ref) => {\n const { bulmaHelperClasses, rest } = useBulmaClasses({\n color: textColor,\n backgroundColor: bgColor,\n ...props,\n });\n const kbdClass = usePrefixedClassNames('kbd');\n\n // The name is text rather than an aria-label, which ARIA in HTML\n // prohibits on kbd. Its styles are inline, so it stays hidden in a\n // modular build that leaves out Bulma's helper classes.\n return (\n <kbd\n ref={ref}\n className={classNames(kbdClass, bulmaHelperClasses, className)}\n {...rest}\n >\n {label ? (\n <>\n <span aria-hidden=\"true\">{children}</span>\n <span style={visuallyHidden}>{label}</span>\n </>\n ) : (\n children\n )}\n </kbd>\n );\n }\n);\n\nexport const Kbd = withSubComponents(KbdRoot, { Group: KbdGroup }, 'Kbd');\n\nexport default Kbd;\n"],"names":["_jsx","_jsxs"],"mappings":";;;;;;;;;AAwFA;;;;;;;;;;;;;;;;;;;;;;;AAuBG;AACI,MAAM,QAAQ,GAAG,UAAU,CAChC,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,KAAI;AACrE,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC;AACnD,QAAA,KAAK,EAAE,SAAS;AAChB,QAAA,GAAG,KAAK;AACT,KAAA,CAAC;AACF,IAAA,MAAM,UAAU,GAAG,qBAAqB,CAAC,WAAW,CAAC;AACrD,IAAA,MAAM,cAAc,GAAG,qBAAqB,CAAC,eAAe,CAAC;;;;;;AAO7D,IAAA,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC;AAEtC,IAAA,QACEA,GAAA,CAAA,KAAA,EAAA,EACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,UAAU,CAAC,UAAU,EAAE,kBAAkB,EAAE,SAAS,CAAC,KAC5D,IAAI,EAAA,QAAA,EAEP,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,MAGnBC,KAAC,KAAK,CAAC,QAAQ,EAAA,EAAA,QAAA,EAAA,CAGZ,KAAK,GAAG,CAAC,KACRD,GAAA,CAAA,MAAA,EAAA,EAAM,SAAS,EAAE,cAAc,EAAA,QAAA,EAAG,CAAA,CAAA,EAAI,SAAS,CAAA,CAAA,CAAG,EAAA,CAAQ,CAC3D,EACA,GAAG,CAAA,EAAA,EALC,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,KAAK,CAMpD,CAClB,CAAC,EAAA,CACE;AAEV,CAAC;AAGH,QAAQ,CAAC,WAAW,GAAG,WAAW;AAElC;;;;;;;;;;;;;;;;;;;;AAoBG;AACH,MAAM,OAAO,GAAG,UAAU,CACxB,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,KAAI;AACpE,IAAA,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,GAAG,eAAe,CAAC;AACnD,QAAA,KAAK,EAAE,SAAS;AAChB,QAAA,eAAe,EAAE,OAAO;AACxB,QAAA,GAAG,KAAK;AACT,KAAA,CAAC;AACF,IAAA,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC;;;;IAK7C,QACEA,aACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,UAAU,CAAC,QAAQ,EAAE,kBAAkB,EAAE,SAAS,CAAC,EAAA,GAC1D,IAAI,EAAA,QAAA,EAEP,KAAK,IACJC,4BACED,GAAA,CAAA,MAAA,EAAA,EAAA,aAAA,EAAkB,MAAM,YAAE,QAAQ,EAAA,CAAQ,EAC1CA,GAAA,CAAA,MAAA,EAAA,EAAM,KAAK,EAAE,cAAc,EAAA,QAAA,EAAG,KAAK,GAAQ,CAAA,EAAA,CAC1C,KAEH,QAAQ,CACT,EAAA,CACG;AAEV,CAAC,CACF;AAEM,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK;;"}
|
|
@@ -11,7 +11,8 @@ import { usePrefixedClassNames, classNames } from '../helpers/classNames.js';
|
|
|
11
11
|
* carries no form value: a setting saved with a form is a `Checkbox` or a
|
|
12
12
|
* `Switch`. A set of toggles that each turn on and off on their own goes in
|
|
13
13
|
* `Buttons hasAddons` with `role="group"` and an `aria-label`. A set where only
|
|
14
|
-
* one can be on is a radio group rather than a row of these: use
|
|
14
|
+
* one can be on is a radio group rather than a row of these: use
|
|
15
|
+
* `SegmentedControl`, or `Radios` for a choice that's sent with a form.
|
|
15
16
|
*
|
|
16
17
|
* @function
|
|
17
18
|
* @param {ToggleButtonProps} props - Props for the ToggleButton component.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToggleButton.js","sources":["../../../src/elements/ToggleButton.tsx"],"sourcesContent":["import React, { forwardRef, useState } from 'react';\nimport { Button, type ButtonOwnProps } from './Button';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\n\n// Spelled out rather than as an `Exclude` of Button's colors so the API page\n// can list the values. The tests hold it to that `Exclude`, so a color Button\n// gains fails there until it's added here or left out on purpose.\n/**\n * The fills a pressed `ToggleButton` takes: `Button`'s colors, minus `text`\n * and `ghost`, which don't fill, and `white`, `light`, `dark` and `black`.\n * Bulma's color modifier fills the button and drops its border, and those four\n * fill it with about the lightness a plain button already has in one theme or\n * the other, so pressed would look like off without its border.\n */\nexport type ToggleButtonColor =\n 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n\n/**\n * Props for the ToggleButton component. The helper props, the other button\n * attributes and the `ref` all land on the `<button>`.\n *\n * Some of `Button`'s props are left out on purpose. `as`, since only a button\n * can be pressed, and `type`, which is always `button`. `isActive`, which is\n * `pressed` here, and `isStatic`, a toggle you couldn't press. And the props\n * that change the fill (`isLight`, `isOutlined`, `isInverted`, `textColor`,\n * `bgColor` and their shades), since each would color the button in both\n * states or turn \"on\" into a faint tint. `color` takes only the fills that\n * stand out from a plain button: see `ToggleButtonColor`.\n *\n * @extraProp {React.Ref<HTMLButtonElement>} [ref] - Ref forwarded to the `<button>`.\n */\nexport interface ToggleButtonProps\n extends\n Omit<\n ButtonOwnProps,\n | 'color'\n | 'isActive'\n | 'isStatic'\n | 'isLight'\n | 'isOutlined'\n | 'isInverted'\n | 'textColor'\n | 'bgColor'\n | 'colorShade'\n | 'backgroundColorShade'\n >,\n Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ButtonOwnProps | 'type' | 'aria-pressed'\n > {\n /**\n * Whether it's pressed, when you hold the state. Setting it makes the toggle\n * controlled: a click only calls `onPressedChange`, and nothing changes until\n * you pass the new value. Pick this or `defaultPressed` for the button's\n * lifetime.\n */\n pressed?: boolean;\n /** Whether it starts pressed, when it holds its own state. */\n defaultPressed?: boolean;\n /**\n * Called with the new state each time it's toggled, whether or not\n * `pressed` is set. An `onClick` that calls `preventDefault()` stops the\n * toggle, and this doesn't fire.\n */\n onPressedChange?: (pressed: boolean) => void;\n /**\n * The fill while it's pressed. Not pressed, it's a plain button, so the fill\n * is the difference people see. `white`, `light`, `dark` and `black` aren't\n * offered, since in one theme or the other each fills about as light or dark\n * as a plain button already is. Of the rest, a pale one such as `warning`\n * changes less than the default `link`.\n */\n color?: ToggleButtonColor;\n /**\n * The label, which stays the same in both states: a screen reader announces\n * whether it's pressed, so \"Mute\" stays \"Mute\". An icon can swap, but an\n * icon-only toggle needs an `aria-label` that doesn't. A label that changes\n * (\"Play\", then \"Pause\") belongs on a plain `Button`.\n */\n children?: React.ReactNode;\n /**\n * Shows Bulma's spinner in place of the label and holds the toggle where it\n * is: a click, Enter or Space does nothing, so neither `onClick` nor\n * `onPressedChange` fires, until it's cleared. Set it while a change is\n * saving. The button carries `aria-busy` and `aria-disabled` meanwhile, and\n * stays focusable, so focus isn't lost mid-save. Under\n * `prefers-reduced-motion: reduce` the spinner stops and stays drawn (with\n * bestax's CSS loaded).\n */\n isLoading?: boolean;\n /**\n * Not accepted: `aria-pressed` follows the state. Declared because JSX lets a\n * hyphenated attribute through when the props don't name it, so leaving it\n * out of the type alone would not reject `aria-pressed=\"mixed\"`.\n * @internal\n */\n 'aria-pressed'?: never;\n}\n\n/**\n * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.\n *\n * It's always a `<button type=\"button\">`, so it never submits a form and\n * carries no form value: a setting saved with a form is a `Checkbox` or a\n * `Switch`. A set of toggles that each turn on and off on their own goes in\n * `Buttons hasAddons` with `role=\"group\"` and an `aria-label`. A set where only\n * one can be on is a radio group rather than a row of these: use `Radios
|
|
1
|
+
{"version":3,"file":"ToggleButton.js","sources":["../../../src/elements/ToggleButton.tsx"],"sourcesContent":["import React, { forwardRef, useState } from 'react';\nimport { Button, type ButtonOwnProps } from './Button';\nimport { classNames, usePrefixedClassNames } from '../helpers/classNames';\n\n// Spelled out rather than as an `Exclude` of Button's colors so the API page\n// can list the values. The tests hold it to that `Exclude`, so a color Button\n// gains fails there until it's added here or left out on purpose.\n/**\n * The fills a pressed `ToggleButton` takes: `Button`'s colors, minus `text`\n * and `ghost`, which don't fill, and `white`, `light`, `dark` and `black`.\n * Bulma's color modifier fills the button and drops its border, and those four\n * fill it with about the lightness a plain button already has in one theme or\n * the other, so pressed would look like off without its border.\n */\nexport type ToggleButtonColor =\n 'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger';\n\n/**\n * Props for the ToggleButton component. The helper props, the other button\n * attributes and the `ref` all land on the `<button>`.\n *\n * Some of `Button`'s props are left out on purpose. `as`, since only a button\n * can be pressed, and `type`, which is always `button`. `isActive`, which is\n * `pressed` here, and `isStatic`, a toggle you couldn't press. And the props\n * that change the fill (`isLight`, `isOutlined`, `isInverted`, `textColor`,\n * `bgColor` and their shades), since each would color the button in both\n * states or turn \"on\" into a faint tint. `color` takes only the fills that\n * stand out from a plain button: see `ToggleButtonColor`.\n *\n * @extraProp {React.Ref<HTMLButtonElement>} [ref] - Ref forwarded to the `<button>`.\n */\nexport interface ToggleButtonProps\n extends\n Omit<\n ButtonOwnProps,\n | 'color'\n | 'isActive'\n | 'isStatic'\n | 'isLight'\n | 'isOutlined'\n | 'isInverted'\n | 'textColor'\n | 'bgColor'\n | 'colorShade'\n | 'backgroundColorShade'\n >,\n Omit<\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n keyof ButtonOwnProps | 'type' | 'aria-pressed'\n > {\n /**\n * Whether it's pressed, when you hold the state. Setting it makes the toggle\n * controlled: a click only calls `onPressedChange`, and nothing changes until\n * you pass the new value. Pick this or `defaultPressed` for the button's\n * lifetime.\n */\n pressed?: boolean;\n /** Whether it starts pressed, when it holds its own state. */\n defaultPressed?: boolean;\n /**\n * Called with the new state each time it's toggled, whether or not\n * `pressed` is set. An `onClick` that calls `preventDefault()` stops the\n * toggle, and this doesn't fire.\n */\n onPressedChange?: (pressed: boolean) => void;\n /**\n * The fill while it's pressed. Not pressed, it's a plain button, so the fill\n * is the difference people see. `white`, `light`, `dark` and `black` aren't\n * offered, since in one theme or the other each fills about as light or dark\n * as a plain button already is. Of the rest, a pale one such as `warning`\n * changes less than the default `link`.\n */\n color?: ToggleButtonColor;\n /**\n * The label, which stays the same in both states: a screen reader announces\n * whether it's pressed, so \"Mute\" stays \"Mute\". An icon can swap, but an\n * icon-only toggle needs an `aria-label` that doesn't. A label that changes\n * (\"Play\", then \"Pause\") belongs on a plain `Button`.\n */\n children?: React.ReactNode;\n /**\n * Shows Bulma's spinner in place of the label and holds the toggle where it\n * is: a click, Enter or Space does nothing, so neither `onClick` nor\n * `onPressedChange` fires, until it's cleared. Set it while a change is\n * saving. The button carries `aria-busy` and `aria-disabled` meanwhile, and\n * stays focusable, so focus isn't lost mid-save. Under\n * `prefers-reduced-motion: reduce` the spinner stops and stays drawn (with\n * bestax's CSS loaded).\n */\n isLoading?: boolean;\n /**\n * Not accepted: `aria-pressed` follows the state. Declared because JSX lets a\n * hyphenated attribute through when the props don't name it, so leaving it\n * out of the type alone would not reject `aria-pressed=\"mixed\"`.\n * @internal\n */\n 'aria-pressed'?: never;\n}\n\n/**\n * The `ToggleButton` component renders a button that stays pressed until it's pressed again, and says whether it's on through `aria-pressed`.\n *\n * It's always a `<button type=\"button\">`, so it never submits a form and\n * carries no form value: a setting saved with a form is a `Checkbox` or a\n * `Switch`. A set of toggles that each turn on and off on their own goes in\n * `Buttons hasAddons` with `role=\"group\"` and an `aria-label`. A set where only\n * one can be on is a radio group rather than a row of these: use\n * `SegmentedControl`, or `Radios` for a choice that's sent with a form.\n *\n * @function\n * @param {ToggleButtonProps} props - Props for the ToggleButton component.\n * @param {React.Ref<HTMLButtonElement>} ref - Forwarded ref to the `<button>`.\n * @returns {JSX.Element} The rendered toggle button.\n * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/button/ | APG button pattern}\n *\n * @example\n * <ToggleButton onPressedChange={setMuted}>Mute</ToggleButton>\n *\n * @example\n * <Buttons hasAddons role=\"group\" aria-label=\"Text style\">\n * <ToggleButton aria-label=\"Bold\" pressed={bold} onPressedChange={setBold}>\n * <Icon name=\"bold\" />\n * </ToggleButton>\n * </Buttons>\n */\nexport const ToggleButton = forwardRef<HTMLButtonElement, ToggleButtonProps>(\n function ToggleButton(\n {\n pressed: controlledPressed,\n defaultPressed = false,\n onPressedChange,\n color = 'link',\n isLoading,\n 'aria-busy': ariaBusy,\n 'aria-disabled': ariaDisabled,\n className,\n onClick,\n ...props\n },\n ref\n ) {\n const isControlled = controlledPressed !== undefined;\n const [internalPressed, setInternalPressed] = useState(defaultPressed);\n const pressed = controlledPressed ?? internalPressed;\n\n // Prefixed like Button's own classes, which a hand-written\n // `className=\"is-selected\"` could not be under ConfigProvider classPrefix.\n // Bulma lifts `is-selected` above its neighbours in `.buttons.has-addons`.\n const selectedClass = usePrefixedClassNames({ 'is-selected': pressed });\n\n // The caller's onClick runs first, so a `preventDefault()` there vetoes\n // the toggle, as it does in Radix, MUI and Zag. Enter and Space reach here\n // through the button's own click, and a disabled button gets no click.\n //\n // A loading one gets the click but drops it. Bulma's spinner only stops a\n // pointer, so without this a key flipped a toggle the mouse couldn't.\n // `disabled` would stop both, but it takes focus away mid-save.\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n if (isLoading) return;\n onClick?.(event);\n if (event.defaultPrevented) return;\n const next = !pressed;\n onPressedChange?.(next);\n if (!isControlled) setInternalPressed(next);\n };\n\n // The props are spread first, so what a toggle depends on wins over\n // anything a JavaScript caller or a spread object passes along: it stays\n // a button, never submits, reports the state it's in, and says it's busy\n // and won't respond while it's loading. A caller's own `aria-busy` or\n // `aria-disabled` stays when it isn't loading.\n return (\n <Button\n {...props}\n ref={ref}\n as=\"button\"\n type=\"button\"\n isLoading={isLoading}\n aria-busy={isLoading || ariaBusy}\n aria-disabled={isLoading || ariaDisabled}\n aria-pressed={pressed}\n color={pressed ? color : undefined}\n className={classNames(selectedClass, className)}\n onClick={handleClick}\n />\n );\n }\n);\n\nToggleButton.displayName = 'ToggleButton';\n\nexport default ToggleButton;\n"],"names":["_jsx"],"mappings":";;;;;;AAmGA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBG;AACI,MAAM,YAAY,GAAG,UAAU,CACpC,SAAS,YAAY,CACnB,EACE,OAAO,EAAE,iBAAiB,EAC1B,cAAc,GAAG,KAAK,EACtB,eAAe,EACf,KAAK,GAAG,MAAM,EACd,SAAS,EACT,WAAW,EAAE,QAAQ,EACrB,eAAe,EAAE,YAAY,EAC7B,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EACD,GAAG,EAAA;AAEH,IAAA,MAAM,YAAY,GAAG,iBAAiB,KAAK,SAAS;IACpD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,cAAc,CAAC;AACtE,IAAA,MAAM,OAAO,GAAG,iBAAiB,IAAI,eAAe;;;;IAKpD,MAAM,aAAa,GAAG,qBAAqB,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;;;;;;;;AASvE,IAAA,MAAM,WAAW,GAAG,CAAC,KAA0C,KAAI;AACjE,QAAA,IAAI,SAAS;YAAE;AACf,QAAA,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,KAAK,CAAC,gBAAgB;YAAE;AAC5B,QAAA,MAAM,IAAI,GAAG,CAAC,OAAO;AACrB,QAAA,eAAe,GAAG,IAAI,CAAC;AACvB,QAAA,IAAI,CAAC,YAAY;YAAE,kBAAkB,CAAC,IAAI,CAAC;AAC7C,IAAA,CAAC;;;;;;IAOD,QACEA,GAAA,CAAC,MAAM,EAAA,EAAA,GACD,KAAK,EACT,GAAG,EAAE,GAAG,EACR,EAAE,EAAC,QAAQ,EACX,IAAI,EAAC,QAAQ,EACb,SAAS,EAAE,SAAS,EAAA,WAAA,EACT,SAAS,IAAI,QAAQ,EAAA,eAAA,EACjB,SAAS,IAAI,YAAY,kBAC1B,OAAO,EACrB,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,EAClC,SAAS,EAAE,UAAU,CAAC,aAAa,EAAE,SAAS,CAAC,EAC/C,OAAO,EAAE,WAAW,EAAA,CACpB;AAEN,CAAC;AAGH,YAAY,CAAC,WAAW,GAAG,cAAc;;"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
// INTERNAL: deliberately not exported from src/index.ts.
|
|
5
|
+
/**
|
|
6
|
+
* A component's children as a flat list, with each Fragment opened up
|
|
7
|
+
* (recursively) so the items inside one count one by one rather than as the
|
|
8
|
+
* Fragment. `Children.toArray` already drops `null`, `undefined` and
|
|
9
|
+
* booleans and flattens arrays, but it keeps a Fragment whole, so an empty
|
|
10
|
+
* one would still count as an item and two items in one would count as one.
|
|
11
|
+
*
|
|
12
|
+
* `Children.toArray` keys restart at ".0" inside each Fragment, so the items
|
|
13
|
+
* in one are re-keyed with the Fragment's own key as a prefix. Otherwise an
|
|
14
|
+
* item in a Fragment and a sibling outside it could share a key.
|
|
15
|
+
*
|
|
16
|
+
* Text and numbers pass through as they are, so a caller that wants only
|
|
17
|
+
* elements filters with `React.isValidElement`.
|
|
18
|
+
*/
|
|
19
|
+
function flattenChildren(children, keyPrefix = '') {
|
|
20
|
+
return React.Children.toArray(children).flatMap(child => {
|
|
21
|
+
if (!React.isValidElement(child))
|
|
22
|
+
return [child];
|
|
23
|
+
if (child.type === React.Fragment) {
|
|
24
|
+
return flattenChildren(child.props.children, keyPrefix + child.key);
|
|
25
|
+
}
|
|
26
|
+
return [
|
|
27
|
+
keyPrefix
|
|
28
|
+
? React.cloneElement(child, { key: keyPrefix + child.key })
|
|
29
|
+
: child,
|
|
30
|
+
];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { flattenChildren };
|
|
35
|
+
//# sourceMappingURL=flattenChildren.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flattenChildren.js","sources":["../../../src/helpers/flattenChildren.ts"],"sourcesContent":["// INTERNAL: deliberately not exported from src/index.ts.\nimport React from 'react';\n\n/**\n * A component's children as a flat list, with each Fragment opened up\n * (recursively) so the items inside one count one by one rather than as the\n * Fragment. `Children.toArray` already drops `null`, `undefined` and\n * booleans and flattens arrays, but it keeps a Fragment whole, so an empty\n * one would still count as an item and two items in one would count as one.\n *\n * `Children.toArray` keys restart at \".0\" inside each Fragment, so the items\n * in one are re-keyed with the Fragment's own key as a prefix. Otherwise an\n * item in a Fragment and a sibling outside it could share a key.\n *\n * Text and numbers pass through as they are, so a caller that wants only\n * elements filters with `React.isValidElement`.\n */\nexport function flattenChildren(\n children: React.ReactNode,\n keyPrefix = ''\n): React.ReactNode[] {\n return React.Children.toArray(children).flatMap(child => {\n if (!React.isValidElement(child)) return [child];\n if (child.type === React.Fragment) {\n return flattenChildren(\n (child.props as { children?: React.ReactNode }).children,\n keyPrefix + child.key\n );\n }\n return [\n keyPrefix\n ? React.cloneElement(child, { key: keyPrefix + child.key })\n : child,\n ];\n });\n}\n"],"names":[],"mappings":";;;AAAA;AAGA;;;;;;;;;;;;;AAaG;SACa,eAAe,CAC7B,QAAyB,EACzB,SAAS,GAAG,EAAE,EAAA;AAEd,IAAA,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,IAAG;AACtD,QAAA,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,KAAK,CAAC;QAChD,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,QAAQ,EAAE;AACjC,YAAA,OAAO,eAAe,CACnB,KAAK,CAAC,KAAwC,CAAC,QAAQ,EACxD,SAAS,GAAG,KAAK,CAAC,GAAG,CACtB;QACH;QACA,OAAO;YACL;AACE,kBAAE,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,EAAE;AAC1D,kBAAE,KAAK;SACV;AACH,IAAA,CAAC,CAAC;AACJ;;"}
|