@basic-ui/core 0.0.60 → 0.0.62

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 (214) hide show
  1. package/build/cjs/index.js.map +1 -1
  2. package/build/esm/Accordion/AccordionBody.d.ts.map +1 -1
  3. package/build/esm/Accordion/AccordionBody.js +6 -26
  4. package/build/esm/Accordion/AccordionBody.js.map +1 -1
  5. package/build/esm/Accordion/AccordionHeader.d.ts.map +1 -1
  6. package/build/esm/Accordion/AccordionHeader.js +21 -69
  7. package/build/esm/Accordion/AccordionHeader.js.map +1 -1
  8. package/build/esm/Accordion/AccordionItem.d.ts.map +1 -1
  9. package/build/esm/Accordion/AccordionItem.js +31 -18
  10. package/build/esm/Accordion/AccordionItem.js.map +1 -1
  11. package/build/esm/Accordion/context.d.ts +0 -8
  12. package/build/esm/Accordion/context.d.ts.map +1 -1
  13. package/build/esm/Accordion/context.js +0 -11
  14. package/build/esm/Accordion/context.js.map +1 -1
  15. package/build/esm/Accordion/scopeQuery.d.ts +1 -0
  16. package/build/esm/Accordion/scopeQuery.d.ts.map +1 -1
  17. package/build/esm/Accordion/scopeQuery.js +3 -0
  18. package/build/esm/Accordion/scopeQuery.js.map +1 -1
  19. package/build/esm/Collapsible/Collapsible.d.ts +13 -0
  20. package/build/esm/Collapsible/Collapsible.d.ts.map +1 -0
  21. package/build/esm/Collapsible/Collapsible.js +53 -0
  22. package/build/esm/Collapsible/Collapsible.js.map +1 -0
  23. package/build/esm/Collapsible/CollapsiblePanel.d.ts +10 -0
  24. package/build/esm/Collapsible/CollapsiblePanel.d.ts.map +1 -0
  25. package/build/esm/Collapsible/CollapsiblePanel.js +85 -0
  26. package/build/esm/Collapsible/CollapsiblePanel.js.map +1 -0
  27. package/build/esm/Collapsible/CollapsibleTrigger.d.ts +11 -0
  28. package/build/esm/Collapsible/CollapsibleTrigger.d.ts.map +1 -0
  29. package/build/esm/Collapsible/CollapsibleTrigger.js +51 -0
  30. package/build/esm/Collapsible/CollapsibleTrigger.js.map +1 -0
  31. package/build/esm/Collapsible/context.d.ts +16 -0
  32. package/build/esm/Collapsible/context.d.ts.map +1 -0
  33. package/build/esm/Collapsible/context.js +11 -0
  34. package/build/esm/Collapsible/context.js.map +1 -0
  35. package/build/esm/Collapsible/index.d.ts +4 -0
  36. package/build/esm/Collapsible/index.d.ts.map +1 -0
  37. package/build/esm/Collapsible/index.js +4 -0
  38. package/build/esm/Collapsible/index.js.map +1 -0
  39. package/build/esm/Menu/Menu.d.ts +3 -2
  40. package/build/esm/Menu/Menu.d.ts.map +1 -1
  41. package/build/esm/Menu/Menu.js +64 -4
  42. package/build/esm/Menu/Menu.js.map +1 -1
  43. package/build/esm/Menu/MenuButton.d.ts.map +1 -1
  44. package/build/esm/Menu/MenuButton.js +85 -8
  45. package/build/esm/Menu/MenuButton.js.map +1 -1
  46. package/build/esm/Menu/MenuItem.d.ts.map +1 -1
  47. package/build/esm/Menu/MenuItem.js +16 -4
  48. package/build/esm/Menu/MenuItem.js.map +1 -1
  49. package/build/esm/Menu/MenuList.d.ts.map +1 -1
  50. package/build/esm/Menu/MenuList.js +47 -12
  51. package/build/esm/Menu/MenuList.js.map +1 -1
  52. package/build/esm/Menu/MenuPopover.d.ts.map +1 -1
  53. package/build/esm/Menu/MenuPopover.js +12 -1
  54. package/build/esm/Menu/MenuPopover.js.map +1 -1
  55. package/build/esm/Menu/MenuSubmenuTrigger.d.ts +8 -0
  56. package/build/esm/Menu/MenuSubmenuTrigger.d.ts.map +1 -0
  57. package/build/esm/Menu/MenuSubmenuTrigger.js +131 -0
  58. package/build/esm/Menu/MenuSubmenuTrigger.js.map +1 -0
  59. package/build/esm/Menu/context.d.ts +13 -3
  60. package/build/esm/Menu/context.d.ts.map +1 -1
  61. package/build/esm/Menu/context.js +1 -0
  62. package/build/esm/Menu/context.js.map +1 -1
  63. package/build/esm/Menu/index.d.ts +3 -0
  64. package/build/esm/Menu/index.d.ts.map +1 -1
  65. package/build/esm/Menu/index.js +2 -0
  66. package/build/esm/Menu/index.js.map +1 -1
  67. package/build/esm/Menu/scope.d.ts +1 -0
  68. package/build/esm/Menu/scope.d.ts.map +1 -1
  69. package/build/esm/Menu/scope.js +2 -1
  70. package/build/esm/Menu/scope.js.map +1 -1
  71. package/build/esm/MenuBar/MenuBar.d.ts +11 -0
  72. package/build/esm/MenuBar/MenuBar.d.ts.map +1 -0
  73. package/build/esm/MenuBar/MenuBar.js +153 -0
  74. package/build/esm/MenuBar/MenuBar.js.map +1 -0
  75. package/build/esm/MenuBar/context.d.ts +29 -0
  76. package/build/esm/MenuBar/context.d.ts.map +1 -0
  77. package/build/esm/MenuBar/context.js +7 -0
  78. package/build/esm/MenuBar/context.js.map +1 -0
  79. package/build/esm/MenuBar/index.d.ts +2 -0
  80. package/build/esm/MenuBar/index.d.ts.map +1 -0
  81. package/build/esm/MenuBar/index.js +2 -0
  82. package/build/esm/MenuBar/index.js.map +1 -0
  83. package/build/esm/Slider/Slider.d.ts +47 -1
  84. package/build/esm/Slider/Slider.d.ts.map +1 -1
  85. package/build/esm/Slider/Slider.js +91 -5
  86. package/build/esm/Slider/Slider.js.map +1 -1
  87. package/build/esm/ToggleGroup/ToggleGroup.d.ts +40 -0
  88. package/build/esm/ToggleGroup/ToggleGroup.d.ts.map +1 -0
  89. package/build/esm/ToggleGroup/ToggleGroup.js +113 -0
  90. package/build/esm/ToggleGroup/ToggleGroup.js.map +1 -0
  91. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts +10 -0
  92. package/build/esm/ToggleGroup/ToggleGroupContext.d.ts.map +1 -0
  93. package/build/esm/ToggleGroup/ToggleGroupContext.js +6 -0
  94. package/build/esm/ToggleGroup/ToggleGroupContext.js.map +1 -0
  95. package/build/esm/ToggleGroup/index.d.ts +3 -0
  96. package/build/esm/ToggleGroup/index.d.ts.map +1 -0
  97. package/build/esm/ToggleGroup/index.js +3 -0
  98. package/build/esm/ToggleGroup/index.js.map +1 -0
  99. package/build/esm/Tree/Tree.d.ts +3 -0
  100. package/build/esm/Tree/Tree.d.ts.map +1 -0
  101. package/build/esm/Tree/Tree.js +730 -0
  102. package/build/esm/Tree/Tree.js.map +1 -0
  103. package/build/esm/Tree/TreeHeader.d.ts +3 -0
  104. package/build/esm/Tree/TreeHeader.d.ts.map +1 -0
  105. package/build/esm/Tree/TreeHeader.js +5 -0
  106. package/build/esm/Tree/TreeHeader.js.map +1 -0
  107. package/build/esm/Tree/TreeItem.d.ts +3 -0
  108. package/build/esm/Tree/TreeItem.d.ts.map +1 -0
  109. package/build/esm/Tree/TreeItem.js +5 -0
  110. package/build/esm/Tree/TreeItem.js.map +1 -0
  111. package/build/esm/Tree/TreeItemContent.d.ts +3 -0
  112. package/build/esm/Tree/TreeItemContent.d.ts.map +1 -0
  113. package/build/esm/Tree/TreeItemContent.js +69 -0
  114. package/build/esm/Tree/TreeItemContent.js.map +1 -0
  115. package/build/esm/Tree/TreeSection.d.ts +3 -0
  116. package/build/esm/Tree/TreeSection.d.ts.map +1 -0
  117. package/build/esm/Tree/TreeSection.js +5 -0
  118. package/build/esm/Tree/TreeSection.js.map +1 -0
  119. package/build/esm/Tree/collection.d.ts +18 -0
  120. package/build/esm/Tree/collection.d.ts.map +1 -0
  121. package/build/esm/Tree/collection.js +252 -0
  122. package/build/esm/Tree/collection.js.map +1 -0
  123. package/build/esm/Tree/context.d.ts +3 -0
  124. package/build/esm/Tree/context.d.ts.map +1 -0
  125. package/build/esm/Tree/context.js +3 -0
  126. package/build/esm/Tree/context.js.map +1 -0
  127. package/build/esm/Tree/index.d.ts +8 -0
  128. package/build/esm/Tree/index.d.ts.map +1 -0
  129. package/build/esm/Tree/index.js +7 -0
  130. package/build/esm/Tree/index.js.map +1 -0
  131. package/build/esm/Tree/types.d.ts +128 -0
  132. package/build/esm/Tree/types.d.ts.map +1 -0
  133. package/build/esm/Tree/types.js +2 -0
  134. package/build/esm/Tree/types.js.map +1 -0
  135. package/build/esm/hooks/index.d.ts +1 -0
  136. package/build/esm/hooks/index.d.ts.map +1 -1
  137. package/build/esm/hooks/index.js +1 -0
  138. package/build/esm/hooks/index.js.map +1 -1
  139. package/build/esm/hooks/useTransitionStatus.d.ts +7 -0
  140. package/build/esm/hooks/useTransitionStatus.d.ts.map +1 -0
  141. package/build/esm/hooks/useTransitionStatus.js +48 -0
  142. package/build/esm/hooks/useTransitionStatus.js.map +1 -0
  143. package/build/esm/index.d.ts +5 -0
  144. package/build/esm/index.d.ts.map +1 -1
  145. package/build/esm/index.js +5 -0
  146. package/build/esm/index.js.map +1 -1
  147. package/build/esm/toggle/Toggle.d.ts +28 -0
  148. package/build/esm/toggle/Toggle.d.ts.map +1 -0
  149. package/build/esm/toggle/Toggle.js +55 -0
  150. package/build/esm/toggle/Toggle.js.map +1 -0
  151. package/build/esm/toggle/index.d.ts +2 -0
  152. package/build/esm/toggle/index.d.ts.map +1 -0
  153. package/build/esm/toggle/index.js +2 -0
  154. package/build/esm/toggle/index.js.map +1 -0
  155. package/build/esm/utils/assign-ref.d.ts +3 -3
  156. package/build/esm/utils/assign-ref.d.ts.map +1 -1
  157. package/build/esm/utils/assign-ref.js +1 -1
  158. package/build/esm/utils/assign-ref.js.map +1 -1
  159. package/build/tsconfig-build.tsbuildinfo +1 -1
  160. package/build/tsconfig.tsbuildinfo +1 -1
  161. package/package.json +7 -4
  162. package/src/Accordion/AccordionBody.tsx +6 -35
  163. package/src/Accordion/AccordionHeader.tsx +29 -103
  164. package/src/Accordion/AccordionItem.tsx +40 -29
  165. package/src/Accordion/context.ts +0 -18
  166. package/src/Accordion/scopeQuery.ts +4 -0
  167. package/src/Collapsible/Collapsible.story.tsx +153 -0
  168. package/src/Collapsible/Collapsible.tsx +79 -0
  169. package/src/Collapsible/CollapsiblePanel.tsx +103 -0
  170. package/src/Collapsible/CollapsibleTrigger.tsx +60 -0
  171. package/src/Collapsible/context.ts +28 -0
  172. package/src/Collapsible/index.ts +3 -0
  173. package/src/Menu/Menu.story.tsx +70 -1
  174. package/src/Menu/Menu.tsx +141 -65
  175. package/src/Menu/MenuButton.tsx +115 -9
  176. package/src/Menu/MenuItem.tsx +20 -3
  177. package/src/Menu/MenuList.tsx +50 -13
  178. package/src/Menu/MenuPopover.tsx +12 -2
  179. package/src/Menu/MenuSubmenuTrigger.tsx +167 -0
  180. package/src/Menu/context.ts +20 -10
  181. package/src/Menu/index.ts +3 -0
  182. package/src/Menu/scope.ts +4 -1
  183. package/src/Menu/styles.css +57 -22
  184. package/src/MenuBar/MenuBar.story.tsx +92 -0
  185. package/src/MenuBar/MenuBar.tsx +236 -0
  186. package/src/MenuBar/context.ts +46 -0
  187. package/src/MenuBar/index.ts +1 -0
  188. package/src/MenuBar/styles.css +78 -0
  189. package/src/Slider/Slider.story.tsx +1 -1
  190. package/src/Slider/Slider.tsx +145 -8
  191. package/src/Toggle/Toggle.story.tsx +42 -0
  192. package/src/Toggle/Toggle.tsx +95 -0
  193. package/src/Toggle/index.ts +1 -0
  194. package/src/Toggle/styles.css +39 -0
  195. package/src/ToggleGroup/ToggleGroup.story.tsx +86 -0
  196. package/src/ToggleGroup/ToggleGroup.tsx +185 -0
  197. package/src/ToggleGroup/ToggleGroupContext.ts +17 -0
  198. package/src/ToggleGroup/index.ts +2 -0
  199. package/src/ToggleGroup/styles.css +66 -0
  200. package/src/Tree/Tree.story.tsx +221 -0
  201. package/src/Tree/Tree.tsx +1081 -0
  202. package/src/Tree/TreeHeader.tsx +9 -0
  203. package/src/Tree/TreeItem.tsx +9 -0
  204. package/src/Tree/TreeItemContent.tsx +91 -0
  205. package/src/Tree/TreeSection.tsx +9 -0
  206. package/src/Tree/collection.tsx +371 -0
  207. package/src/Tree/context.ts +6 -0
  208. package/src/Tree/index.ts +7 -0
  209. package/src/Tree/styles.css +135 -0
  210. package/src/Tree/types.ts +161 -0
  211. package/src/hooks/index.ts +1 -0
  212. package/src/hooks/useTransitionStatus.ts +65 -0
  213. package/src/index.ts +5 -0
  214. package/src/utils/assign-ref.ts +4 -4
@@ -1,16 +1,10 @@
1
- import type {
2
- HTMLAttributes,
3
- ElementType,
4
- ReactNode,
5
- MouseEvent,
6
- KeyboardEvent,
7
- FocusEvent,
8
- } from 'react';
9
- import { forwardRef, useRef, useState, useEffect } from 'react';
1
+ import type { HTMLAttributes, ElementType, ReactNode, KeyboardEvent, FocusEvent } from 'react';
2
+ import { forwardRef } from 'react';
10
3
 
11
- import { wrapEvent, assignMultipleRefs, getCircularIndex } from '../utils';
12
- import { useAccordionContext, useAccordionItemContext } from './context';
13
- import { headerScopeQuery as scopeQuery } from './scopeQuery';
4
+ import { wrapEvent, getCircularIndex } from '../utils';
5
+ import { useAccordionContext } from './context';
6
+ import { CollapsibleTrigger } from '../Collapsible';
7
+ import { headerScopeQuery } from './scopeQuery';
14
8
 
15
9
  export interface AccordionHeaderProps extends HTMLAttributes<HTMLDivElement> {
16
10
  as?: ElementType<any>;
@@ -23,57 +17,15 @@ export const AccordionHeader = forwardRef<HTMLDivElement, AccordionHeaderProps>(
23
17
  const {
24
18
  as: Comp = 'div',
25
19
  onKeyDown,
26
- onClick: onClickProp,
27
20
  onFocus,
28
21
  onBlur,
29
22
  ...otherProps
30
23
  } = props;
24
+
31
25
  const accordionContext = useAccordionContext();
32
- const accordionItemContext = useAccordionItemContext();
33
- const ref = useRef<HTMLDivElement | null>(null);
34
- const [index, setIndex] = useState<number | undefined>();
35
-
36
- if (!accordionItemContext) {
37
- throw new Error('Missing parent <Accordion /> component');
38
- }
39
-
40
- useEffect(() => {
41
- if (accordionContext) {
42
- const allHeaders =
43
- accordionContext.scope.current.queryAllNodes(scopeQuery) || [];
44
-
45
- const index = allHeaders.findIndex((e) => e === ref.current);
46
- setIndex(index);
47
- }
48
- }, [accordionContext]);
49
-
50
- const onClick = wrapEvent(onClickProp, (e: MouseEvent<HTMLDivElement>) => {
51
- let index = 0;
52
- if (accordionItemContext.expanded) {
53
- index = -1;
54
- } else if (accordionContext) {
55
- const allHeaders =
56
- accordionContext.scope.current.queryAllNodes(scopeQuery) || [];
57
-
58
- index = allHeaders.findIndex((e) => e === ref.current);
59
- if (index === accordionContext.expandedIndex) {
60
- index = -1;
61
- }
62
- accordionContext.onChange && accordionContext.onChange(e, index);
63
- }
64
-
65
- accordionItemContext.onChange &&
66
- accordionItemContext.onChange(e, index >= 0);
67
- });
68
26
 
69
27
  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
70
28
  switch (e.key) {
71
- case 'Enter':
72
- case ' ': {
73
- onClick(e as unknown as MouseEvent<HTMLDivElement>);
74
- e.preventDefault();
75
- break;
76
- }
77
29
  case 'ArrowUp':
78
30
  case 'ArrowDown':
79
31
  case 'Home':
@@ -81,85 +33,59 @@ export const AccordionHeader = forwardRef<HTMLDivElement, AccordionHeaderProps>(
81
33
  if (!accordionContext) {
82
34
  return;
83
35
  }
84
- const allHeaders =
85
- accordionContext.scope.current.queryAllNodes(scopeQuery);
86
-
87
- e.preventDefault();
88
-
36
+ const allHeaders = accordionContext.scope.current.queryAllNodes(headerScopeQuery);
37
+
89
38
  if (allHeaders.length === 0) {
90
39
  return;
91
40
  }
92
41
 
93
- let nextIndex = allHeaders.findIndex((e) => e === ref.current);
42
+ e.preventDefault();
43
+ let nextIndex = allHeaders.findIndex(el => el === e.currentTarget);
44
+
94
45
  switch (e.key) {
95
- case 'ArrowUp':
96
- nextIndex += -1;
97
- break;
98
- case 'ArrowDown':
99
- nextIndex += +1;
100
- break;
101
- case 'Home':
102
- nextIndex = 0;
103
- break;
104
- case 'End':
105
- nextIndex = -1;
106
- break;
46
+ case 'ArrowUp': nextIndex -= 1; break;
47
+ case 'ArrowDown': nextIndex += 1; break;
48
+ case 'Home': nextIndex = 0; break;
49
+ case 'End': nextIndex = -1; break;
107
50
  }
108
51
 
109
- // We're sure it will not be null, because we already checked for allHeaders.length > 0 above
110
52
  nextIndex = getCircularIndex(nextIndex, allHeaders.length)!;
111
- allHeaders[nextIndex] && allHeaders[nextIndex].focus();
53
+ allHeaders[nextIndex]?.focus();
112
54
  break;
113
55
  }
114
- default:
115
- return;
116
56
  }
117
57
  };
118
58
 
119
59
  const handleFocus = () => {
120
- if (accordionContext) {
121
- if (!accordionContext.childrenHeaderHasFocus) {
122
- // this is needed to avoid rerendering the parent and
123
- // messing up with the internal count for children/parent count
124
- accordionContext.setChildrenHeaderHasFocus(true);
125
- }
60
+ if (accordionContext && !accordionContext.childrenHeaderHasFocus) {
61
+ accordionContext.setChildrenHeaderHasFocus(true);
126
62
  }
127
63
  };
128
64
 
129
65
  const handleBlur = (e: FocusEvent<HTMLDivElement>) => {
130
66
  if (accordionContext) {
131
- const allHeaders =
132
- accordionContext.scope.current.queryAllNodes(scopeQuery);
133
- const newFocusIsHeader =
134
- allHeaders.findIndex((header) => header === e.relatedTarget) >= 0;
135
-
136
- // only remove focus flag if the focus went to some element
137
- // that is not an accordion header
67
+ const allHeaders = accordionContext.scope.current.queryAllNodes(headerScopeQuery);
68
+ const newFocusIsHeader = allHeaders.findIndex(header => header === e.relatedTarget) >= 0;
69
+
138
70
  if (!newFocusIsHeader) {
139
71
  accordionContext.setChildrenHeaderHasFocus(false);
140
72
  }
141
73
  }
142
74
  };
143
75
 
144
- const expanded = Boolean(
145
- accordionItemContext.expanded ||
146
- (accordionContext && accordionContext.expandedIndex === index)
147
- );
76
+ const isNonButton = Comp !== 'button';
148
77
 
149
78
  return (
150
- <Comp
151
- ref={assignMultipleRefs(ref, forwardedRef)}
152
- {...otherProps}
153
- id={accordionItemContext.headerId}
154
- aria-controls={accordionItemContext.bodyId}
155
- role="button"
79
+ <CollapsibleTrigger
80
+ as={Comp}
81
+ ref={forwardedRef}
156
82
  data-accordion-header=""
157
- tabIndex="0"
83
+ role={isNonButton ? 'button' : undefined}
84
+ tabIndex={isNonButton ? 0 : undefined}
158
85
  onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
159
86
  onFocus={wrapEvent(onFocus, handleFocus)}
160
87
  onBlur={wrapEvent(onBlur, handleBlur)}
161
- onClick={onClick}
162
- aria-expanded={String(expanded)}
88
+ {...otherProps}
163
89
  />
164
90
  );
165
91
  }
@@ -1,50 +1,61 @@
1
- import type {
2
- HTMLAttributes,
3
- ElementType,
4
- ReactNode,
5
- KeyboardEvent,
6
- MouseEvent,
7
- } from 'react';
8
- import { Fragment, forwardRef, useId } from 'react';
1
+ import type { HTMLAttributes, ElementType, ReactNode, KeyboardEvent, MouseEvent } from 'react';
2
+ import { forwardRef, useRef, useState, useEffect } from 'react';
9
3
 
10
- import type { AccordionItemContextProps } from './context';
11
- import { AccordionItemProvider } from './context';
4
+ import { useAccordionContext } from './context';
5
+ import { Collapsible } from '../Collapsible';
6
+ import { assignMultipleRefs } from '../utils';
7
+ import { itemScopeQuery } from './scopeQuery';
12
8
 
13
- export interface AccordionItemProps
14
- extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
9
+ export interface AccordionItemProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
15
10
  as?: ElementType<any>;
16
11
  innerAs?: ElementType<any>;
17
12
  children?: ReactNode;
18
13
  expanded?: boolean;
19
- onChange?: (
20
- e: KeyboardEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>,
21
- value: boolean
22
- ) => void;
14
+ onChange?: (e: KeyboardEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>, value: boolean) => void;
23
15
  }
24
16
 
25
17
  export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
26
18
  function AccordionItem(props, forwardedRef) {
27
19
  const {
28
- as: Comp = Fragment,
29
- expanded = false,
20
+ as: Comp = 'div',
21
+ innerAs,
22
+ expanded: expandedProp = false,
30
23
  onChange,
31
24
  ...otherProps
32
25
  } = props;
33
- const id = useId();
26
+
27
+ const accordionContext = useAccordionContext();
28
+ const ref = useRef<HTMLDivElement | null>(null);
29
+ const [index, setIndex] = useState<number | undefined>();
34
30
 
35
- const headerId = id ? `accordion-header-${id}` : undefined;
36
- const bodyId = id ? `accordion-body-${id}` : undefined;
37
- const contextValue: AccordionItemContextProps = {
38
- headerId,
39
- bodyId,
40
- expanded,
41
- onChange,
31
+ useEffect(() => {
32
+ if (accordionContext && ref.current) {
33
+ const allItems = accordionContext.scope.current.queryAllNodes(itemScopeQuery) || [];
34
+ setIndex(allItems.findIndex(e => e === ref.current));
35
+ }
36
+ }, [accordionContext]);
37
+
38
+ const isExpanded = Boolean(
39
+ expandedProp || (accordionContext && index !== undefined && accordionContext.expandedIndex === index)
40
+ );
41
+
42
+ const handleOpenChange = (e: any, isOpen: boolean) => {
43
+ onChange?.(e, isOpen);
44
+ if (accordionContext && index !== undefined) {
45
+ accordionContext.onChange?.(e, isOpen ? index : -1);
46
+ }
42
47
  };
43
48
 
44
49
  return (
45
- <AccordionItemProvider value={contextValue}>
46
- <Comp ref={forwardedRef} {...otherProps} />
47
- </AccordionItemProvider>
50
+ <Collapsible
51
+ as={Comp}
52
+ innerAs={innerAs}
53
+ open={isExpanded}
54
+ onChange={handleOpenChange}
55
+ ref={assignMultipleRefs(forwardedRef, ref)}
56
+ data-accordion-item=""
57
+ {...otherProps}
58
+ />
48
59
  );
49
60
  }
50
61
  );
@@ -3,7 +3,6 @@ import { useContext, createContext } from 'react';
3
3
 
4
4
  import type { Scope } from '../hooks/useScope';
5
5
 
6
- // AccordionGroup Component
7
6
  export interface AccordionContextProps {
8
7
  childrenHeaderHasFocus: boolean;
9
8
  setChildrenHeaderHasFocus: (value: boolean) => void;
@@ -18,20 +17,3 @@ export interface AccordionContextProps {
18
17
  const accordionContext = createContext<AccordionContextProps | null>(null);
19
18
  export const { Provider: AccordionProvider } = accordionContext;
20
19
  export const useAccordionContext = () => useContext(accordionContext);
21
-
22
- // Accordion Component
23
- export interface AccordionItemContextProps {
24
- headerId: string | undefined;
25
- bodyId: string | undefined;
26
- expanded: boolean;
27
- onChange?: (
28
- e: KeyboardEvent<HTMLDivElement> | MouseEvent<HTMLDivElement>,
29
- value: boolean
30
- ) => void;
31
- }
32
-
33
- const accordionItemContext = createContext<AccordionItemContextProps | null>(
34
- null
35
- );
36
- export const { Provider: AccordionItemProvider } = accordionItemContext;
37
- export const useAccordionItemContext = () => useContext(accordionItemContext);
@@ -5,3 +5,7 @@ export function headerScopeQuery(type: string, props: Record<string, unknown>) {
5
5
  export function bodyScopeQuery(type: string, props: Record<string, unknown>) {
6
6
  return props['data-accordion-body'] === '';
7
7
  }
8
+
9
+ export function itemScopeQuery(type: string, props: Record<string, unknown>) {
10
+ return props['data-accordion-item'] === '';
11
+ }
@@ -0,0 +1,153 @@
1
+ import { useState } from 'react';
2
+
3
+ import { Collapsible, CollapsibleTrigger, CollapsiblePanel } from './';
4
+
5
+ export default {
6
+ title: 'components/Collapsible',
7
+ };
8
+
9
+ const Wrapper = ({ children }: { children: React.ReactNode }) => {
10
+ return (
11
+ <div
12
+ style={{
13
+ boxSizing: 'border-box',
14
+ display: 'flex',
15
+ alignItems: 'flex-start',
16
+ padding: '24px',
17
+ width: '100%',
18
+ }}
19
+ >
20
+ <div style={{ width: 300 }}>{children}</div>
21
+ </div>
22
+ );
23
+ };
24
+
25
+ export const Default = () => (
26
+ <Wrapper>
27
+ <Collapsible>
28
+ <CollapsibleTrigger
29
+ style={{
30
+ width: '100%',
31
+ padding: '8px 16px',
32
+ textAlign: 'left',
33
+ background: '#f0f0f0',
34
+ border: '1px solid #ccc',
35
+ cursor: 'pointer',
36
+ }}
37
+ >
38
+ Toggle Panel
39
+ </CollapsibleTrigger>
40
+ <CollapsiblePanel
41
+ style={{
42
+ padding: '16px',
43
+ border: '1px solid #ccc',
44
+ borderTop: 'none',
45
+ }}
46
+ >
47
+ This is the collapsible panel content. You can put anything here.
48
+ </CollapsiblePanel>
49
+ </Collapsible>
50
+ </Wrapper>
51
+ );
52
+
53
+ export const Controlled = () => {
54
+ const [open, setOpen] = useState(true);
55
+
56
+ return (
57
+ <Wrapper>
58
+ <div style={{ marginBottom: 16 }}>
59
+ <button onClick={() => setOpen(!open)}>Toggle from outside</button>
60
+ </div>
61
+ <Collapsible open={open} onChange={(e, isOpen) => setOpen(isOpen)}>
62
+ <CollapsibleTrigger
63
+ style={{
64
+ width: '100%',
65
+ padding: '8px 16px',
66
+ textAlign: 'left',
67
+ background: '#e0e0f0',
68
+ border: '1px solid #ccc',
69
+ cursor: 'pointer',
70
+ }}
71
+ >
72
+ Controlled Trigger
73
+ </CollapsibleTrigger>
74
+ <CollapsiblePanel
75
+ style={{
76
+ padding: '16px',
77
+ border: '1px solid #ccc',
78
+ borderTop: 'none',
79
+ }}
80
+ >
81
+ This panel is controlled from outside.
82
+ </CollapsiblePanel>
83
+ </Collapsible>
84
+ </Wrapper>
85
+ );
86
+ };
87
+
88
+ export const Animated = () => {
89
+ return (
90
+ <Wrapper>
91
+ <style>{`
92
+ .AnimatedPanel {
93
+ overflow: hidden;
94
+ }
95
+ .AnimatedPanel[data-state='open'] {
96
+ animation: slideDown 300ms ease-out forwards;
97
+ }
98
+ .AnimatedPanel[data-state='closed'] {
99
+ animation: slideUp 300ms ease-out forwards;
100
+ }
101
+ @keyframes slideDown {
102
+ from {
103
+ height: 0;
104
+ opacity: 0;
105
+ }
106
+ to {
107
+ height: var(--collapsible-panel-height);
108
+ opacity: 1;
109
+ }
110
+ }
111
+ @keyframes slideUp {
112
+ from {
113
+ height: var(--collapsible-panel-height);
114
+ opacity: 1;
115
+ }
116
+ to {
117
+ height: 0;
118
+ opacity: 0;
119
+ }
120
+ }
121
+ `}</style>
122
+ <Collapsible>
123
+ <CollapsibleTrigger
124
+ style={{
125
+ width: '100%',
126
+ padding: '8px 16px',
127
+ textAlign: 'left',
128
+ background: '#e8f0fe',
129
+ border: '1px solid #c0d0f0',
130
+ cursor: 'pointer',
131
+ fontWeight: 'bold',
132
+ }}
133
+ >
134
+ Animated Panel (Click me)
135
+ </CollapsibleTrigger>
136
+ <CollapsiblePanel
137
+ className="AnimatedPanel"
138
+ style={{
139
+ background: '#fafafa',
140
+ border: '1px solid #c0d0f0',
141
+ borderTop: 'none',
142
+ }}
143
+ >
144
+ <div style={{ padding: '16px' }}>
145
+ <p style={{ margin: 0 }}>This panel&apos;s height is animated.</p>
146
+ <p>It uses the <code>--collapsible-panel-height</code> variable exposed by the component.</p>
147
+ <p>Look at how smoothly it opens and closes!</p>
148
+ </div>
149
+ </CollapsiblePanel>
150
+ </Collapsible>
151
+ </Wrapper>
152
+ );
153
+ };
@@ -0,0 +1,79 @@
1
+ import type { ElementType, ReactNode, SyntheticEvent, MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, HTMLAttributes } from 'react';
2
+ import { forwardRef, Fragment, useState, useCallback } from 'react';
3
+ import { CollapsibleProvider } from './context';
4
+ import { useControlledState, useTransitionStatus } from '../hooks';
5
+
6
+ export type CollapsibleTriggerEvent =
7
+ | ReactKeyboardEvent<any>
8
+ | ReactMouseEvent<any>
9
+ | Event
10
+ | SyntheticEvent<any>;
11
+
12
+ export interface CollapsibleProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
13
+ as?: ElementType<any>;
14
+ innerAs?: ElementType<any>;
15
+ children?: ReactNode;
16
+ open?: boolean;
17
+ defaultOpen?: boolean;
18
+ onChange?: (e: CollapsibleTriggerEvent, isOpen: boolean) => void;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export const Collapsible = forwardRef<HTMLElement, CollapsibleProps>(
23
+ function Collapsible(props, forwardedRef) {
24
+ const {
25
+ as: Comp = 'div',
26
+ innerAs,
27
+ open: openProp,
28
+ defaultOpen = false,
29
+ onChange: onChangeProp,
30
+ disabled = false,
31
+ ...otherProps
32
+ } = props;
33
+
34
+ const [openState, setOpenState] = useControlledState(
35
+ openProp,
36
+ onChangeProp,
37
+ defaultOpen,
38
+ (setState) => (e, isOpen) => {
39
+ setState(isOpen);
40
+ }
41
+ );
42
+
43
+ const [panelId, setPanelId] = useState<string | undefined>();
44
+
45
+ const { mounted, setMounted, transitionStatus } = useTransitionStatus(openState, true, true);
46
+
47
+ const onChange = useCallback(
48
+ (e: CollapsibleTriggerEvent, isOpen: boolean) => {
49
+ if (!disabled) {
50
+ setOpenState(e, isOpen);
51
+ }
52
+ },
53
+ [setOpenState, disabled]
54
+ );
55
+
56
+ const contextValue = {
57
+ open: openState,
58
+ mounted,
59
+ transitionStatus,
60
+ setMounted,
61
+ panelId,
62
+ disabled,
63
+ onChange,
64
+ setPanelId,
65
+ };
66
+
67
+ return (
68
+ <CollapsibleProvider value={contextValue}>
69
+ <Comp
70
+ {...(Comp !== Fragment ? { as: innerAs, ref: forwardedRef } : {})}
71
+ data-open={openState ? '' : undefined}
72
+ data-disabled={disabled ? '' : undefined}
73
+ data-state={openState ? 'open' : 'closed'}
74
+ {...otherProps}
75
+ />
76
+ </CollapsibleProvider>
77
+ );
78
+ }
79
+ );
@@ -0,0 +1,103 @@
1
+ import type { ElementType, ReactNode, HTMLAttributes } from 'react';
2
+ import { forwardRef, Fragment, useEffect, useLayoutEffect, useId, useRef, useState } from 'react';
3
+ import { useCollapsibleContext } from './context';
4
+ import { assignMultipleRefs } from '../utils';
5
+
6
+ export interface CollapsiblePanelProps extends HTMLAttributes<HTMLElement> {
7
+ as?: ElementType<any>;
8
+ innerAs?: ElementType<any>;
9
+ children?: ReactNode;
10
+ id?: string;
11
+ keepMounted?: boolean;
12
+ }
13
+
14
+ export const CollapsiblePanel = forwardRef<HTMLElement, CollapsiblePanelProps>(
15
+ function CollapsiblePanel(props, forwardedRef) {
16
+ const {
17
+ as: Comp = 'div',
18
+ innerAs,
19
+ id: idProp,
20
+ keepMounted = false,
21
+ style,
22
+ ...otherProps
23
+ } = props;
24
+
25
+ const { open, mounted, transitionStatus, setMounted, setPanelId, disabled } = useCollapsibleContext();
26
+ const generatedId = useId();
27
+ const panelId = idProp ?? generatedId;
28
+
29
+ const panelRef = useRef<HTMLElement>(null);
30
+ const mergedRef = assignMultipleRefs(panelRef, forwardedRef);
31
+
32
+ const [height, setHeight] = useState<number | undefined>();
33
+ const [width, setWidth] = useState<number | undefined>();
34
+
35
+ useEffect(() => {
36
+ setPanelId(panelId);
37
+ return () => {
38
+ setPanelId(undefined);
39
+ };
40
+ }, [panelId, setPanelId]);
41
+
42
+ useLayoutEffect(() => {
43
+ const node = panelRef.current;
44
+ if (!node) return;
45
+
46
+ if (open || transitionStatus === 'starting' || transitionStatus === 'ending') {
47
+ setHeight(node.scrollHeight);
48
+ setWidth(node.scrollWidth);
49
+ }
50
+ }, [open, transitionStatus]);
51
+
52
+ useEffect(() => {
53
+ if (open || !mounted || transitionStatus !== 'ending') return;
54
+
55
+ const node = panelRef.current;
56
+ if (!node) return;
57
+
58
+ const handleComplete = (e?: Event) => {
59
+ if (e && e.target !== node) return; // Ignore bubbled events
60
+ setMounted(false);
61
+ };
62
+
63
+ node.addEventListener('animationend', handleComplete);
64
+ node.addEventListener('transitionend', handleComplete);
65
+
66
+ const styles = window.getComputedStyle(node);
67
+ const hasAnimation = styles.animationDuration !== '0s' && styles.animationName !== 'none';
68
+ const hasTransition = styles.transitionDuration !== '0s' && styles.transitionDuration !== '';
69
+
70
+ if (!hasAnimation && !hasTransition) {
71
+ setMounted(false);
72
+ }
73
+
74
+ return () => {
75
+ node.removeEventListener('animationend', handleComplete);
76
+ node.removeEventListener('transitionend', handleComplete);
77
+ };
78
+ }, [open, mounted, transitionStatus, setMounted]);
79
+
80
+ if (!mounted && !keepMounted) {
81
+ return null;
82
+ }
83
+
84
+ const hidden = !open && !mounted;
85
+
86
+ return (
87
+ <Comp
88
+ {...(Comp !== Fragment ? { as: innerAs, ref: mergedRef } : {})}
89
+ id={panelId}
90
+ hidden={hidden ? true : undefined}
91
+ data-open={open ? '' : undefined}
92
+ data-disabled={disabled ? '' : undefined}
93
+ data-state={open ? 'open' : 'closed'}
94
+ style={{
95
+ ...style,
96
+ '--collapsible-panel-height': height !== undefined ? `${height}px` : undefined,
97
+ '--collapsible-panel-width': width !== undefined ? `${width}px` : undefined,
98
+ } as React.CSSProperties}
99
+ {...otherProps}
100
+ />
101
+ );
102
+ }
103
+ );