@allxsmith/bestax-bulma 5.45.1 → 5.47.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.
Files changed (42) hide show
  1. package/dist/esm/components/TreeView.js +5 -2
  2. package/dist/esm/components/TreeView.js.map +1 -1
  3. package/dist/esm/form/Checkbox.js.map +1 -1
  4. package/dist/esm/form/File.js.map +1 -1
  5. package/dist/esm/form/Numberinput.js.map +1 -1
  6. package/dist/esm/form/PinInput.js.map +1 -1
  7. package/dist/esm/form/Radio.js.map +1 -1
  8. package/dist/esm/form/SelectBase.js.map +1 -1
  9. package/dist/esm/form/Slider.js.map +1 -1
  10. package/dist/esm/form/Switch.js.map +1 -1
  11. package/dist/esm/helpers/Theme.js +191 -49
  12. package/dist/esm/helpers/Theme.js.map +1 -1
  13. package/dist/esm/helpers/useTreeView.js +64 -9
  14. package/dist/esm/helpers/useTreeView.js.map +1 -1
  15. package/dist/esm/helpers/useVisibilityClasses.js.map +1 -1
  16. package/dist/index.cjs +258 -59
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/types/components/TreeView.d.ts +2 -0
  19. package/dist/types/form/Checkbox.d.ts +3 -0
  20. package/dist/types/form/File.d.ts +3 -0
  21. package/dist/types/form/Numberinput.d.ts +2 -0
  22. package/dist/types/form/PinInput.d.ts +3 -0
  23. package/dist/types/form/Radio.d.ts +3 -0
  24. package/dist/types/form/SelectBase.d.ts +3 -0
  25. package/dist/types/form/Slider.d.ts +3 -0
  26. package/dist/types/form/Switch.d.ts +2 -0
  27. package/dist/types/helpers/Theme.d.ts +37 -10
  28. package/dist/types/helpers/useTreeView.d.ts +19 -3
  29. package/dist/types/helpers/useVisibilityClasses.d.ts +5 -1
  30. package/dist/types-cjs/components/TreeView.d.ts +2 -0
  31. package/dist/types-cjs/form/Checkbox.d.ts +3 -0
  32. package/dist/types-cjs/form/File.d.ts +3 -0
  33. package/dist/types-cjs/form/Numberinput.d.ts +2 -0
  34. package/dist/types-cjs/form/PinInput.d.ts +3 -0
  35. package/dist/types-cjs/form/Radio.d.ts +3 -0
  36. package/dist/types-cjs/form/SelectBase.d.ts +3 -0
  37. package/dist/types-cjs/form/Slider.d.ts +3 -0
  38. package/dist/types-cjs/form/Switch.d.ts +2 -0
  39. package/dist/types-cjs/helpers/Theme.d.ts +37 -10
  40. package/dist/types-cjs/helpers/useTreeView.d.ts +19 -3
  41. package/dist/types-cjs/helpers/useVisibilityClasses.d.ts +5 -1
  42. package/package.json +2 -1
package/dist/index.cjs CHANGED
@@ -10138,6 +10138,38 @@ function parentItemOf(element) {
10138
10138
  const group = element.parentElement;
10139
10139
  return group.getAttribute('role') === 'group' ? group.parentElement : null;
10140
10140
  }
10141
+ /** No positions: on the server, and until the tree has mounted. */
10142
+ const NO_POSITIONS = new Map();
10143
+ /**
10144
+ * Each item's place among the items beside it, by its key, read off the
10145
+ * items on the page: the ones in the same group, or at the top. A loading or
10146
+ * failed load's row counts, as it does for a browser that works positions
10147
+ * out for itself, so the declared ones say what a browser would.
10148
+ */
10149
+ function positionsOf(shown, { values }) {
10150
+ const sets = new Map();
10151
+ shown.forEach(element => {
10152
+ const set = sets.get(element.parentElement);
10153
+ if (set)
10154
+ set.push(element);
10155
+ else
10156
+ sets.set(element.parentElement, [element]);
10157
+ });
10158
+ const positions = new Map();
10159
+ sets.forEach(set => set.forEach((element, at) => positions.set(values.get(element), {
10160
+ posinset: at + 1,
10161
+ setsize: set.length,
10162
+ })));
10163
+ return positions;
10164
+ }
10165
+ /** Whether two sets of positions say the same for every item. */
10166
+ function samePositions(a, b) {
10167
+ return (a.size === b.size &&
10168
+ Array.from(a).every(([key, { posinset, setsize }]) => {
10169
+ const other = b.get(key);
10170
+ return other?.posinset === posinset && other.setsize === setsize;
10171
+ }));
10172
+ }
10141
10173
  /** The values of the items an item on the page is in, from the top. */
10142
10174
  function pathOf(element, { values }) {
10143
10175
  const path = [];
@@ -10232,9 +10264,11 @@ function useControlled(controlled, initial) {
10232
10264
  return [isControlled ? controlled : own, set];
10233
10265
  }
10234
10266
  /**
10235
- * The open state, the selection, the keys, the tab stop and the registry of
10236
- * a TreeView (#1096). Focus moves for real, with a roving tabindex, rather
10237
- * than through `aria-activedescendant`.
10267
+ * The open state, the selection, the keys, the tab stop, the positions and
10268
+ * the registry of a TreeView (#1096). Focus moves for real, with a roving
10269
+ * tabindex, rather than through `aria-activedescendant`. Each item's
10270
+ * position among the items beside it is declared rather than left to the
10271
+ * browser (#1171), read off the page once the tree has mounted.
10238
10272
  *
10239
10273
  * Every item registers its value, its parent and whether it's disabled from
10240
10274
  * a layout effect. In a checkbox tree a closed item's children still render
@@ -10276,6 +10310,8 @@ function useTreeView(options) {
10276
10310
  const restingPathRef = React.useRef([]);
10277
10311
  // The values above an item that left the page with focus in it.
10278
10312
  const lostRef = React.useRef(null);
10313
+ // Each item's place among the items beside it, by its key.
10314
+ const [positions, setPositions] = React.useState(NO_POSITIONS);
10279
10315
  const { records } = registryRef.current;
10280
10316
  const checks = mode === 'checkbox'
10281
10317
  ? // eslint-disable-next-line react-hooks/refs -- read in render on purpose: items write the registry from their layout effects and then ask for this render with `refresh`
@@ -10329,11 +10365,12 @@ function useTreeView(options) {
10329
10365
  else
10330
10366
  act(item, list);
10331
10367
  };
10332
- // After every render, put the stop where it belongs. While focus is in
10333
- // the tree it's the focused item. Otherwise it's the selected item, or the
10334
- // first checked one on the page, and failing that it stays where it was.
10335
- // An item that goes away hands it to its nearest ancestor on the page, or
10336
- // to the first item, and focus goes with it if it had focus.
10368
+ // After every render, read each item's position off the page, and put the
10369
+ // stop where it belongs. While focus is in the tree it's the focused item.
10370
+ // Otherwise it's the selected item, or the first checked one on the page,
10371
+ // and failing that it stays where it was. An item that goes away hands it
10372
+ // to its nearest ancestor on the page, or to the first item, and focus goes
10373
+ // with it if it had focus.
10337
10374
  // eslint-disable-next-line react-hooks/exhaustive-deps -- after every render on purpose: it reads the page, and sets state only when what it reads has changed
10338
10375
  useClientLayoutEffect(() => {
10339
10376
  const tree = treeRef.current;
@@ -10343,6 +10380,9 @@ function useTreeView(options) {
10343
10380
  // sharing a value, which they shouldn't, can leave it without one the
10344
10381
  // page still has.
10345
10382
  const shown = shownIn(tree, registry);
10383
+ const placed = positionsOf(shown, registry);
10384
+ if (!samePositions(positions, placed))
10385
+ setPositions(placed);
10346
10386
  const onPage = shown.map(element => registry.values.get(element));
10347
10387
  const elementOf = (value) => shown[onPage.indexOf(value)];
10348
10388
  const isShown = (value) => value != null && onPage.includes(value);
@@ -10364,6 +10404,20 @@ function useTreeView(options) {
10364
10404
  restingPathRef.current = next ? pathOf(elementOf(next), registry) : [];
10365
10405
  }
10366
10406
  });
10407
+ // Items can also move without the tree rendering, such as children that a
10408
+ // component of the app sorts with state of its own. That changes no record,
10409
+ // so the page is watched for items coming, going and moving, and the
10410
+ // positions are read again.
10411
+ React.useEffect(() => {
10412
+ const tree = treeRef.current;
10413
+ const observer = new MutationObserver(() => {
10414
+ const registry = registryRef.current;
10415
+ const placed = positionsOf(shownIn(tree, registry), registry);
10416
+ setPositions(current => samePositions(current, placed) ? current : placed);
10417
+ });
10418
+ observer.observe(tree, { childList: true, subtree: true });
10419
+ return () => observer.disconnect();
10420
+ }, []);
10367
10421
  const register = React.useCallback((value, record) => {
10368
10422
  const { records: known } = registryRef.current;
10369
10423
  known.set(value, record);
@@ -10486,6 +10540,7 @@ function useTreeView(options) {
10486
10540
  tabStop,
10487
10541
  isOpen,
10488
10542
  stateOf,
10543
+ positionOf: key => positions.get(key),
10489
10544
  register,
10490
10545
  setElement,
10491
10546
  onFocus,
@@ -11071,7 +11126,9 @@ const TreeViewItem = React.forwardRef(function TreeViewItem({ value, label, icon
11071
11126
  if (branch.hidden)
11072
11127
  return jsxRuntime.jsx(jsxRuntime.Fragment, { children: items });
11073
11128
  const info = { value, parent, disabled, isParent, open };
11074
- return (jsxRuntime.jsxs("li", { ...props, ref: setRefs, className: classNames(itemClass, className), role: "treeitem", "aria-labelledby": labelId, "aria-level": branch.path.length + 1, "aria-expanded": isParent ? open : undefined, "aria-disabled": disabled || undefined, "aria-selected": selected, "aria-checked": checked, tabIndex: tree.tabStop === value ? 0 : -1, onKeyDown: event => {
11129
+ // A position the caller gives wins, for a set the page holds only part of.
11130
+ const position = tree.positionOf(value);
11131
+ return (jsxRuntime.jsxs("li", { ...props, ref: setRefs, className: classNames(itemClass, className), role: "treeitem", "aria-labelledby": labelId, "aria-level": branch.path.length + 1, "aria-posinset": props['aria-posinset'] ?? position?.posinset, "aria-setsize": props['aria-setsize'] ?? position?.setsize, "aria-expanded": isParent ? open : undefined, "aria-disabled": disabled || undefined, "aria-selected": selected, "aria-checked": checked, tabIndex: tree.tabStop === value ? 0 : -1, onKeyDown: event => {
11075
11132
  onKeyDown?.(event);
11076
11133
  tree.onKeyDown(event, info);
11077
11134
  }, onFocus: event => {
@@ -11141,7 +11198,8 @@ function StatusRow({ kind, parent, level, message, onRetry, labelRef, }) {
11141
11198
  open: false,
11142
11199
  retry,
11143
11200
  };
11144
- return (jsxRuntime.jsx("li", { ref: setRef, className: itemClass, role: "treeitem", "aria-labelledby": retry ? `${labelId} ${retryId}` : labelId, "aria-level": level, "aria-disabled": !retry || undefined, tabIndex: tree.tabStop === labelId ? 0 : -1, onKeyDown: event => tree.onKeyDown(event, info), onFocus: event => {
11201
+ const position = tree.positionOf(labelId);
11202
+ return (jsxRuntime.jsx("li", { ref: setRef, className: itemClass, role: "treeitem", "aria-labelledby": retry ? `${labelId} ${retryId}` : labelId, "aria-level": level, "aria-posinset": position?.posinset, "aria-setsize": position?.setsize, "aria-disabled": !retry || undefined, tabIndex: tree.tabStop === labelId ? 0 : -1, onKeyDown: event => tree.onKeyDown(event, info), onFocus: event => {
11145
11203
  // Focus in something the message holds isn't the row's.
11146
11204
  if (event.target === event.currentTarget) {
11147
11205
  tree.onFocus(labelId, event.currentTarget);
@@ -20954,6 +21012,21 @@ const bulmaGlobalVars = [
20954
21012
  '--bulma-danger-h',
20955
21013
  '--bulma-danger-s',
20956
21014
  '--bulma-danger-l',
21015
+ // the lightness of each color's label on a solid surface, and of its text
21016
+ // on the page, which Bulma's Sass build picks for contrast with the color
21017
+ // it was built with, so a new trio does not move them
21018
+ '--bulma-primary-invert-l',
21019
+ '--bulma-primary-on-scheme-l',
21020
+ '--bulma-link-invert-l',
21021
+ '--bulma-link-on-scheme-l',
21022
+ '--bulma-info-invert-l',
21023
+ '--bulma-info-on-scheme-l',
21024
+ '--bulma-success-invert-l',
21025
+ '--bulma-success-on-scheme-l',
21026
+ '--bulma-warning-invert-l',
21027
+ '--bulma-warning-on-scheme-l',
21028
+ '--bulma-danger-invert-l',
21029
+ '--bulma-danger-on-scheme-l',
20957
21030
  // shadow (the upstream token .box/.card/.dropdown/.panel derive their own
20958
21031
  // shadow variables from; overriding it from an ancestor is the route that
20959
21032
  // cascades, since those selectors re-declare their derived var on
@@ -21704,6 +21777,104 @@ const setRootThemeRules = (order, rules) => {
21704
21777
  }
21705
21778
  renderRootThemeRules();
21706
21779
  };
21780
+ /** A value given per scheme, rather than as one string. */
21781
+ const isSchemeValues = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
21782
+ /**
21783
+ * The selector of one of Bulma's schemes, as the stylesheet Theme is used
21784
+ * with writes it: `[data-theme=dark], .theme-dark`, or with the class prefix
21785
+ * in both names (`[data-bestax-theme=dark], .bestax-theme-dark`) for a
21786
+ * prefixed build. A prefix that cannot be written into a selector as it
21787
+ * stands gets the plain names, which `colorMode` writes under any prefix.
21788
+ */
21789
+ const schemeSelector = (classPrefix, scheme) => {
21790
+ const prefix = /^[A-Za-z_][\w-]*$/.test(classPrefix) ? classPrefix : '';
21791
+ return `[data-${prefix}theme=${scheme}], .${prefix}theme-${scheme}`;
21792
+ };
21793
+ /**
21794
+ * The `:root` rules for variables given a value per scheme, laid out the
21795
+ * way Bulma lays out its own schemes so each value applies exactly where
21796
+ * Bulma's would: under the visitor's OS preference, unless the page names
21797
+ * the other scheme, and wherever the page names this one, on `<html>` or on
21798
+ * an element inside. A scheme with no value gets no rules, which leaves the
21799
+ * stylesheet's own value there.
21800
+ *
21801
+ * Every selector is as specific as `:root` (the guard is in `:where()`), so
21802
+ * between Themes, and against a plain `:root` rule, the later one still
21803
+ * wins, as it does for every other variable.
21804
+ */
21805
+ const schemeRules = (vars, classPrefix) => {
21806
+ const other = { light: 'dark', dark: 'light' };
21807
+ const body = (scheme) => vars
21808
+ .filter(([, values]) => values[scheme])
21809
+ .map(([key, values]) => `${key}: ${values[scheme]};`)
21810
+ .join(' ');
21811
+ const schemes = ['light', 'dark']
21812
+ .map(scheme => [scheme, body(scheme)])
21813
+ .filter(([, declarations]) => declarations);
21814
+ // Bulma's order: both preferences, then both names, the dark one last.
21815
+ return [
21816
+ ...schemes.map(([scheme, declarations]) => `@media (prefers-color-scheme: ${scheme}) { :root:where(:not(` +
21817
+ `${schemeSelector(classPrefix, other[scheme])})) { ${declarations} } }`),
21818
+ ...schemes.map(([scheme, declarations]) => `${schemeSelector(classPrefix, scheme)} { ${declarations} }`),
21819
+ ];
21820
+ };
21821
+ const schemeAttributes = new Map();
21822
+ /** Set a theme attribute on `<html>`, or remove it for `null`. */
21823
+ const setSchemeAttribute = (name, value) => {
21824
+ const root = document.documentElement;
21825
+ if (value === null) {
21826
+ root.removeAttribute(name);
21827
+ }
21828
+ else {
21829
+ root.setAttribute(name, value);
21830
+ }
21831
+ };
21832
+ /**
21833
+ * Write one Theme's value for a theme attribute, `null` removing it, and
21834
+ * make that Theme the attribute's latest writer. Throws, recording nothing,
21835
+ * when `name` cannot be an attribute name.
21836
+ *
21837
+ * A Theme that writes the value it already holds keeps its place and leaves
21838
+ * the page alone. React runs a layout effect again when a `Suspense`
21839
+ * boundary shows its tree again, with nothing changed, and that must not
21840
+ * take the attribute back from a Theme that wrote after it.
21841
+ */
21842
+ const writeSchemeAttribute = (order, name, value) => {
21843
+ const held = schemeAttributes.get(name);
21844
+ if (held?.writers.some(writer => writer.order === order && writer.value === value)) {
21845
+ return;
21846
+ }
21847
+ const entry = held ?? {
21848
+ original: document.documentElement.getAttribute(name),
21849
+ writers: [],
21850
+ };
21851
+ setSchemeAttribute(name, value);
21852
+ entry.writers = entry.writers.filter(writer => writer.order !== order);
21853
+ entry.writers.push({ order, value });
21854
+ schemeAttributes.set(name, entry);
21855
+ };
21856
+ /**
21857
+ * Take one Theme off every theme attribute it wrote, except those named in
21858
+ * `keep`. An attribute it was the latest writer of goes back to the value of
21859
+ * the writer before it, or to what it held before any of them.
21860
+ */
21861
+ const releaseSchemeAttributes = (order, keep = []) => {
21862
+ for (const [name, entry] of schemeAttributes) {
21863
+ const index = entry.writers.findIndex(writer => writer.order === order);
21864
+ if (index === -1 || keep.includes(name)) {
21865
+ continue;
21866
+ }
21867
+ const latest = index === entry.writers.length - 1;
21868
+ entry.writers.splice(index, 1);
21869
+ if (entry.writers.length === 0) {
21870
+ schemeAttributes.delete(name);
21871
+ setSchemeAttribute(name, entry.original);
21872
+ }
21873
+ else if (latest) {
21874
+ setSchemeAttribute(name, entry.writers[entry.writers.length - 1].value);
21875
+ }
21876
+ }
21877
+ };
21707
21878
  /**
21708
21879
  * Theme component that injects Bulma CSS variables either globally or locally.
21709
21880
  *
@@ -21790,7 +21961,9 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21790
21961
  });
21791
21962
  // Merge bulmaVars and individual props, with props taking precedence
21792
21963
  const mergedVars = React.useMemo(() => {
21793
- const vars = { ...bulmaVars };
21964
+ const vars = {
21965
+ ...bulmaVars,
21966
+ };
21794
21967
  for (const [propName, cssVar] of Object.entries(bulmaVarPropMap)) {
21795
21968
  if (bulmaVarProps[propName] !== undefined) {
21796
21969
  vars[cssVar] = bulmaVarProps[propName];
@@ -21804,61 +21977,99 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21804
21977
  }
21805
21978
  return vars;
21806
21979
  }, [bulmaVars, bulmaVarProps, radiusVar, columnGapVar]);
21980
+ // The variables Theme writes, split by form: a value per scheme, or one
21981
+ // value for every scheme. A key Theme does not take, or an empty value, is
21982
+ // left out. A plain value is written as given, so a number untyped code
21983
+ // passes still works.
21984
+ const { plainVars, perSchemeVars } = React.useMemo(() => {
21985
+ const plain = [];
21986
+ const perScheme = [];
21987
+ for (const [key, value] of Object.entries(mergedVars)) {
21988
+ if (!value || !bulmaCssVars.includes(key)) {
21989
+ continue;
21990
+ }
21991
+ if (isSchemeValues(value)) {
21992
+ perScheme.push([key, value]);
21993
+ }
21994
+ else {
21995
+ plain.push([key, value]);
21996
+ }
21997
+ }
21998
+ return { plainVars: plain, perSchemeVars: perScheme };
21999
+ }, [mergedVars]);
21807
22000
  // From an effect rather than the render body, so rendering does none of
21808
22001
  // this work, and a server render none at all. In production it returns
21809
22002
  // at its development check before looking at the variables.
21810
22003
  React.useEffect(() => {
21811
22004
  warnComponentVars(mergedVars);
21812
22005
  }, [mergedVars]);
21813
- // This Theme's place among root Themes; see `rootThemeRules`.
22006
+ // A scoped Theme writes its variables as an inline style, which has no way
22007
+ // to say which scheme a value is for, so it skips a value given per
22008
+ // scheme. Say so, since nothing on the page does.
22009
+ React.useEffect(() => {
22010
+ if (isRoot || perSchemeVars.length === 0 || !isDev()) {
22011
+ return;
22012
+ }
22013
+ const keys = perSchemeVars.map(([key]) => key).sort();
22014
+ warnOnce(`Theme:scoped-scheme-values:${keys.join(',')}`, `[bestax-bulma] <Theme>: a scoped Theme writes its variables as an ` +
22015
+ `inline style, which cannot hold a value per scheme, so ` +
22016
+ `${keys.join(', ')} ${keys.length === 1 ? 'is' : 'are'} not set. ` +
22017
+ 'Give a single string, set the values on an isRoot Theme, or set ' +
22018
+ 'them in your CSS.');
22019
+ }, [isRoot, perSchemeVars]);
22020
+ // This Theme's place among the Themes on the page; see `rootThemeRules`
22021
+ // and `schemeAttributes`.
21814
22022
  const [rootOrder] = React.useState(() => nextRootOrder++);
21815
- // The `:root` rules this Theme contributes, or '' when it contributes none.
22023
+ // Bulma names its scheme attribute after the class prefix a stylesheet was
22024
+ // built with (`data-<prefix>theme`), so the prefixed builds read
22025
+ // `data-bestax-theme` and never `data-theme`. Its scheme classes take the
22026
+ // prefix too (`.bestax-theme-dark`).
22027
+ const classPrefix = useClassPrefix();
22028
+ // The `:root` rules this Theme contributes, or '' when it contributes none:
22029
+ // one block for the plain values, then the rules for each value given per
22030
+ // scheme.
21816
22031
  const rootRules = React.useMemo(() => {
21817
22032
  if (!isRoot) {
21818
22033
  return '';
21819
22034
  }
21820
- const cssRules = Object.entries(mergedVars)
21821
- .filter(([key, value]) => bulmaCssVars.includes(key) && value)
21822
- .map(([key, value]) => `${key}: ${value};`)
21823
- .join(' ');
21824
- return cssRules ? `:root { ${cssRules} }` : '';
21825
- }, [mergedVars, isRoot]);
22035
+ const plain = plainVars.map(([key, value]) => `${key}: ${value};`);
22036
+ return [
22037
+ ...(plain.length > 0 ? [`:root { ${plain.join(' ')} }`] : []),
22038
+ ...schemeRules(perSchemeVars, classPrefix),
22039
+ ].join('\n');
22040
+ }, [plainVars, perSchemeVars, classPrefix, isRoot]);
21826
22041
  // Inject CSS variables globally at :root level, alongside any other root
21827
22042
  // Themes. Clearing `isRoot` or every variable passes '', which withdraws
21828
- // this Theme's rules.
21829
- React.useEffect(() => {
22043
+ // this Theme's rules. A layout effect, so they are in place before the
22044
+ // browser paints what this Theme rendered.
22045
+ useClientLayoutEffect(() => {
21830
22046
  setRootThemeRules(rootOrder, rootRules);
21831
22047
  }, [rootOrder, rootRules]);
21832
22048
  // Withdraw them on unmount. Kept apart from the effect above so a change
21833
22049
  // rewrites the shared element in place rather than removing and recreating
21834
- // it between the cleanup and the next run.
22050
+ // it between the cleanup and the next run, and passive, so a Theme that a
22051
+ // `Suspense` fallback hides keeps its variables meanwhile: React cleans up
22052
+ // a hidden tree's layout effects, but not its passive ones.
21835
22053
  React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
21836
- // Bulma names its scheme attribute after the class prefix a stylesheet was
21837
- // built with (`data-<prefix>theme`), so the prefixed builds read
21838
- // `data-bestax-theme` and never `data-theme`.
21839
- const classPrefix = useClassPrefix();
21840
22054
  // Toggle Bulma's light/dark scheme by writing the theme attribute on the
21841
22055
  // document root (<html>). This is always global, even on a scoped Theme.
21842
22056
  // Under a class prefix the prefixed attribute is written as well as
21843
22057
  // `data-theme`, so whichever build is loaded sees it; with no prefix only
21844
22058
  // `data-theme` is. `'system'` removes them so Bulma follows the OS
21845
- // preference.
21846
- React.useEffect(() => {
21847
- if (colorMode === undefined) {
21848
- return;
21849
- }
21850
- const root = document.documentElement;
21851
- const names = classPrefix
21852
- ? ['data-theme', `data-${classPrefix}theme`]
21853
- : ['data-theme'];
21854
- const previous = names.map(name => root.getAttribute(name));
22059
+ // preference. Written before the browser paints, and handed back on
22060
+ // unmount, for the reasons the two effects above give; `schemeAttributes`
22061
+ // keeps the two in step.
22062
+ useClientLayoutEffect(() => {
22063
+ const names = colorMode === undefined
22064
+ ? []
22065
+ : classPrefix
22066
+ ? ['data-theme', `data-${classPrefix}theme`]
22067
+ : ['data-theme'];
22068
+ const value = colorMode === undefined || colorMode === 'system' ? null : colorMode;
22069
+ releaseSchemeAttributes(rootOrder, names);
21855
22070
  for (const name of names) {
21856
- if (colorMode === 'system') {
21857
- root.removeAttribute(name);
21858
- continue;
21859
- }
21860
22071
  try {
21861
- root.setAttribute(name, colorMode);
22072
+ writeSchemeAttribute(rootOrder, name, value);
21862
22073
  }
21863
22074
  catch {
21864
22075
  // A class prefix can hold characters an attribute name cannot.
@@ -21871,32 +22082,20 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21871
22082
  'with that prefix keeps its scheme.');
21872
22083
  }
21873
22084
  }
21874
- // Restore the previous values when colorMode changes or the component unmounts.
21875
- return () => {
21876
- names.forEach((name, i) => {
21877
- const value = previous[i];
21878
- if (value === null) {
21879
- root.removeAttribute(name);
21880
- }
21881
- else {
21882
- root.setAttribute(name, value);
21883
- }
21884
- });
21885
- };
21886
- }, [colorMode, classPrefix]);
22085
+ }, [colorMode, classPrefix, rootOrder]);
22086
+ // Hand the attributes back on unmount.
22087
+ React.useEffect(() => () => releaseSchemeAttributes(rootOrder), [rootOrder]);
21887
22088
  // For local injection (when isRoot is false), prepare style object for CSS vars
21888
22089
  const style = React.useMemo(() => {
21889
22090
  if (isRoot) {
21890
22091
  return {};
21891
22092
  }
21892
22093
  const styleObj = {};
21893
- for (const [key, value] of Object.entries(mergedVars)) {
21894
- if (bulmaCssVars.includes(key) && value) {
21895
- styleObj[key] = value;
21896
- }
22094
+ for (const [key, value] of plainVars) {
22095
+ styleObj[key] = value;
21897
22096
  }
21898
22097
  return styleObj;
21899
- }, [mergedVars, isRoot]);
22098
+ }, [plainVars, isRoot]);
21900
22099
  // Generate combined class names for the wrapper div
21901
22100
  const combinedClassName = React.useMemo(() => {
21902
22101
  if (isRoot) {