funda-ui 4.7.545 → 4.7.555

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.
@@ -2,13 +2,12 @@ import React, { useState, useRef, useEffect } from 'react';
2
2
 
3
3
  import Item from './AccordionItem';
4
4
 
5
- import animateStyles from 'funda-utils/dist/cjs/anim';
6
5
  import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
7
6
 
8
-
9
7
  // Adapt the easing parameters of TweenMax
10
8
  export enum EasingList {
11
9
  linear = 'linear',
10
+ ease = 'ease',
12
11
  easeIn = 'ease-in',
13
12
  easeOut = 'ease-out',
14
13
  easeInOut = 'ease-in-out'
@@ -18,6 +17,7 @@ export enum EasingList {
18
17
  export type AccordionOptionChangeFnType = (element: HTMLDivElement, index: number) => void;
19
18
 
20
19
 
20
+
21
21
  export type AccordionProps = {
22
22
  wrapperClassName?: string;
23
23
  /** The index of the item to be displayed by default. Set to -1 to display none, or an array of indices to display multiple items */
@@ -46,7 +46,7 @@ const Accordion = (props: AccordionProps) => {
46
46
  defaultActiveIndex,
47
47
  defaultActiveAll = false,
48
48
  duration,
49
- easing,
49
+ easing = 'ease',
50
50
  alternateCollapse = true,
51
51
  arrowOnly = false,
52
52
  onChange,
@@ -56,17 +56,16 @@ const Accordion = (props: AccordionProps) => {
56
56
  const animSpeed = duration || 200;
57
57
  const easeType: string = typeof alternateCollapse === 'undefined' ? EasingList['linear'] : EasingList[easing as never];
58
58
  const rootRef = useRef<any>(null);
59
- const animPlaceholderRef = useRef<HTMLDivElement>(null);
60
- const [animOK, setAnimOK] = useState<boolean>(false);
61
- const [heightObserver, setHeightObserver] = useState<number>(-1);
59
+ const [heightObserver, setHeightObserver] = useState<number[]>([]);
62
60
  const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set()); // Keep track of all expanded items
63
61
  const animationInProgress = useRef<boolean>(false);
62
+ const initExpanded = useRef<boolean>(false);
63
+
64
64
 
65
65
 
66
66
  function handleClickItem(e: React.MouseEvent) {
67
67
  if (animationInProgress.current) return;
68
68
  if ((e.target as any).closest('.custom-accordion-header') === null) return;
69
- if (animOK) return;
70
69
 
71
70
  animationInProgress.current = true;
72
71
 
@@ -74,34 +73,10 @@ const Accordion = (props: AccordionProps) => {
74
73
  const curIndex: number = parseInt(reactDomEl.dataset.index);
75
74
  const reactDomWrapperEl: HTMLElement = rootRef.current;
76
75
  const $li = reactDomWrapperEl.querySelectorAll('.custom-accordion-item');
77
- const $allContent = reactDomWrapperEl.querySelectorAll('.custom-accordion-content__wrapper');
78
- const $curContent = reactDomEl.querySelector('.custom-accordion-content__wrapper');
79
76
 
80
77
  if (reactDomEl.getAttribute('aria-expanded') === 'false' || reactDomEl.getAttribute('aria-expanded') === null) {
81
- setAnimOK(true);
82
- setTimeout(() => {
83
- setAnimOK(false);
84
- }, animSpeed);
85
-
86
78
  if (alternateCollapse) {
87
- // Hide other all sibling content
88
- Array.prototype.forEach.call($allContent, (node) => {
89
- if (node.clientHeight > 0) {
90
- animateStyles(node, {
91
- startHeight: node.scrollHeight,
92
- endHeight: 0,
93
- speed: animSpeed
94
- } as never, easeType, () => {
95
- animationInProgress.current = false;
96
- });
97
- }
98
- });
99
-
100
- // Update all items to collapsed state
101
- Array.prototype.forEach.call($li, (node) => {
102
- node.setAttribute('aria-expanded', 'false');
103
- });
104
-
79
+
105
80
  // Update expanded items state
106
81
  setExpandedItems(new Set([curIndex]));
107
82
  } else {
@@ -109,82 +84,71 @@ const Accordion = (props: AccordionProps) => {
109
84
  setExpandedItems(prev => new Set([...(prev as never), curIndex]));
110
85
  }
111
86
 
112
- reactDomEl.setAttribute('aria-expanded', 'true');
113
-
114
-
115
- animateStyles($curContent, {
116
- startHeight: 0,
117
- endHeight: $curContent.scrollHeight,
118
- speed: animSpeed
119
- } as never, easeType, () => {
120
- setHeightObserver(curIndex);
121
- animationInProgress.current = false;
122
- });
123
87
  } else {
124
- reactDomEl.setAttribute('aria-expanded', 'false');
125
-
88
+
126
89
  // Remove current item from expanded items
127
90
  setExpandedItems(prev => {
128
91
  const newSet = new Set(prev);
129
92
  newSet.delete(curIndex);
130
93
  return newSet;
131
94
  });
132
-
133
- animateStyles($curContent, {
134
- startHeight: $curContent.scrollHeight,
135
- endHeight: 0,
136
- speed: animSpeed
137
- } as never, easeType, () => {
138
- animationInProgress.current = false;
139
- });
140
95
  }
141
96
 
97
+ // Reset animation flag after transition duration
98
+ setTimeout(() => {
99
+ animationInProgress.current = false;
100
+ }, animSpeed);
101
+
142
102
  onChange?.(reactDomEl, curIndex);
143
103
  }
144
104
 
145
105
 
146
- // Initialize expanded items based on defaultActiveIndex or defaultActiveAll
106
+ //Initialize expanded items based on defaultActiveIndex or defaultActiveAll
147
107
  useEffect(() => {
148
- if (defaultActiveAll && children && rootRef.current && animPlaceholderRef.current) {
108
+
109
+ // Skip the default height initialization to prevent re-initialization.
110
+ // This method is useful when you want to manually control when the accordion should
111
+ // skip the automatic height initialization process, especially useful when `children` change frequently.
112
+ if (initExpanded.current === true) return;
113
+
114
+ if (defaultActiveAll && children && rootRef.current) {
149
115
  const allIndices = Array.from({ length: (children as any[]).length }, (_, i) => i);
150
116
  setExpandedItems(new Set(allIndices));
117
+ // Set heightObserver for all items to enable height monitoring
118
+ setHeightObserver(allIndices);
151
119
 
152
- // Actually expand all items without animation
153
- const $allItems = rootRef.current.querySelectorAll('.custom-accordion-item');
154
- Array.prototype.forEach.call($allItems, (node, index) => {
155
- const $curContent = node.querySelector('.custom-accordion-content__wrapper');
156
- if ($curContent) {
157
- // !!! Don't use the .custom-accordion-contentwrapper height directly, it may be more than a dozen pixels
158
- $curContent.style.height = `${node.querySelector('.custom-accordion-content__wrapper > .custom-accordion-content').scrollHeight}px`;
159
- }
160
- });
161
-
162
-
163
- animateStyles(animPlaceholderRef.current as HTMLDivElement, {
164
- startHeight: 0,
165
- endHeight: 10,
166
- speed: animSpeed
167
- } as never, easeType, () => {
168
- setTimeout(() => {
169
-
170
- }, animSpeed);
171
- });
172
-
173
-
120
+ //
121
+ initExpanded.current = true;
174
122
  } else if (defaultActiveIndex !== undefined) {
175
123
  const initialExpanded = new Set<number>();
124
+ const observerIndices: number[] = [];
125
+
176
126
  if (Array.isArray(defaultActiveIndex)) {
177
- defaultActiveIndex.forEach(index => initialExpanded.add(index));
127
+ defaultActiveIndex.forEach(index => {
128
+ initialExpanded.add(index);
129
+ observerIndices.push(index);
130
+ });
178
131
  } else if (typeof defaultActiveIndex === 'number') {
179
132
  initialExpanded.add(defaultActiveIndex);
133
+ observerIndices.push(defaultActiveIndex);
180
134
  }
135
+
181
136
  setExpandedItems(initialExpanded);
137
+ // Set heightObserver for default active items to enable height monitoring
138
+ if (observerIndices.length > 0) {
139
+ setHeightObserver(observerIndices);
140
+ }
141
+
142
+ //
143
+ initExpanded.current = true;
144
+
182
145
  }
146
+
183
147
  }, [defaultActiveIndex, defaultActiveAll, children]);
184
148
 
149
+
185
150
  return (
186
151
  <>
187
- <div className="custom-accordion-anim-placeholder" style={{display: 'none'}} ref={animPlaceholderRef}></div>
188
152
  <div className={combinedCls(
189
153
  'custom-accordion-item',
190
154
  clsWrite(wrapperClassName, 'accordion')
@@ -204,6 +168,7 @@ const Accordion = (props: AccordionProps) => {
204
168
  index={i}
205
169
  defaultActiveAll={defaultActiveAll}
206
170
  animSpeed={animSpeed}
171
+ easing={easeType}
207
172
  arrowOnly={arrowOnly}
208
173
  heightObserver={heightObserver}
209
174
  activeItem={_defaultActive}
@@ -4,11 +4,11 @@ import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
4
4
 
5
5
 
6
6
  export type AccordionItemProps = {
7
- heightObserver?: number;
7
+ heightObserver?: number[];
8
8
  index?: number;
9
9
  arrowOnly?: boolean;
10
10
  animSpeed?: number;
11
- easeType?: string;
11
+ easing?: string;
12
12
  /** Class of items */
13
13
  itemClassName?: string;
14
14
  itemContentWrapperClassName?: string;
@@ -41,7 +41,7 @@ const AccordionItem = (props: AccordionItemProps) => {
41
41
  heightObserver,
42
42
  index,
43
43
  animSpeed,
44
- easeType,
44
+ easing,
45
45
  arrowOnly,
46
46
  itemClassName,
47
47
  itemContentWrapperClassName,
@@ -83,6 +83,19 @@ const AccordionItem = (props: AccordionItemProps) => {
83
83
  onToggleEv?.(e);
84
84
  };
85
85
 
86
+
87
+ // Generate CSS transition style
88
+ const getTransitionStyle = () => {
89
+ const transitionDuration = `${animSpeed}ms`;
90
+ const transitionTiming = easing;
91
+
92
+ return {
93
+ transition: `max-height ${transitionDuration} ${transitionTiming}`,
94
+ overflow: 'hidden'
95
+ };
96
+ };
97
+
98
+
86
99
  useEffect(() => {
87
100
  if (triggerRef.current && typeof onItemCollapse === 'function') {
88
101
  if (isFirstRender.current) {
@@ -91,7 +104,7 @@ const AccordionItem = (props: AccordionItemProps) => {
91
104
  }
92
105
  onItemCollapse(triggerRef.current, iconRef.current as HTMLElement, isExpanded);
93
106
  }
94
- }, [isExpanded, onItemCollapse]);
107
+ }, [isExpanded]);
95
108
 
96
109
  useEffect(() => {
97
110
  if (contentWrapperRef.current && !initialHeightSet.current) {
@@ -101,47 +114,55 @@ const AccordionItem = (props: AccordionItemProps) => {
101
114
  setInternalExpanded(shouldBeExpanded);
102
115
  }
103
116
 
104
- // Set initial height when activeItem is true
117
+ // Set initial maxHeight when activeItem is true
105
118
  if (shouldBeExpanded && contentRef.current) {
106
119
  const contentHeight = contentRef.current.offsetHeight;
107
- contentWrapperRef.current.style.height = `${contentHeight}px`;
120
+ contentWrapperRef.current.style.maxHeight = `${contentHeight}px`;
108
121
  }
109
122
  }
110
123
  }, [activeItem, controlledExpanded]);
111
124
 
112
125
  useEffect(() => {
126
+ if (!heightObserver || !Array.isArray(heightObserver) || !heightObserver.includes(index as number)) return;
113
127
 
114
- if (parseFloat(heightObserver as never) !== index) return;
115
-
116
- // When the content height changes dynamically, change the height of the wrapper
128
+ // When the content height changes dynamically, change the maxHeight of the wrapper
117
129
  if (contentRef.current && contentWrapperRef.current) {
118
130
  const _contentPadding = window.getComputedStyle(contentRef.current as HTMLDivElement).getPropertyValue('padding-bottom');
119
131
 
120
132
  observer.current = new ResizeObserver(entries => {
121
-
122
133
  if (!Array.isArray(entries) || !entries.length) {
123
134
  return;
124
135
  }
125
136
 
126
137
  entries.forEach(entry => {
127
- if (contentWrapperRef.current !== null) (contentWrapperRef.current as HTMLDivElement).style.height = entry.contentRect.bottom + parseFloat(_contentPadding) + 'px';
138
+ if (contentWrapperRef.current !== null && isExpanded) {
139
+ (contentWrapperRef.current as HTMLDivElement).style.maxHeight = entry.contentRect.bottom + parseFloat(_contentPadding) + 'px';
140
+ }
128
141
  });
129
142
  });
130
143
  observer.current.observe(contentRef.current);
131
-
132
144
  }
133
145
 
134
146
  return () => {
135
147
  if (contentRef.current) observer.current?.unobserve(contentRef.current);
136
148
  };
137
149
 
150
+ }, [heightObserver, isExpanded]);
138
151
 
139
- }, [heightObserver]);
140
-
152
+ // Update maxHeight when expanded state changes
153
+ useEffect(() => {
154
+ if (contentWrapperRef.current && contentRef.current) {
155
+ if (isExpanded) {
156
+ const contentHeight = contentRef.current.scrollHeight;
157
+ contentWrapperRef.current.style.maxHeight = `${contentHeight}px`;
158
+ } else {
159
+ contentWrapperRef.current.style.maxHeight = '0px';
160
+ }
161
+ }
162
+ }, [isExpanded]);
141
163
 
142
164
  return (
143
165
  <>
144
-
145
166
  <div
146
167
  {...attributes}
147
168
  data-index={index}
@@ -211,10 +232,7 @@ const AccordionItem = (props: AccordionItemProps) => {
211
232
  clsWrite(itemContentWrapperClassName, 'accordion-collapse')
212
233
  )}
213
234
  role="tabpanel"
214
- style={{
215
- height: '0',
216
- overflow: 'hidden' // "overflow" affects the width, so add `w-100` to `custom-accordion-content__wrapper`
217
- }}>
235
+ style={getTransitionStyle()}>
218
236
  <div className={combinedCls(
219
237
  'custom-accordion-content',
220
238
  clsWrite(itemContentClassName, 'accordion-body')
@@ -1,2 +1,2 @@
1
1
  export { default as Accordion } from './Accordion';
2
- export { default as AccordionItem } from './AccordionItem';
2
+ export { default as AccordionItem } from './AccordionItem';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "UIUX Lab",
3
3
  "email": "uiuxlab@gmail.com",
4
4
  "name": "funda-ui",
5
- "version": "4.7.545",
5
+ "version": "4.7.555",
6
6
  "description": "React components using pure Bootstrap 5+ which does not contain any external style and script libraries.",
7
7
  "repository": {
8
8
  "type": "git",