braid-design-system 34.9.0 → 34.10.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 (76) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/color-mode/query-param.d.cts +1 -1
  3. package/dist/color-mode/query-param.d.mts +1 -1
  4. package/dist/lib/components/Accordion/Accordion.cjs +61 -13
  5. package/dist/lib/components/Accordion/Accordion.d.cts +56 -2
  6. package/dist/lib/components/Accordion/Accordion.d.mts +56 -2
  7. package/dist/lib/components/Accordion/Accordion.mjs +62 -14
  8. package/dist/lib/components/Accordion/AccordionContext.d.cts +7 -0
  9. package/dist/lib/components/Accordion/AccordionContext.d.mts +7 -0
  10. package/dist/lib/components/Accordion/AccordionItem.cjs +86 -12
  11. package/dist/lib/components/Accordion/AccordionItem.css.cjs +15 -0
  12. package/dist/lib/components/Accordion/AccordionItem.css.mjs +13 -1
  13. package/dist/lib/components/Accordion/AccordionItem.d.cts +5 -0
  14. package/dist/lib/components/Accordion/AccordionItem.d.mts +5 -0
  15. package/dist/lib/components/Accordion/AccordionItem.mjs +88 -14
  16. package/dist/lib/components/Accordion/AccordionItem.playroom.cjs +6 -2
  17. package/dist/lib/components/Accordion/AccordionItem.playroom.d.cts +1 -1
  18. package/dist/lib/components/Accordion/AccordionItem.playroom.d.mts +1 -1
  19. package/dist/lib/components/Accordion/AccordionItem.playroom.mjs +6 -2
  20. package/dist/lib/components/Avatar/Avatar.cjs +1 -1
  21. package/dist/lib/components/Avatar/Avatar.css.cjs +2 -1
  22. package/dist/lib/components/Avatar/Avatar.css.mjs +2 -1
  23. package/dist/lib/components/Avatar/Avatar.mjs +1 -1
  24. package/dist/lib/components/BraidPortal/BraidPortal.cjs +0 -1
  25. package/dist/lib/components/BraidPortal/BraidPortal.mjs +0 -1
  26. package/dist/lib/components/BraidProvider/BraidProvider.cjs +0 -1
  27. package/dist/lib/components/BraidProvider/BraidProvider.mjs +0 -1
  28. package/dist/lib/components/BraidProvider/VanillaThemeContainer.cjs +2 -2
  29. package/dist/lib/components/BraidProvider/VanillaThemeContainer.mjs +2 -2
  30. package/dist/lib/components/Drawer/Drawer.cjs +5 -0
  31. package/dist/lib/components/Drawer/Drawer.mjs +5 -0
  32. package/dist/lib/components/private/Modal/Modal.cjs +0 -10
  33. package/dist/lib/components/private/Modal/Modal.mjs +0 -10
  34. package/dist/lib/components/private/Modal/ModalContent.cjs +9 -4
  35. package/dist/lib/components/private/Modal/ModalContent.mjs +9 -4
  36. package/dist/lib/components/useToast/Toast.cjs +8 -3
  37. package/dist/lib/components/useToast/Toast.mjs +8 -3
  38. package/dist/lib/components/useToast/ToastTypes.d.cts +3 -1
  39. package/dist/lib/components/useToast/ToastTypes.d.mts +3 -1
  40. package/dist/lib/components/useToast/useFlipList.cjs +3 -3
  41. package/dist/lib/components/useToast/useFlipList.mjs +3 -3
  42. package/dist/lib/components/useToast/useTimeout.cjs +1 -1
  43. package/dist/lib/components/useToast/useTimeout.mjs +1 -1
  44. package/dist/lib/playroom/snippets/Accordion.cjs +8 -12
  45. package/dist/lib/playroom/snippets/Accordion.mjs +8 -12
  46. package/dist/lib/playroom/snippets/BulkActions.cjs +8 -0
  47. package/dist/lib/playroom/snippets/BulkActions.mjs +8 -0
  48. package/dist/lib/playroom/snippets/DividedLists.cjs +8 -0
  49. package/dist/lib/playroom/snippets/DividedLists.mjs +8 -0
  50. package/dist/lib/playroom/snippets/EmptyState.cjs +12 -0
  51. package/dist/lib/playroom/snippets/EmptyState.mjs +12 -0
  52. package/dist/lib/playroom/snippets/ErrorState.cjs +12 -0
  53. package/dist/lib/playroom/snippets/ErrorState.mjs +12 -0
  54. package/dist/lib/playroom/snippets/Forms.cjs +12 -0
  55. package/dist/lib/playroom/snippets/Forms.mjs +12 -0
  56. package/dist/lib/playroom/snippets/Nudge.cjs +20 -0
  57. package/dist/lib/playroom/snippets/Nudge.mjs +20 -0
  58. package/dist/lib/playroom/snippets/ServiceOutageBanner.cjs +12 -0
  59. package/dist/lib/playroom/snippets/ServiceOutageBanner.mjs +12 -0
  60. package/dist/lib/playroom/snippets/SkeletonLoader.cjs +8 -0
  61. package/dist/lib/playroom/snippets/SkeletonLoader.mjs +8 -0
  62. package/dist/lib/playroom/snippets/SocialShare.cjs +8 -0
  63. package/dist/lib/playroom/snippets/SocialShare.mjs +8 -0
  64. package/dist/lib/playroom/snippets.cjs +20 -1
  65. package/dist/lib/playroom/snippets.mjs +116 -97
  66. package/dist/playroom/FrameComponent.mjs +1 -2
  67. package/dist/playroom/components.d.cts +5 -0
  68. package/dist/playroom/components.d.mts +5 -0
  69. package/dist/playroom/scope.mjs +1 -2
  70. package/dist/playroom/snippets.mjs +1 -2
  71. package/dist/test.mjs +1 -2
  72. package/dist/themes/docs.mjs +1 -2
  73. package/dist/themes/seekBusiness.mjs +1 -2
  74. package/dist/themes/seekJobs.mjs +1 -2
  75. package/dist/themes/wireframe.mjs +1 -2
  76. package/package.json +4 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # braid-design-system
2
2
 
3
+ ## 34.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Accordion, AccordionItem:** Add height animation and shared open state ([#2144](https://github.com/seek-oss/braid-design-system/pull/2144))
8
+
9
+ Items animate their height when opening and closing, unless reduced motion is set.
10
+
11
+ Set `multiple` to `false` so only one item can be open, or use `value`, `defaultValue`, and `onChange` on `Accordion` to choose which items are open.
12
+
13
+ **EXAMPLE USAGE:**
14
+
15
+ ```jsx
16
+ <Accordion multiple={false} defaultValue="one">
17
+ <AccordionItem value="one" label="One">
18
+ ...
19
+ </AccordionItem>
20
+ <AccordionItem value="two" label="Two">...</AccordionItem>
21
+ </Accordion>
22
+ ```
23
+ - **useToast:** Add `duration` option ([#2181](https://github.com/seek-oss/braid-design-system/pull/2181))
24
+
25
+ Toasts accept `short` (6 seconds), `standard` (10 seconds), or `indefinite`. The default is `standard`, matching the previous dismiss time.
26
+
27
+ **EXAMPLE USAGE:**
28
+
29
+ ```jsx
30
+ showToast({
31
+ tone: 'positive',
32
+ message: 'Saved',
33
+ duration: 'short',
34
+ });
35
+ ```
36
+
37
+ ### Patch Changes
38
+
39
+ - **Dialog, Drawer:** Fix focus in stacked modals ([#2184](https://github.com/seek-oss/braid-design-system/pull/2184))
40
+
41
+ Fixes an issue where, with multiple modals open, focus could move to an underlying modal after switching away from and back to the browser window, preventing interaction with fields in the top-most modal.
42
+
43
+ ## 34.9.1
44
+
45
+ ### Patch Changes
46
+
47
+ - **Drawer:** Reject empty `title` ([#2171](https://github.com/seek-oss/braid-design-system/pull/2171))
48
+
49
+ An empty `title` is now treated as invalid, consistent with `aria-label`. Provide a non-empty `title`, or use `aria-label` for an untitled `Drawer`.
50
+
51
+ - **Avatar:** Ensure loading background fills completely in dark mode ([#2169](https://github.com/seek-oss/braid-design-system/pull/2169))
52
+
53
+ - playroom: Add Patterns group to snippets ([#2167](https://github.com/seek-oss/braid-design-system/pull/2167))
54
+
55
+ - **Avatar:** Prevent layout collapse inside Inline component ([#2169](https://github.com/seek-oss/braid-design-system/pull/2169))
56
+
57
+ - **BraidProvider:** Ensure default text tone follows the theme ([#2163](https://github.com/seek-oss/braid-design-system/pull/2163))
58
+
59
+ Nested `BraidProvider` components using different themes now always apply the default text tones according to the selected theme.
60
+
61
+ - **Dialog:** Restore header rendering when `title` is empty ([#2171](https://github.com/seek-oss/braid-design-system/pull/2171))
62
+
63
+ Fixes a regression where a `Dialog` with an empty `title` no longer rendered its `illustration` or `description`.
64
+
3
65
  ## 34.9.0
4
66
 
5
67
  ### Minor Changes
@@ -43,6 +43,6 @@ declare const colorModeQueryParamCheck: string;
43
43
  *
44
44
  * @returns Braid color mode preference as query string parameter
45
45
  */
46
- declare const getColorModeQueryParam: () => "" | "_bdsdm=1" | "_bdsdm=2" | "_bdsdm=0";
46
+ declare const getColorModeQueryParam: () => "_bdsdm=0" | "_bdsdm=1" | "_bdsdm=2" | "";
47
47
  //#endregion
48
48
  export { colorModeQueryParamCheck, getColorModeQueryParam };
@@ -43,6 +43,6 @@ declare const colorModeQueryParamCheck: string;
43
43
  *
44
44
  * @returns Braid color mode preference as query string parameter
45
45
  */
46
- declare const getColorModeQueryParam: () => "" | "_bdsdm=1" | "_bdsdm=2" | "_bdsdm=0";
46
+ declare const getColorModeQueryParam: () => "_bdsdm=0" | "_bdsdm=1" | "_bdsdm=2" | "";
47
47
  //#endregion
48
48
  export { colorModeQueryParamCheck, getColorModeQueryParam };
@@ -16,6 +16,13 @@ const validSpaceValues = [
16
16
  "xlarge"
17
17
  ];
18
18
  const defaultSize = "large";
19
+ const normalizeAccordionValue = (input, multiple) => {
20
+ if (input == null || input === "") return [];
21
+ const list = typeof input === "string" ? [input] : [...input];
22
+ (0, assert.default)(list.every((item) => typeof item === "string" && item.length > 0), "Accordion 'value' and 'defaultValue' must be a non-empty string or an array of non-empty strings.");
23
+ (0, assert.default)(multiple || list.length <= 1, "When 'multiple' is false, Accordion 'value' and 'defaultValue' must be a string or a single-item array.");
24
+ return list;
25
+ };
19
26
  const defaultSpaceForSize = {
20
27
  divided: {
21
28
  xsmall: "medium",
@@ -30,26 +37,67 @@ const defaultSpaceForSize = {
30
37
  large: "large"
31
38
  }
32
39
  };
33
- const Accordion = ({ children, size = defaultSize, tone, weight, space: spaceProp, dividers = true, data, ...restProps }) => {
34
- (0, assert.default)(spaceProp === void 0 || Object.values(require_sprinkles_css.normalizeResponsiveValue(spaceProp)).every((value) => value === void 0 || validSpaceValues.includes(value)), `To ensure adequate space for touch targets, 'space' prop values must be one of the following: ${validSpaceValues.map((x) => `"${x}"`).join(", ")}`);
40
+ const assertUniqueItemValues = (children) => {
41
+ const seen = /* @__PURE__ */ new Set();
42
+ for (const child of require_flattenChildren.default(children)) {
43
+ if (!(0, react.isValidElement)(child)) continue;
44
+ const itemValue = child.props.value;
45
+ if (typeof itemValue !== "string") continue;
46
+ (0, assert.default)(!seen.has(itemValue), `AccordionItem value "${itemValue}" is used more than once. Each AccordionItem value must be unique.`);
47
+ seen.add(itemValue);
48
+ }
49
+ };
50
+ const Accordion = (props) => {
51
+ const { children, size = defaultSize, tone, weight, multiple = true, value, defaultValue, space: spaceProp, dividers = true, data, ...restProps } = props;
52
+ (0, assert.default)(spaceProp === void 0 || Object.values(require_sprinkles_css.normalizeResponsiveValue(spaceProp)).every((spaceValue) => spaceValue === void 0 || validSpaceValues.includes(spaceValue)), `To ensure adequate space for touch targets, 'space' prop values must be one of the following: ${validSpaceValues.map((x) => `"${x}"`).join(", ")}`);
35
53
  (0, assert.default)(tone === void 0 || require_AccordionContext.validTones.includes(tone), `The 'tone' prop should be one of the following: ${require_AccordionContext.validTones.map((x) => `"${x}"`).join(", ")}`);
36
- if (process.env.NODE_ENV !== "production")
37
- /**
38
- * Validate that consumers are not passing `data-*`props,
39
- * which will not work and are not validated by TypeScript.
40
- */
41
- require_buildDataAttributes.default({
42
- data,
43
- validateRestProps: restProps
44
- });
54
+ (0, assert.default)(value === void 0 || defaultValue === void 0, "Accordion 'defaultValue' cannot be set when 'value' is set. Use 'value' to control the open items, or 'defaultValue' for the initial state.");
55
+ (0, assert.default)(value === void 0 || typeof props.onChange === "function", "Accordion 'onChange' must be set when 'value' is set.");
56
+ const managed = multiple === false || value !== void 0 || defaultValue !== void 0 || props.onChange !== void 0;
57
+ if (process.env.NODE_ENV !== "production") {
58
+ /**
59
+ * Validate that consumers are not passing `data-*`props,
60
+ * which will not work and are not validated by TypeScript.
61
+ */
62
+ require_buildDataAttributes.default({
63
+ data,
64
+ validateRestProps: restProps
65
+ });
66
+ assertUniqueItemValues(children);
67
+ }
68
+ const normalizedValue = (0, react.useMemo)(() => value === void 0 ? void 0 : normalizeAccordionValue(value, multiple), [multiple, value]);
69
+ const [uncontrolledValue, setUncontrolledValue] = (0, react.useState)(() => normalizeAccordionValue(defaultValue, multiple));
70
+ const openValues = normalizedValue ?? uncontrolledValue;
71
+ const openValuesRef = (0, react.useRef)(openValues);
72
+ const propsRef = (0, react.useRef)(props);
73
+ openValuesRef.current = openValues;
74
+ propsRef.current = props;
75
+ const toggleValue = (0, react.useCallback)((itemValue) => {
76
+ const current = openValuesRef.current;
77
+ const isOpen = current.includes(itemValue);
78
+ let next;
79
+ if (!multiple) next = isOpen ? [] : [itemValue];
80
+ else if (isOpen) next = current.filter((openValue) => openValue !== itemValue);
81
+ else next = [...current, itemValue];
82
+ if (normalizedValue === void 0) setUncontrolledValue(next);
83
+ const p = propsRef.current;
84
+ if (p.multiple === false) p.onChange?.(next[0] ?? "");
85
+ else p.onChange?.(next);
86
+ }, [multiple, normalizedValue]);
45
87
  const contextValue = (0, react.useMemo)(() => ({
46
88
  size,
47
89
  tone,
48
- weight
90
+ weight,
91
+ managed,
92
+ openValues,
93
+ toggleValue
49
94
  }), [
50
95
  size,
51
96
  tone,
52
- weight
97
+ weight,
98
+ managed,
99
+ openValues,
100
+ toggleValue
53
101
  ]);
54
102
  const space = spaceProp ?? defaultSpaceForSize[dividers ? "divided" : "undivided"][size];
55
103
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_AccordionContext.AccordionContext.Provider, {
@@ -5,7 +5,61 @@ import { AccordionContextValue } from "./AccordionContext.cjs";
5
5
  import { FC } from "react";
6
6
  //#region src/lib/components/Accordion/Accordion.d.ts
7
7
  declare const validSpaceValues: readonly ["medium", "large", "xlarge"];
8
- interface AccordionProps {
8
+ /** Open item value, or `""` when none are open. */
9
+ type AccordionSingleValue = string | '';
10
+ type ControlledStateSingle = {
11
+ /**
12
+ * Allow more than one item to be open. Defaults to true.
13
+ * Note: Will default to false next major release.
14
+ */
15
+ multiple: false;
16
+ /**
17
+ * Value of the open AccordionItem, or `""` when none are open.
18
+ */
19
+ value: AccordionSingleValue;
20
+ onChange: (value: AccordionSingleValue) => void;
21
+ defaultValue?: never;
22
+ };
23
+ type ControlledStateMultiple = {
24
+ /**
25
+ * Allow more than one item to be open. Defaults to true.
26
+ * Note: Will default to false next major release.
27
+ */
28
+ multiple?: true;
29
+ /**
30
+ * Values of the open AccordionItems. Pass an empty array when none are open.
31
+ */
32
+ value: string[];
33
+ onChange: (value: string[]) => void;
34
+ defaultValue?: never;
35
+ };
36
+ type UncontrolledStateSingle = {
37
+ /**
38
+ * Allow more than one item to be open. Defaults to true.
39
+ * Note: Will default to false next major release.
40
+ */
41
+ multiple: false;
42
+ /**
43
+ * Value of the AccordionItem to expand by default.
44
+ */
45
+ defaultValue?: AccordionSingleValue;
46
+ value?: never;
47
+ onChange?: never;
48
+ };
49
+ type UncontrolledStateMultiple = {
50
+ /**
51
+ * Allow more than one item to be open. Defaults to true.
52
+ * Note: Will default to false next major release.
53
+ */
54
+ multiple?: true;
55
+ /**
56
+ * Values of the AccordionItems to expand by default.
57
+ */
58
+ defaultValue?: string[];
59
+ value?: never;
60
+ onChange?: never;
61
+ };
62
+ type AccordionProps = {
9
63
  children: ReactNodeNoStrings;
10
64
  dividers?: boolean;
11
65
  size?: AccordionContextValue['size'];
@@ -14,7 +68,7 @@ interface AccordionProps {
14
68
  /** @deprecated The spacing is now derived from the `size` prop and will be removed in a future release. */
15
69
  space?: RequiredResponsiveValue<(typeof validSpaceValues)[number]>;
16
70
  data?: DataAttributeMap;
17
- }
71
+ } & (ControlledStateSingle | ControlledStateMultiple | UncontrolledStateSingle | UncontrolledStateMultiple);
18
72
  declare const Accordion: FC<AccordionProps>;
19
73
  //#endregion
20
74
  export { Accordion, AccordionProps };
@@ -5,7 +5,61 @@ import { AccordionContextValue } from "./AccordionContext.mjs";
5
5
  import { FC } from "react";
6
6
  //#region src/lib/components/Accordion/Accordion.d.ts
7
7
  declare const validSpaceValues: readonly ["medium", "large", "xlarge"];
8
- interface AccordionProps {
8
+ /** Open item value, or `""` when none are open. */
9
+ type AccordionSingleValue = string | '';
10
+ type ControlledStateSingle = {
11
+ /**
12
+ * Allow more than one item to be open. Defaults to true.
13
+ * Note: Will default to false next major release.
14
+ */
15
+ multiple: false;
16
+ /**
17
+ * Value of the open AccordionItem, or `""` when none are open.
18
+ */
19
+ value: AccordionSingleValue;
20
+ onChange: (value: AccordionSingleValue) => void;
21
+ defaultValue?: never;
22
+ };
23
+ type ControlledStateMultiple = {
24
+ /**
25
+ * Allow more than one item to be open. Defaults to true.
26
+ * Note: Will default to false next major release.
27
+ */
28
+ multiple?: true;
29
+ /**
30
+ * Values of the open AccordionItems. Pass an empty array when none are open.
31
+ */
32
+ value: string[];
33
+ onChange: (value: string[]) => void;
34
+ defaultValue?: never;
35
+ };
36
+ type UncontrolledStateSingle = {
37
+ /**
38
+ * Allow more than one item to be open. Defaults to true.
39
+ * Note: Will default to false next major release.
40
+ */
41
+ multiple: false;
42
+ /**
43
+ * Value of the AccordionItem to expand by default.
44
+ */
45
+ defaultValue?: AccordionSingleValue;
46
+ value?: never;
47
+ onChange?: never;
48
+ };
49
+ type UncontrolledStateMultiple = {
50
+ /**
51
+ * Allow more than one item to be open. Defaults to true.
52
+ * Note: Will default to false next major release.
53
+ */
54
+ multiple?: true;
55
+ /**
56
+ * Values of the AccordionItems to expand by default.
57
+ */
58
+ defaultValue?: string[];
59
+ value?: never;
60
+ onChange?: never;
61
+ };
62
+ type AccordionProps = {
9
63
  children: ReactNodeNoStrings;
10
64
  dividers?: boolean;
11
65
  size?: AccordionContextValue['size'];
@@ -14,7 +68,7 @@ interface AccordionProps {
14
68
  /** @deprecated The spacing is now derived from the `size` prop and will be removed in a future release. */
15
69
  space?: RequiredResponsiveValue<(typeof validSpaceValues)[number]>;
16
70
  data?: DataAttributeMap;
17
- }
71
+ } & (ControlledStateSingle | ControlledStateMultiple | UncontrolledStateSingle | UncontrolledStateMultiple);
18
72
  declare const Accordion: FC<AccordionProps>;
19
73
  //#endregion
20
74
  export { Accordion, AccordionProps };
@@ -5,7 +5,7 @@ import { Divider } from "../Divider/Divider.mjs";
5
5
  import { Stack } from "../Stack/Stack.mjs";
6
6
  import { AccordionContext, validTones } from "./AccordionContext.mjs";
7
7
  import assert from "assert";
8
- import { Children, useMemo } from "react";
8
+ import { Children, isValidElement, useCallback, useMemo, useRef, useState } from "react";
9
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/lib/components/Accordion/Accordion.tsx
11
11
  const validSpaceValues = [
@@ -14,6 +14,13 @@ const validSpaceValues = [
14
14
  "xlarge"
15
15
  ];
16
16
  const defaultSize = "large";
17
+ const normalizeAccordionValue = (input, multiple) => {
18
+ if (input == null || input === "") return [];
19
+ const list = typeof input === "string" ? [input] : [...input];
20
+ assert(list.every((item) => typeof item === "string" && item.length > 0), "Accordion 'value' and 'defaultValue' must be a non-empty string or an array of non-empty strings.");
21
+ assert(multiple || list.length <= 1, "When 'multiple' is false, Accordion 'value' and 'defaultValue' must be a string or a single-item array.");
22
+ return list;
23
+ };
17
24
  const defaultSpaceForSize = {
18
25
  divided: {
19
26
  xsmall: "medium",
@@ -28,26 +35,67 @@ const defaultSpaceForSize = {
28
35
  large: "large"
29
36
  }
30
37
  };
31
- const Accordion = ({ children, size = defaultSize, tone, weight, space: spaceProp, dividers = true, data, ...restProps }) => {
32
- assert(spaceProp === void 0 || Object.values(normalizeResponsiveValue(spaceProp)).every((value) => value === void 0 || validSpaceValues.includes(value)), `To ensure adequate space for touch targets, 'space' prop values must be one of the following: ${validSpaceValues.map((x) => `"${x}"`).join(", ")}`);
38
+ const assertUniqueItemValues = (children) => {
39
+ const seen = /* @__PURE__ */ new Set();
40
+ for (const child of flattenChildren(children)) {
41
+ if (!isValidElement(child)) continue;
42
+ const itemValue = child.props.value;
43
+ if (typeof itemValue !== "string") continue;
44
+ assert(!seen.has(itemValue), `AccordionItem value "${itemValue}" is used more than once. Each AccordionItem value must be unique.`);
45
+ seen.add(itemValue);
46
+ }
47
+ };
48
+ const Accordion = (props) => {
49
+ const { children, size = defaultSize, tone, weight, multiple = true, value, defaultValue, space: spaceProp, dividers = true, data, ...restProps } = props;
50
+ assert(spaceProp === void 0 || Object.values(normalizeResponsiveValue(spaceProp)).every((spaceValue) => spaceValue === void 0 || validSpaceValues.includes(spaceValue)), `To ensure adequate space for touch targets, 'space' prop values must be one of the following: ${validSpaceValues.map((x) => `"${x}"`).join(", ")}`);
33
51
  assert(tone === void 0 || validTones.includes(tone), `The 'tone' prop should be one of the following: ${validTones.map((x) => `"${x}"`).join(", ")}`);
34
- if (process.env.NODE_ENV !== "production")
35
- /**
36
- * Validate that consumers are not passing `data-*`props,
37
- * which will not work and are not validated by TypeScript.
38
- */
39
- buildDataAttributes_default({
40
- data,
41
- validateRestProps: restProps
42
- });
52
+ assert(value === void 0 || defaultValue === void 0, "Accordion 'defaultValue' cannot be set when 'value' is set. Use 'value' to control the open items, or 'defaultValue' for the initial state.");
53
+ assert(value === void 0 || typeof props.onChange === "function", "Accordion 'onChange' must be set when 'value' is set.");
54
+ const managed = multiple === false || value !== void 0 || defaultValue !== void 0 || props.onChange !== void 0;
55
+ if (process.env.NODE_ENV !== "production") {
56
+ /**
57
+ * Validate that consumers are not passing `data-*`props,
58
+ * which will not work and are not validated by TypeScript.
59
+ */
60
+ buildDataAttributes_default({
61
+ data,
62
+ validateRestProps: restProps
63
+ });
64
+ assertUniqueItemValues(children);
65
+ }
66
+ const normalizedValue = useMemo(() => value === void 0 ? void 0 : normalizeAccordionValue(value, multiple), [multiple, value]);
67
+ const [uncontrolledValue, setUncontrolledValue] = useState(() => normalizeAccordionValue(defaultValue, multiple));
68
+ const openValues = normalizedValue ?? uncontrolledValue;
69
+ const openValuesRef = useRef(openValues);
70
+ const propsRef = useRef(props);
71
+ openValuesRef.current = openValues;
72
+ propsRef.current = props;
73
+ const toggleValue = useCallback((itemValue) => {
74
+ const current = openValuesRef.current;
75
+ const isOpen = current.includes(itemValue);
76
+ let next;
77
+ if (!multiple) next = isOpen ? [] : [itemValue];
78
+ else if (isOpen) next = current.filter((openValue) => openValue !== itemValue);
79
+ else next = [...current, itemValue];
80
+ if (normalizedValue === void 0) setUncontrolledValue(next);
81
+ const p = propsRef.current;
82
+ if (p.multiple === false) p.onChange?.(next[0] ?? "");
83
+ else p.onChange?.(next);
84
+ }, [multiple, normalizedValue]);
43
85
  const contextValue = useMemo(() => ({
44
86
  size,
45
87
  tone,
46
- weight
88
+ weight,
89
+ managed,
90
+ openValues,
91
+ toggleValue
47
92
  }), [
48
93
  size,
49
94
  tone,
50
- weight
95
+ weight,
96
+ managed,
97
+ openValues,
98
+ toggleValue
51
99
  ]);
52
100
  const space = spaceProp ?? defaultSpaceForSize[dividers ? "divided" : "undivided"][size];
53
101
  return /* @__PURE__ */ jsx(AccordionContext.Provider, {
@@ -5,6 +5,13 @@ interface AccordionContextValue {
5
5
  size?: TextProps['size'];
6
6
  tone?: (typeof validTones)[number];
7
7
  weight?: TextProps['weight'];
8
+ /**
9
+ * True when Accordion owns the open items via `multiple={false}`,
10
+ * `value`, `defaultValue`, or `onChange`.
11
+ */
12
+ managed: boolean;
13
+ openValues: readonly string[];
14
+ toggleValue: (value: string) => void;
8
15
  }
9
16
  //#endregion
10
17
  export { AccordionContextValue, validTones };
@@ -5,6 +5,13 @@ interface AccordionContextValue {
5
5
  size?: TextProps['size'];
6
6
  tone?: (typeof validTones)[number];
7
7
  weight?: TextProps['weight'];
8
+ /**
9
+ * True when Accordion owns the open items via `multiple={false}`,
10
+ * `value`, `defaultValue`, or `onChange`.
11
+ */
12
+ managed: boolean;
13
+ openValues: readonly string[];
14
+ toggleValue: (value: string) => void;
8
15
  }
9
16
  //#endregion
10
17
  export { AccordionContextValue, validTones };
@@ -1,9 +1,9 @@
1
1
  const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
2
2
  const require_buildDataAttributes = require("../private/buildDataAttributes.cjs");
3
3
  const require_Box = require("../Box/Box.cjs");
4
- const require_Stack = require("../Stack/Stack.cjs");
5
4
  const require_AccordionContext = require("./AccordionContext.cjs");
6
5
  require("./Accordion.cjs");
6
+ const require_useFallbackId = require("../../hooks/useFallbackId.cjs");
7
7
  const require_useDisclosure = require("../Disclosure/useDisclosure.cjs");
8
8
  const require_Spread = require("../Spread/Spread.cjs");
9
9
  const require_Text = require("../Text/Text.cjs");
@@ -21,12 +21,21 @@ const itemSpaceForSize = {
21
21
  standard: "medium",
22
22
  large: "medium"
23
23
  };
24
- const AccordionItem = ({ id, label, children, badge, size: sizeProp, tone: toneProp, weight: weightProp, icon, data, ...restProps }) => {
24
+ const minDurationMs = 200;
25
+ const maxDurationMs = 500;
26
+ const pixelsPerSecond = 320;
27
+ const durationMsForHeight = (height) => Math.round(Math.min(maxDurationMs, Math.max(minDurationMs, height / pixelsPerSecond * 1e3)));
28
+ const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
29
+ const isHeightTransition = (event) => event.propertyName === "height" && event.target === event.currentTarget;
30
+ const AccordionItem = ({ id, value, label, children, badge, size: sizeProp, tone: toneProp, weight: weightProp, icon, data, expanded: expandedProp, onToggle, ...restProps }) => {
25
31
  const accordionContext = (0, react.useContext)(require_AccordionContext.AccordionContext);
32
+ const managed = Boolean(accordionContext?.managed);
26
33
  (0, assert.default)(!(accordionContext && sizeProp), "Size cannot be set on AccordionItem when inside Accordion. Size should be set on Accordion instead.");
27
34
  (0, assert.default)(!(accordionContext && toneProp), "Tone cannot be set on AccordionItem when inside Accordion. Tone should be set on Accordion instead.");
28
35
  (0, assert.default)(!(accordionContext && weightProp), "Weight cannot be set on AccordionItem when inside Accordion. Weight should be set on Accordion instead.");
29
- (0, assert.default)(toneProp === void 0 || require_AccordionContext.validTones.includes(toneProp), `The 'tone' prop should be one of the following: ${require_AccordionContext.validTones.map((x) => `"${x}"`).join(", ")}`);
36
+ (0, assert.default)(toneProp === void 0 || require_AccordionContext.validTones.includes(toneProp), `The 'tone' prop should be one of the following: ${require_AccordionContext.validTones.map((tone) => `"${tone}"`).join(", ")}`);
37
+ (0, assert.default)(!managed || typeof value === "string" && value.length > 0, "AccordionItem 'value' must be a non-empty string when Accordion controls which items are open.");
38
+ (0, assert.default)(!(managed && expandedProp !== void 0), "expanded cannot be set on AccordionItem when Accordion controls which items are open. Use 'value' and 'onChange' on Accordion.");
30
39
  (0, assert.default)(!badge || badge.type.__isBadge__, `AccordionItem badge prop can only be an instance of Badge. e.g. <AccordionItem badge={<Badge>New</Badge>}>`);
31
40
  (0, assert.default)(!badge || badge.props.bleedY === void 0, "Badge elements cannot set the 'bleedY' prop when passed to an AccordionItem component");
32
41
  (0, assert.default)(!icon || icon.props.size === void 0 && icon.props.tone === void 0, "Icons cannot set the 'size' or 'tone' prop when passed to an AccordionItem component");
@@ -34,14 +43,64 @@ const AccordionItem = ({ id, label, children, badge, size: sizeProp, tone: toneP
34
43
  const tone = accordionContext?.tone ?? toneProp ?? "neutral";
35
44
  const weight = accordionContext?.weight ?? weightProp ?? "medium";
36
45
  const itemSpace = itemSpaceForSize[size] ?? "none";
46
+ const contentRef = (0, react.useRef)(null);
47
+ const contentSizeRef = (0, react.useRef)(null);
37
48
  (0, assert.default)(typeof label === "undefined" || typeof label === "string", "Label must be a string");
49
+ const resolvedId = require_useFallbackId.useFallbackId(id);
50
+ let disclosureState = { onToggle };
51
+ if (managed && accordionContext && value) disclosureState = {
52
+ expanded: accordionContext.openValues.includes(value),
53
+ onToggle: (nextExpanded) => {
54
+ accordionContext.toggleValue(value);
55
+ onToggle?.(nextExpanded);
56
+ }
57
+ };
58
+ else if (expandedProp !== void 0) disclosureState = {
59
+ expanded: expandedProp,
60
+ onToggle
61
+ };
38
62
  const { expanded, buttonProps, contentProps } = require_useDisclosure.useDisclosure({
39
- id,
40
- ...restProps.expanded !== void 0 ? {
41
- onToggle: restProps.onToggle,
42
- expanded: restProps.expanded
43
- } : { onToggle: restProps.onToggle }
63
+ id: resolvedId,
64
+ ...disclosureState
44
65
  });
66
+ const [trackedExpanded, setTrackedExpanded] = (0, react.useState)(expanded);
67
+ const [animatedHeight, setAnimatedHeight] = (0, react.useState)(null);
68
+ const [animationDurationMs, setAnimationDurationMs] = (0, react.useState)(minDurationMs);
69
+ const isAnimating = animatedHeight !== null;
70
+ if (expanded !== trackedExpanded) {
71
+ setTrackedExpanded(expanded);
72
+ if (prefersReducedMotion()) setAnimatedHeight(null);
73
+ else {
74
+ const height = contentSizeRef.current?.scrollHeight ?? 0;
75
+ setAnimatedHeight(height);
76
+ setAnimationDurationMs(durationMsForHeight(height));
77
+ }
78
+ }
79
+ const finishAnimation = (0, react.useCallback)(() => {
80
+ setAnimatedHeight(null);
81
+ }, []);
82
+ const finishHeightTransition = (0, react.useCallback)((event) => {
83
+ if (isHeightTransition(event)) finishAnimation();
84
+ }, [finishAnimation]);
85
+ (0, react.useLayoutEffect)(() => {
86
+ contentRef.current?.toggleAttribute("inert", !expanded);
87
+ }, [expanded]);
88
+ (0, react.useLayoutEffect)(() => {
89
+ if (!isAnimating) return;
90
+ if (!expanded) {
91
+ contentRef.current?.getBoundingClientRect();
92
+ setAnimatedHeight(0);
93
+ }
94
+ const timeoutId = window.setTimeout(finishAnimation, animationDurationMs + 50);
95
+ return () => {
96
+ window.clearTimeout(timeoutId);
97
+ };
98
+ }, [
99
+ animationDurationMs,
100
+ expanded,
101
+ finishAnimation,
102
+ isAnimating
103
+ ]);
45
104
  if (process.env.NODE_ENV !== "production")
46
105
  /**
47
106
  * Validate that consumers are not passing `data-*`props,
@@ -51,8 +110,7 @@ const AccordionItem = ({ id, label, children, badge, size: sizeProp, tone: toneP
51
110
  data,
52
111
  validateRestProps: restProps
53
112
  });
54
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_Stack.Stack, {
55
- space: itemSpace,
113
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_Box.Box, {
56
114
  data,
57
115
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Box.Box, {
58
116
  position: "relative",
@@ -91,9 +149,25 @@ const AccordionItem = ({ id, label, children, badge, size: sizeProp, tone: toneP
91
149
  })
92
150
  })
93
151
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Box.Box, {
94
- display: expanded ? "block" : "none",
152
+ className: {
153
+ [require_AccordionItem_css.content]: true,
154
+ [require_AccordionItem_css.contentOpen]: expanded && !isAnimating,
155
+ [require_AccordionItem_css.contentClosed]: !expanded && !isAnimating
156
+ },
157
+ style: isAnimating ? {
158
+ height: animatedHeight,
159
+ transitionDuration: `${animationDurationMs}ms`
160
+ } : void 0,
161
+ onTransitionEnd: finishHeightTransition,
162
+ onTransitionCancel: finishHeightTransition,
163
+ "aria-hidden": expanded ? void 0 : true,
164
+ ref: contentRef,
95
165
  ...contentProps,
96
- children
166
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Box.Box, {
167
+ ref: contentSizeRef,
168
+ paddingTop: itemSpace,
169
+ children
170
+ })
97
171
  })]
98
172
  });
99
173
  };
@@ -7,7 +7,22 @@ let _vanilla_extract_css = require("@vanilla-extract/css");
7
7
  (0, _vanilla_extract_css_fileScope.setFileScope)("src/lib/components/Accordion/AccordionItem.css.ts", "braid-design-system");
8
8
  const button = (0, _vanilla_extract_css.style)([{}, require_virtualTouchable_css.virtualTouchable], "button");
9
9
  const focusRing = (0, _vanilla_extract_css.style)([{ outlineOffset: require_vars_css.vars.space.xxsmall }, require_atoms.atoms({ borderRadius: "small" })], "focusRing");
10
+ const content = (0, _vanilla_extract_css.style)([require_atoms.atoms({ overflow: "hidden" }), {
11
+ transition: "height 200ms ease",
12
+ "@media": { "screen and (prefers-reduced-motion: reduce)": { transition: "none" } }
13
+ }], "content");
14
+ const contentOpen = (0, _vanilla_extract_css.style)([require_atoms.atoms({ overflow: "visible" }), {
15
+ height: "auto",
16
+ transition: "none"
17
+ }], "contentOpen");
18
+ const contentClosed = (0, _vanilla_extract_css.style)({
19
+ height: 0,
20
+ visibility: "hidden"
21
+ }, "contentClosed");
10
22
  (0, _vanilla_extract_css_fileScope.endFileScope)();
11
23
  //#endregion
12
24
  exports.button = button;
25
+ exports.content = content;
26
+ exports.contentClosed = contentClosed;
27
+ exports.contentOpen = contentOpen;
13
28
  exports.focusRing = focusRing;
@@ -7,6 +7,18 @@ import { style } from "@vanilla-extract/css";
7
7
  setFileScope("src/lib/components/Accordion/AccordionItem.css.ts", "braid-design-system");
8
8
  const button = style([{}, virtualTouchable], "button");
9
9
  const focusRing = style([{ outlineOffset: vars.space.xxsmall }, atoms({ borderRadius: "small" })], "focusRing");
10
+ const content = style([atoms({ overflow: "hidden" }), {
11
+ transition: "height 200ms ease",
12
+ "@media": { "screen and (prefers-reduced-motion: reduce)": { transition: "none" } }
13
+ }], "content");
14
+ const contentOpen = style([atoms({ overflow: "visible" }), {
15
+ height: "auto",
16
+ transition: "none"
17
+ }], "contentOpen");
18
+ const contentClosed = style({
19
+ height: 0,
20
+ visibility: "hidden"
21
+ }, "contentClosed");
10
22
  endFileScope();
11
23
  //#endregion
12
- export { button, focusRing };
24
+ export { button, content, contentClosed, contentOpen, focusRing };
@@ -14,6 +14,11 @@ interface AccordionItemBaseProps {
14
14
  icon?: TextProps['icon'];
15
15
  data?: DataAttributeMap;
16
16
  badge?: ReactElement<BadgeProps> | null;
17
+ /**
18
+ * Key used when Accordion owns the open items.
19
+ * Required when `multiple` is `false`, or when `value`, `defaultValue`, or `onChange` is set on Accordion.
20
+ */
21
+ value?: string;
17
22
  }
18
23
  type AccordionItemProps = AccordionItemBaseProps & UseDisclosureProps;
19
24
  type AccordionItemStateProps = DisclosureStateProps;