@allxsmith/bestax-bulma 5.45.0 → 5.46.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 (38) hide show
  1. package/dist/esm/components/Modal.js +20 -7
  2. package/dist/esm/components/Modal.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/useVisibilityClasses.js.map +1 -1
  14. package/dist/index.cjs +209 -55
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/types/components/Modal.d.ts +10 -1
  17. package/dist/types/form/Checkbox.d.ts +3 -0
  18. package/dist/types/form/File.d.ts +3 -0
  19. package/dist/types/form/Numberinput.d.ts +2 -0
  20. package/dist/types/form/PinInput.d.ts +3 -0
  21. package/dist/types/form/Radio.d.ts +3 -0
  22. package/dist/types/form/SelectBase.d.ts +3 -0
  23. package/dist/types/form/Slider.d.ts +3 -0
  24. package/dist/types/form/Switch.d.ts +2 -0
  25. package/dist/types/helpers/Theme.d.ts +37 -10
  26. package/dist/types/helpers/useVisibilityClasses.d.ts +5 -1
  27. package/dist/types-cjs/components/Modal.d.ts +10 -1
  28. package/dist/types-cjs/form/Checkbox.d.ts +3 -0
  29. package/dist/types-cjs/form/File.d.ts +3 -0
  30. package/dist/types-cjs/form/Numberinput.d.ts +2 -0
  31. package/dist/types-cjs/form/PinInput.d.ts +3 -0
  32. package/dist/types-cjs/form/Radio.d.ts +3 -0
  33. package/dist/types-cjs/form/SelectBase.d.ts +3 -0
  34. package/dist/types-cjs/form/Slider.d.ts +3 -0
  35. package/dist/types-cjs/form/Switch.d.ts +2 -0
  36. package/dist/types-cjs/helpers/Theme.d.ts +37 -10
  37. package/dist/types-cjs/helpers/useVisibilityClasses.d.ts +5 -1
  38. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -4595,8 +4595,12 @@ function useFocusTrap(containerRef, options = {}) {
4595
4595
  });
4596
4596
  }
4597
4597
 
4598
+ // The enclosing modal's `onClose`, for a `Modal.Close` written as a part, so
4599
+ // it closes the modal the way the button the props form renders does.
4600
+ const ModalCloseContext = React.createContext(undefined);
4598
4601
  /**
4599
- * Modal.Background - Renders the modal background overlay.
4602
+ * Modal.Background - Renders the modal background overlay. Pass it
4603
+ * `onClick={onClose}` for a click on it to close the modal.
4600
4604
  *
4601
4605
  * @function
4602
4606
  * @param {ModalBackgroundProps} props - Component props.
@@ -4680,7 +4684,8 @@ const ModalCard = withSubComponents(ModalCardComponent, {
4680
4684
  Foot: ModalCardFoot,
4681
4685
  });
4682
4686
  /**
4683
- * Modal.Close - Renders modal close button with two variant styles.
4687
+ * Modal.Close - Renders modal close button with two variant styles. It calls
4688
+ * the modal's `onClose` unless given an `onClick` of its own.
4684
4689
  *
4685
4690
  * @function
4686
4691
  * @param {ModalCloseProps} props - Component props.
@@ -4693,9 +4698,15 @@ const ModalCard = withSubComponents(ModalCardComponent, {
4693
4698
  *
4694
4699
  * The size prop only applies to the 'floating' variant.
4695
4700
  */
4696
- const ModalClose = ({ className, size = 'large', variant = 'delete', ...props }) => {
4701
+ const ModalClose = ({ className, size = 'large', variant = 'delete', onClick, ...props }) => {
4702
+ const onClose = React.useContext(ModalCloseContext);
4697
4703
  const classes = classNames(usePrefixedClassNames(variant === 'delete' ? 'delete' : 'modal-close', variant === 'floating' && size && { [`is-${size}`]: true }), className);
4698
- return (jsxRuntime.jsx("button", { className: classes, ...props, "aria-label": props['aria-label'] || 'close', type: buttonType(props.type) }));
4704
+ return (jsxRuntime.jsx("button", { className: classes, ...props, "aria-label": props['aria-label'] || 'close', type: buttonType(props.type),
4705
+ // A caller's own `onClick` replaces the close rather than running
4706
+ // before it, unlike `Popover.Close`: this button used to close only
4707
+ // through that handler, so a caller already closing there, as with
4708
+ // `onClick={onClose}`, would otherwise close twice.
4709
+ onClick: onClick ?? (onClose && (() => onClose())) }));
4699
4710
  };
4700
4711
  /**
4701
4712
  * The `Modal` component provides an empty, accessible overlay for arbitrary content — for a ready-made confirm or alert, reach for `Dialog` instead.
@@ -4712,12 +4723,12 @@ const ModalClose = ({ className, size = 'large', variant = 'delete', ...props })
4712
4723
  *
4713
4724
  * @example
4714
4725
  * // Compound Component API
4715
- * <Modal isActive={isOpen}>
4726
+ * <Modal isActive={isOpen} onClose={handleClose}>
4716
4727
  * <Modal.Background onClick={handleClose} />
4717
4728
  * <Modal.Card>
4718
4729
  * <Modal.Card.Head>
4719
4730
  * <Modal.Card.Title>Title</Modal.Card.Title>
4720
- * <Modal.Close onClick={handleClose} />
4731
+ * <Modal.Close />
4721
4732
  * </Modal.Card.Head>
4722
4733
  * <Modal.Card.Body>Content</Modal.Card.Body>
4723
4734
  * </Modal.Card>
@@ -4896,6 +4907,8 @@ const ModalRoot = React.forwardRef(function ModalRoot({ active, isActive, onClos
4896
4907
  isModalCard = !!modalCardTitle || !!modalCardFoot;
4897
4908
  modalElement = (jsxRuntime.jsxs("div", { className: modalClasses, ref: combinedRef, role: resolvedRole, "aria-modal": resolvedAriaModal, "aria-labelledby": titleId, tabIndex: -1, ...rest, "data-testid": "modal", children: [jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-background'), onClick: onClose, "data-testid": "modal-background" }), isModalCard ? (jsxRuntime.jsxs("div", { className: prefixedClassNames(classPrefix, 'modal-card'), children: [modalCardTitle && (jsxRuntime.jsxs("header", { className: prefixedClassNames(classPrefix, 'modal-card-head'), children: [jsxRuntime.jsx("p", { id: titleId, className: prefixedClassNames(classPrefix, 'modal-card-title'), children: modalCardTitle }), onClose && (jsxRuntime.jsx("button", { className: deleteClass, "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close" }))] })), jsxRuntime.jsx("section", { className: prefixedClassNames(classPrefix, 'modal-card-body'), "data-testid": "modal-body", children: children }), modalCardFoot && (jsxRuntime.jsx("footer", { className: prefixedClassNames(classPrefix, 'modal-card-foot'), children: modalCardFoot }))] })) : (jsxRuntime.jsx("div", { className: prefixedClassNames(classPrefix, 'modal-content'), "data-testid": "modal-content", children: children })), (!isModalCard || (!modalCardTitle && onClose)) && onClose && (jsxRuntime.jsx("button", { className: prefixedClassNames(classPrefix, 'modal-close', 'is-large'), "aria-label": "close", onClick: onClose, type: "button", "data-testid": "modal-close-float" }))] }));
4898
4909
  }
4910
+ // In both forms, so a `Modal.Close` anywhere inside closes this modal.
4911
+ modalElement = (jsxRuntime.jsx(ModalCloseContext.Provider, { value: onClose, children: modalElement }));
4899
4912
  if (isPortaled) {
4900
4913
  const target = resolveContainer(typeof portal === 'boolean' ? undefined : portal);
4901
4914
  return reactDom.createPortal(modalElement, target);
@@ -20941,6 +20954,21 @@ const bulmaGlobalVars = [
20941
20954
  '--bulma-danger-h',
20942
20955
  '--bulma-danger-s',
20943
20956
  '--bulma-danger-l',
20957
+ // the lightness of each color's label on a solid surface, and of its text
20958
+ // on the page, which Bulma's Sass build picks for contrast with the color
20959
+ // it was built with, so a new trio does not move them
20960
+ '--bulma-primary-invert-l',
20961
+ '--bulma-primary-on-scheme-l',
20962
+ '--bulma-link-invert-l',
20963
+ '--bulma-link-on-scheme-l',
20964
+ '--bulma-info-invert-l',
20965
+ '--bulma-info-on-scheme-l',
20966
+ '--bulma-success-invert-l',
20967
+ '--bulma-success-on-scheme-l',
20968
+ '--bulma-warning-invert-l',
20969
+ '--bulma-warning-on-scheme-l',
20970
+ '--bulma-danger-invert-l',
20971
+ '--bulma-danger-on-scheme-l',
20944
20972
  // shadow (the upstream token .box/.card/.dropdown/.panel derive their own
20945
20973
  // shadow variables from; overriding it from an ancestor is the route that
20946
20974
  // cascades, since those selectors re-declare their derived var on
@@ -21691,6 +21719,104 @@ const setRootThemeRules = (order, rules) => {
21691
21719
  }
21692
21720
  renderRootThemeRules();
21693
21721
  };
21722
+ /** A value given per scheme, rather than as one string. */
21723
+ const isSchemeValues = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
21724
+ /**
21725
+ * The selector of one of Bulma's schemes, as the stylesheet Theme is used
21726
+ * with writes it: `[data-theme=dark], .theme-dark`, or with the class prefix
21727
+ * in both names (`[data-bestax-theme=dark], .bestax-theme-dark`) for a
21728
+ * prefixed build. A prefix that cannot be written into a selector as it
21729
+ * stands gets the plain names, which `colorMode` writes under any prefix.
21730
+ */
21731
+ const schemeSelector = (classPrefix, scheme) => {
21732
+ const prefix = /^[A-Za-z_][\w-]*$/.test(classPrefix) ? classPrefix : '';
21733
+ return `[data-${prefix}theme=${scheme}], .${prefix}theme-${scheme}`;
21734
+ };
21735
+ /**
21736
+ * The `:root` rules for variables given a value per scheme, laid out the
21737
+ * way Bulma lays out its own schemes so each value applies exactly where
21738
+ * Bulma's would: under the visitor's OS preference, unless the page names
21739
+ * the other scheme, and wherever the page names this one, on `<html>` or on
21740
+ * an element inside. A scheme with no value gets no rules, which leaves the
21741
+ * stylesheet's own value there.
21742
+ *
21743
+ * Every selector is as specific as `:root` (the guard is in `:where()`), so
21744
+ * between Themes, and against a plain `:root` rule, the later one still
21745
+ * wins, as it does for every other variable.
21746
+ */
21747
+ const schemeRules = (vars, classPrefix) => {
21748
+ const other = { light: 'dark', dark: 'light' };
21749
+ const body = (scheme) => vars
21750
+ .filter(([, values]) => values[scheme])
21751
+ .map(([key, values]) => `${key}: ${values[scheme]};`)
21752
+ .join(' ');
21753
+ const schemes = ['light', 'dark']
21754
+ .map(scheme => [scheme, body(scheme)])
21755
+ .filter(([, declarations]) => declarations);
21756
+ // Bulma's order: both preferences, then both names, the dark one last.
21757
+ return [
21758
+ ...schemes.map(([scheme, declarations]) => `@media (prefers-color-scheme: ${scheme}) { :root:where(:not(` +
21759
+ `${schemeSelector(classPrefix, other[scheme])})) { ${declarations} } }`),
21760
+ ...schemes.map(([scheme, declarations]) => `${schemeSelector(classPrefix, scheme)} { ${declarations} }`),
21761
+ ];
21762
+ };
21763
+ const schemeAttributes = new Map();
21764
+ /** Set a theme attribute on `<html>`, or remove it for `null`. */
21765
+ const setSchemeAttribute = (name, value) => {
21766
+ const root = document.documentElement;
21767
+ if (value === null) {
21768
+ root.removeAttribute(name);
21769
+ }
21770
+ else {
21771
+ root.setAttribute(name, value);
21772
+ }
21773
+ };
21774
+ /**
21775
+ * Write one Theme's value for a theme attribute, `null` removing it, and
21776
+ * make that Theme the attribute's latest writer. Throws, recording nothing,
21777
+ * when `name` cannot be an attribute name.
21778
+ *
21779
+ * A Theme that writes the value it already holds keeps its place and leaves
21780
+ * the page alone. React runs a layout effect again when a `Suspense`
21781
+ * boundary shows its tree again, with nothing changed, and that must not
21782
+ * take the attribute back from a Theme that wrote after it.
21783
+ */
21784
+ const writeSchemeAttribute = (order, name, value) => {
21785
+ const held = schemeAttributes.get(name);
21786
+ if (held?.writers.some(writer => writer.order === order && writer.value === value)) {
21787
+ return;
21788
+ }
21789
+ const entry = held ?? {
21790
+ original: document.documentElement.getAttribute(name),
21791
+ writers: [],
21792
+ };
21793
+ setSchemeAttribute(name, value);
21794
+ entry.writers = entry.writers.filter(writer => writer.order !== order);
21795
+ entry.writers.push({ order, value });
21796
+ schemeAttributes.set(name, entry);
21797
+ };
21798
+ /**
21799
+ * Take one Theme off every theme attribute it wrote, except those named in
21800
+ * `keep`. An attribute it was the latest writer of goes back to the value of
21801
+ * the writer before it, or to what it held before any of them.
21802
+ */
21803
+ const releaseSchemeAttributes = (order, keep = []) => {
21804
+ for (const [name, entry] of schemeAttributes) {
21805
+ const index = entry.writers.findIndex(writer => writer.order === order);
21806
+ if (index === -1 || keep.includes(name)) {
21807
+ continue;
21808
+ }
21809
+ const latest = index === entry.writers.length - 1;
21810
+ entry.writers.splice(index, 1);
21811
+ if (entry.writers.length === 0) {
21812
+ schemeAttributes.delete(name);
21813
+ setSchemeAttribute(name, entry.original);
21814
+ }
21815
+ else if (latest) {
21816
+ setSchemeAttribute(name, entry.writers[entry.writers.length - 1].value);
21817
+ }
21818
+ }
21819
+ };
21694
21820
  /**
21695
21821
  * Theme component that injects Bulma CSS variables either globally or locally.
21696
21822
  *
@@ -21777,7 +21903,9 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21777
21903
  });
21778
21904
  // Merge bulmaVars and individual props, with props taking precedence
21779
21905
  const mergedVars = React.useMemo(() => {
21780
- const vars = { ...bulmaVars };
21906
+ const vars = {
21907
+ ...bulmaVars,
21908
+ };
21781
21909
  for (const [propName, cssVar] of Object.entries(bulmaVarPropMap)) {
21782
21910
  if (bulmaVarProps[propName] !== undefined) {
21783
21911
  vars[cssVar] = bulmaVarProps[propName];
@@ -21791,61 +21919,99 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21791
21919
  }
21792
21920
  return vars;
21793
21921
  }, [bulmaVars, bulmaVarProps, radiusVar, columnGapVar]);
21922
+ // The variables Theme writes, split by form: a value per scheme, or one
21923
+ // value for every scheme. A key Theme does not take, or an empty value, is
21924
+ // left out. A plain value is written as given, so a number untyped code
21925
+ // passes still works.
21926
+ const { plainVars, perSchemeVars } = React.useMemo(() => {
21927
+ const plain = [];
21928
+ const perScheme = [];
21929
+ for (const [key, value] of Object.entries(mergedVars)) {
21930
+ if (!value || !bulmaCssVars.includes(key)) {
21931
+ continue;
21932
+ }
21933
+ if (isSchemeValues(value)) {
21934
+ perScheme.push([key, value]);
21935
+ }
21936
+ else {
21937
+ plain.push([key, value]);
21938
+ }
21939
+ }
21940
+ return { plainVars: plain, perSchemeVars: perScheme };
21941
+ }, [mergedVars]);
21794
21942
  // From an effect rather than the render body, so rendering does none of
21795
21943
  // this work, and a server render none at all. In production it returns
21796
21944
  // at its development check before looking at the variables.
21797
21945
  React.useEffect(() => {
21798
21946
  warnComponentVars(mergedVars);
21799
21947
  }, [mergedVars]);
21800
- // This Theme's place among root Themes; see `rootThemeRules`.
21948
+ // A scoped Theme writes its variables as an inline style, which has no way
21949
+ // to say which scheme a value is for, so it skips a value given per
21950
+ // scheme. Say so, since nothing on the page does.
21951
+ React.useEffect(() => {
21952
+ if (isRoot || perSchemeVars.length === 0 || !isDev()) {
21953
+ return;
21954
+ }
21955
+ const keys = perSchemeVars.map(([key]) => key).sort();
21956
+ warnOnce(`Theme:scoped-scheme-values:${keys.join(',')}`, `[bestax-bulma] <Theme>: a scoped Theme writes its variables as an ` +
21957
+ `inline style, which cannot hold a value per scheme, so ` +
21958
+ `${keys.join(', ')} ${keys.length === 1 ? 'is' : 'are'} not set. ` +
21959
+ 'Give a single string, set the values on an isRoot Theme, or set ' +
21960
+ 'them in your CSS.');
21961
+ }, [isRoot, perSchemeVars]);
21962
+ // This Theme's place among the Themes on the page; see `rootThemeRules`
21963
+ // and `schemeAttributes`.
21801
21964
  const [rootOrder] = React.useState(() => nextRootOrder++);
21802
- // The `:root` rules this Theme contributes, or '' when it contributes none.
21965
+ // Bulma names its scheme attribute after the class prefix a stylesheet was
21966
+ // built with (`data-<prefix>theme`), so the prefixed builds read
21967
+ // `data-bestax-theme` and never `data-theme`. Its scheme classes take the
21968
+ // prefix too (`.bestax-theme-dark`).
21969
+ const classPrefix = useClassPrefix();
21970
+ // The `:root` rules this Theme contributes, or '' when it contributes none:
21971
+ // one block for the plain values, then the rules for each value given per
21972
+ // scheme.
21803
21973
  const rootRules = React.useMemo(() => {
21804
21974
  if (!isRoot) {
21805
21975
  return '';
21806
21976
  }
21807
- const cssRules = Object.entries(mergedVars)
21808
- .filter(([key, value]) => bulmaCssVars.includes(key) && value)
21809
- .map(([key, value]) => `${key}: ${value};`)
21810
- .join(' ');
21811
- return cssRules ? `:root { ${cssRules} }` : '';
21812
- }, [mergedVars, isRoot]);
21977
+ const plain = plainVars.map(([key, value]) => `${key}: ${value};`);
21978
+ return [
21979
+ ...(plain.length > 0 ? [`:root { ${plain.join(' ')} }`] : []),
21980
+ ...schemeRules(perSchemeVars, classPrefix),
21981
+ ].join('\n');
21982
+ }, [plainVars, perSchemeVars, classPrefix, isRoot]);
21813
21983
  // Inject CSS variables globally at :root level, alongside any other root
21814
21984
  // Themes. Clearing `isRoot` or every variable passes '', which withdraws
21815
- // this Theme's rules.
21816
- React.useEffect(() => {
21985
+ // this Theme's rules. A layout effect, so they are in place before the
21986
+ // browser paints what this Theme rendered.
21987
+ useClientLayoutEffect(() => {
21817
21988
  setRootThemeRules(rootOrder, rootRules);
21818
21989
  }, [rootOrder, rootRules]);
21819
21990
  // Withdraw them on unmount. Kept apart from the effect above so a change
21820
21991
  // rewrites the shared element in place rather than removing and recreating
21821
- // it between the cleanup and the next run.
21992
+ // it between the cleanup and the next run, and passive, so a Theme that a
21993
+ // `Suspense` fallback hides keeps its variables meanwhile: React cleans up
21994
+ // a hidden tree's layout effects, but not its passive ones.
21822
21995
  React.useEffect(() => () => setRootThemeRules(rootOrder, ''), [rootOrder]);
21823
- // Bulma names its scheme attribute after the class prefix a stylesheet was
21824
- // built with (`data-<prefix>theme`), so the prefixed builds read
21825
- // `data-bestax-theme` and never `data-theme`.
21826
- const classPrefix = useClassPrefix();
21827
21996
  // Toggle Bulma's light/dark scheme by writing the theme attribute on the
21828
21997
  // document root (<html>). This is always global, even on a scoped Theme.
21829
21998
  // Under a class prefix the prefixed attribute is written as well as
21830
21999
  // `data-theme`, so whichever build is loaded sees it; with no prefix only
21831
22000
  // `data-theme` is. `'system'` removes them so Bulma follows the OS
21832
- // preference.
21833
- React.useEffect(() => {
21834
- if (colorMode === undefined) {
21835
- return;
21836
- }
21837
- const root = document.documentElement;
21838
- const names = classPrefix
21839
- ? ['data-theme', `data-${classPrefix}theme`]
21840
- : ['data-theme'];
21841
- const previous = names.map(name => root.getAttribute(name));
22001
+ // preference. Written before the browser paints, and handed back on
22002
+ // unmount, for the reasons the two effects above give; `schemeAttributes`
22003
+ // keeps the two in step.
22004
+ useClientLayoutEffect(() => {
22005
+ const names = colorMode === undefined
22006
+ ? []
22007
+ : classPrefix
22008
+ ? ['data-theme', `data-${classPrefix}theme`]
22009
+ : ['data-theme'];
22010
+ const value = colorMode === undefined || colorMode === 'system' ? null : colorMode;
22011
+ releaseSchemeAttributes(rootOrder, names);
21842
22012
  for (const name of names) {
21843
- if (colorMode === 'system') {
21844
- root.removeAttribute(name);
21845
- continue;
21846
- }
21847
22013
  try {
21848
- root.setAttribute(name, colorMode);
22014
+ writeSchemeAttribute(rootOrder, name, value);
21849
22015
  }
21850
22016
  catch {
21851
22017
  // A class prefix can hold characters an attribute name cannot.
@@ -21858,32 +22024,20 @@ const Theme = ({ bulmaVars = {}, children, className, isRoot = false, colorMode,
21858
22024
  'with that prefix keeps its scheme.');
21859
22025
  }
21860
22026
  }
21861
- // Restore the previous values when colorMode changes or the component unmounts.
21862
- return () => {
21863
- names.forEach((name, i) => {
21864
- const value = previous[i];
21865
- if (value === null) {
21866
- root.removeAttribute(name);
21867
- }
21868
- else {
21869
- root.setAttribute(name, value);
21870
- }
21871
- });
21872
- };
21873
- }, [colorMode, classPrefix]);
22027
+ }, [colorMode, classPrefix, rootOrder]);
22028
+ // Hand the attributes back on unmount.
22029
+ React.useEffect(() => () => releaseSchemeAttributes(rootOrder), [rootOrder]);
21874
22030
  // For local injection (when isRoot is false), prepare style object for CSS vars
21875
22031
  const style = React.useMemo(() => {
21876
22032
  if (isRoot) {
21877
22033
  return {};
21878
22034
  }
21879
22035
  const styleObj = {};
21880
- for (const [key, value] of Object.entries(mergedVars)) {
21881
- if (bulmaCssVars.includes(key) && value) {
21882
- styleObj[key] = value;
21883
- }
22036
+ for (const [key, value] of plainVars) {
22037
+ styleObj[key] = value;
21884
22038
  }
21885
22039
  return styleObj;
21886
- }, [mergedVars, isRoot]);
22040
+ }, [plainVars, isRoot]);
21887
22041
  // Generate combined class names for the wrapper div
21888
22042
  const combinedClassName = React.useMemo(() => {
21889
22043
  if (isRoot) {