react-native-pro-accordion 1.0.1
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.
- package/README.md +476 -0
- package/index.js +3 -0
- package/lib/commonjs/components/Accordion.js +130 -0
- package/lib/commonjs/components/Accordion.js.map +1 -0
- package/lib/commonjs/components/AccordionContent.js +125 -0
- package/lib/commonjs/components/AccordionContent.js.map +1 -0
- package/lib/commonjs/components/AccordionItem.js +146 -0
- package/lib/commonjs/components/AccordionItem.js.map +1 -0
- package/lib/commonjs/components/AccordionSeparator.js +159 -0
- package/lib/commonjs/components/AccordionSeparator.js.map +1 -0
- package/lib/commonjs/core/AccordionContext.js +82 -0
- package/lib/commonjs/core/AccordionContext.js.map +1 -0
- package/lib/commonjs/core/AccordionRef.js +64 -0
- package/lib/commonjs/core/AccordionRef.js.map +1 -0
- package/lib/commonjs/core/types.js +6 -0
- package/lib/commonjs/core/types.js.map +1 -0
- package/lib/commonjs/hooks/useAccordion.js +18 -0
- package/lib/commonjs/hooks/useAccordion.js.map +1 -0
- package/lib/commonjs/hooks/useAccordionAccessibility.js +39 -0
- package/lib/commonjs/hooks/useAccordionAccessibility.js.map +1 -0
- package/lib/commonjs/hooks/useAccordionAnimation.js +135 -0
- package/lib/commonjs/hooks/useAccordionAnimation.js.map +1 -0
- package/lib/commonjs/hooks/useAccordionKeyboard.js +43 -0
- package/lib/commonjs/hooks/useAccordionKeyboard.js.map +1 -0
- package/lib/commonjs/hooks/useAccordionState.js +96 -0
- package/lib/commonjs/hooks/useAccordionState.js.map +1 -0
- package/lib/commonjs/index.js +176 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/theme/ThemeContext.js +48 -0
- package/lib/commonjs/theme/ThemeContext.js.map +1 -0
- package/lib/commonjs/theme/defaultTheme.js +79 -0
- package/lib/commonjs/theme/defaultTheme.js.map +1 -0
- package/lib/commonjs/theme/types.js +6 -0
- package/lib/commonjs/theme/types.js.map +1 -0
- package/lib/commonjs/utils/animations.js +41 -0
- package/lib/commonjs/utils/animations.js.map +1 -0
- package/lib/commonjs/utils/layout.js +155 -0
- package/lib/commonjs/utils/layout.js.map +1 -0
- package/lib/commonjs/utils/validators.js +26 -0
- package/lib/commonjs/utils/validators.js.map +1 -0
- package/lib/module/components/Accordion.js +125 -0
- package/lib/module/components/Accordion.js.map +1 -0
- package/lib/module/components/AccordionContent.js +120 -0
- package/lib/module/components/AccordionContent.js.map +1 -0
- package/lib/module/components/AccordionItem.js +141 -0
- package/lib/module/components/AccordionItem.js.map +1 -0
- package/lib/module/components/AccordionSeparator.js +155 -0
- package/lib/module/components/AccordionSeparator.js.map +1 -0
- package/lib/module/core/AccordionContext.js +75 -0
- package/lib/module/core/AccordionContext.js.map +1 -0
- package/lib/module/core/AccordionRef.js +59 -0
- package/lib/module/core/AccordionRef.js.map +1 -0
- package/lib/module/core/types.js +136 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/hooks/useAccordion.js +14 -0
- package/lib/module/hooks/useAccordion.js.map +1 -0
- package/lib/module/hooks/useAccordionAccessibility.js +34 -0
- package/lib/module/hooks/useAccordionAccessibility.js.map +1 -0
- package/lib/module/hooks/useAccordionAnimation.js +131 -0
- package/lib/module/hooks/useAccordionAnimation.js.map +1 -0
- package/lib/module/hooks/useAccordionKeyboard.js +38 -0
- package/lib/module/hooks/useAccordionKeyboard.js.map +1 -0
- package/lib/module/hooks/useAccordionState.js +92 -0
- package/lib/module/hooks/useAccordionState.js.map +1 -0
- package/lib/module/index.js +43 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/theme/ThemeContext.js +41 -0
- package/lib/module/theme/ThemeContext.js.map +1 -0
- package/lib/module/theme/defaultTheme.js +75 -0
- package/lib/module/theme/defaultTheme.js.map +1 -0
- package/lib/module/theme/types.js +66 -0
- package/lib/module/theme/types.js.map +1 -0
- package/lib/module/utils/animations.js +35 -0
- package/lib/module/utils/animations.js.map +1 -0
- package/lib/module/utils/layout.js +147 -0
- package/lib/module/utils/layout.js.map +1 -0
- package/lib/module/utils/validators.js +20 -0
- package/lib/module/utils/validators.js.map +1 -0
- package/package.json +25 -0
- package/src/components/Accordion.js +164 -0
- package/src/components/AccordionContent.js +149 -0
- package/src/components/AccordionItem.js +146 -0
- package/src/components/AccordionSeparator.js +168 -0
- package/src/core/AccordionContext.js +86 -0
- package/src/core/AccordionRef.js +69 -0
- package/src/core/types.js +133 -0
- package/src/hooks/useAccordion.js +14 -0
- package/src/hooks/useAccordionAccessibility.js +30 -0
- package/src/hooks/useAccordionAnimation.js +165 -0
- package/src/hooks/useAccordionKeyboard.js +38 -0
- package/src/hooks/useAccordionState.js +119 -0
- package/src/index.js +56 -0
- package/src/theme/ThemeContext.js +40 -0
- package/src/theme/defaultTheme.js +73 -0
- package/src/theme/types.js +63 -0
- package/src/utils/animations.js +38 -0
- package/src/utils/layout.js +138 -0
- package/src/utils/validators.js +20 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class AccordionRefImpl {
|
|
2
|
+
constructor(expandedValuesRef, setExpandedValues, mode, readOnly) {
|
|
3
|
+
this.expandedValuesRef = expandedValuesRef;
|
|
4
|
+
this.setExpandedValues = setExpandedValues;
|
|
5
|
+
this.mode = mode;
|
|
6
|
+
this.readOnly = readOnly;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
open = (value) => {
|
|
10
|
+
if (this.readOnly) return;
|
|
11
|
+
|
|
12
|
+
const currentValues = this.expandedValuesRef.current;
|
|
13
|
+
if (currentValues.includes(value)) return;
|
|
14
|
+
|
|
15
|
+
let newValues;
|
|
16
|
+
if (this.mode === "single") {
|
|
17
|
+
newValues = [value];
|
|
18
|
+
} else {
|
|
19
|
+
newValues = [...currentValues, value];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.setExpandedValues(newValues);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
close = (value) => {
|
|
26
|
+
if (this.readOnly) return;
|
|
27
|
+
|
|
28
|
+
const currentValues = this.expandedValuesRef.current;
|
|
29
|
+
if (!currentValues.includes(value)) return;
|
|
30
|
+
|
|
31
|
+
const newValues = currentValues.filter((v) => v !== value);
|
|
32
|
+
this.setExpandedValues(newValues);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
toggle = (value) => {
|
|
36
|
+
if (this.readOnly) return;
|
|
37
|
+
|
|
38
|
+
const currentValues = this.expandedValuesRef.current;
|
|
39
|
+
if (currentValues.includes(value)) {
|
|
40
|
+
this.close(value);
|
|
41
|
+
} else {
|
|
42
|
+
this.open(value);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
expandAll = () => {
|
|
47
|
+
if (this.readOnly || this.mode !== "multiple") return;
|
|
48
|
+
console.warn("expandAll requires all item values to be known");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
collapseAll = () => {
|
|
52
|
+
if (this.readOnly) return;
|
|
53
|
+
this.setExpandedValues([]);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
toggleAll = () => {
|
|
57
|
+
if (this.readOnly || this.mode !== "multiple") return;
|
|
58
|
+
const currentValues = this.expandedValuesRef.current;
|
|
59
|
+
if (currentValues.length > 0) {
|
|
60
|
+
this.collapseAll();
|
|
61
|
+
} else {
|
|
62
|
+
this.expandAll();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
getExpandedItems = () => {
|
|
67
|
+
return [...this.expandedValuesRef.current];
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {'timing' | 'spring'} AnimationType
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} TimingAnimationConfig
|
|
7
|
+
* @property {'timing'} type
|
|
8
|
+
* @property {number} [duration]
|
|
9
|
+
* @property {Function} [easing]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} SpringAnimationConfig
|
|
14
|
+
* @property {'spring'} type
|
|
15
|
+
* @property {number} [damping]
|
|
16
|
+
* @property {number} [mass]
|
|
17
|
+
* @property {number} [stiffness]
|
|
18
|
+
* @property {boolean} [overshootClamping]
|
|
19
|
+
* @property {number} [restSpeedThreshold]
|
|
20
|
+
* @property {number} [restDisplacementThreshold]
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {TimingAnimationConfig | SpringAnimationConfig} AnimationConfig
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} AccordionTheme
|
|
29
|
+
* @property {Object} colors
|
|
30
|
+
* @property {string} colors.background
|
|
31
|
+
* @property {string} colors.surface
|
|
32
|
+
* @property {string} colors.primary
|
|
33
|
+
* @property {string} colors.text
|
|
34
|
+
* @property {string} colors.textSecondary
|
|
35
|
+
* @property {string} colors.border
|
|
36
|
+
* @property {string} colors.disabled
|
|
37
|
+
* @property {string} colors.icon
|
|
38
|
+
* @property {Object} spacing
|
|
39
|
+
* @property {number} spacing.xs
|
|
40
|
+
* @property {number} spacing.sm
|
|
41
|
+
* @property {number} spacing.md
|
|
42
|
+
* @property {number} spacing.lg
|
|
43
|
+
* @property {number} spacing.xl
|
|
44
|
+
* @property {Object} typography
|
|
45
|
+
* @property {Object} typography.header
|
|
46
|
+
* @property {Object} typography.content
|
|
47
|
+
* @property {Object} borderRadius
|
|
48
|
+
* @property {number} borderRadius.sm
|
|
49
|
+
* @property {number} borderRadius.md
|
|
50
|
+
* @property {number} borderRadius.lg
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} AccordionItemProps
|
|
55
|
+
* @property {string} value - Unique identifier for the item
|
|
56
|
+
* @property {React.ReactNode | Function} header - Header content or render function
|
|
57
|
+
* @property {React.ReactNode | Function} children - Content to display when expanded
|
|
58
|
+
* @property {boolean} [disabled] - Whether the item is disabled
|
|
59
|
+
* @property {object} [style] - Custom style overrides
|
|
60
|
+
* @property {object} [headerStyle] - Custom header style
|
|
61
|
+
* @property {object} [contentStyle] - Custom content style
|
|
62
|
+
* @property {any} [data] - Custom data attached to item
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} AccordionEventHandlers
|
|
67
|
+
* @property {Function} [onOpen]
|
|
68
|
+
* @property {Function} [onClose]
|
|
69
|
+
* @property {Function} [onToggle]
|
|
70
|
+
* @property {Function} [onAnimationStart]
|
|
71
|
+
* @property {Function} [onAnimationEnd]
|
|
72
|
+
* @property {Function} [onStateChange]
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {Object} AccordionProps
|
|
77
|
+
* @property {React.ReactNode} children - Array of accordion items
|
|
78
|
+
* @property {string[]} [value] - Controlled expanded values
|
|
79
|
+
* @property {string[]} [defaultValue] - Initial expanded values (uncontrolled)
|
|
80
|
+
* @property {Function} [onChange] - Callback when expanded values change
|
|
81
|
+
* @property {'single' | 'multiple'} [mode] - Expand mode
|
|
82
|
+
* @property {boolean} [readOnly] - Whether accordion is in read-only mode
|
|
83
|
+
* @property {AnimationConfig} [animation] - Animation configuration
|
|
84
|
+
* @property {Function} [renderIcon] - Custom renderer for expand/collapse icon
|
|
85
|
+
* @property {'left' | 'right'} [iconPosition] - Icon position
|
|
86
|
+
* @property {boolean} [showSeparators] - Whether to show item separators
|
|
87
|
+
* @property {React.ReactNode} [separatorComponent] - Custom separator component
|
|
88
|
+
* @property {object} [separatorStyle] - Separator style
|
|
89
|
+
* @property {boolean} [stickyHeaders] - Enable sticky headers
|
|
90
|
+
* @property {boolean} [lazyRender] - Lazy render content
|
|
91
|
+
* @property {Partial<AccordionTheme>} [theme] - Custom theme
|
|
92
|
+
* @property {boolean} [darkMode] - Enable dark mode
|
|
93
|
+
* @property {string} [accessibilityLabel] - Accessibility label
|
|
94
|
+
* @property {string} [testID] - Test ID for testing
|
|
95
|
+
* @property {Function} [onOpen]
|
|
96
|
+
* @property {Function} [onClose]
|
|
97
|
+
* @property {Function} [onToggle]
|
|
98
|
+
* @property {Function} [onAnimationStart]
|
|
99
|
+
* @property {Function} [onAnimationEnd]
|
|
100
|
+
* @property {Function} [onStateChange]
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @typedef {Object} AccordionRef
|
|
105
|
+
* @property {Function} open - Open a specific item by value
|
|
106
|
+
* @property {Function} close - Close a specific item by value
|
|
107
|
+
* @property {Function} toggle - Toggle a specific item by value
|
|
108
|
+
* @property {Function} expandAll - Open all items (only works in multiple mode)
|
|
109
|
+
* @property {Function} collapseAll - Close all items
|
|
110
|
+
* @property {Function} toggleAll - Toggle all items
|
|
111
|
+
* @property {Function} getExpandedItems - Get currently expanded items
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {Object} AccordionContextType
|
|
116
|
+
* @property {string} mode
|
|
117
|
+
* @property {boolean} readOnly
|
|
118
|
+
* @property {AnimationConfig} animationConfig
|
|
119
|
+
* @property {Function} [renderIcon]
|
|
120
|
+
* @property {string} iconPosition
|
|
121
|
+
* @property {boolean} showSeparators
|
|
122
|
+
* @property {boolean} lazyRender
|
|
123
|
+
* @property {AccordionTheme} [theme]
|
|
124
|
+
* @property {boolean} darkMode
|
|
125
|
+
* @property {Function} registerItem
|
|
126
|
+
* @property {Function} unregisterItem
|
|
127
|
+
* @property {Function} toggleItem
|
|
128
|
+
* @property {Function} isExpanded
|
|
129
|
+
* @property {Function} getItemData
|
|
130
|
+
* @property {Function} [onEvent]
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useAccordion(defaultIndex = null) {
|
|
4
|
+
const [activeIndex, setActiveIndex] = useState(defaultIndex);
|
|
5
|
+
|
|
6
|
+
const toggle = (index) => {
|
|
7
|
+
setActiveIndex((prev) => (prev === index ? null : index));
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
activeIndex,
|
|
12
|
+
toggle,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
export const useAccordionAccessibility = (props) => {
|
|
5
|
+
const { expanded, disabled, label } = props;
|
|
6
|
+
|
|
7
|
+
const accessibilityProps = useMemo(() => {
|
|
8
|
+
const baseProps = {
|
|
9
|
+
accessible: true,
|
|
10
|
+
accessibilityRole: "button",
|
|
11
|
+
accessibilityState: {
|
|
12
|
+
expanded,
|
|
13
|
+
disabled,
|
|
14
|
+
},
|
|
15
|
+
accessibilityLabel: label || "Toggle accordion",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (Platform.OS === "web") {
|
|
19
|
+
return {
|
|
20
|
+
...baseProps,
|
|
21
|
+
"aria-expanded": expanded,
|
|
22
|
+
"aria-disabled": disabled,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return baseProps;
|
|
27
|
+
}, [expanded, disabled, label]);
|
|
28
|
+
|
|
29
|
+
return { accessibilityProps };
|
|
30
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useSharedValue,
|
|
4
|
+
withTiming,
|
|
5
|
+
withSpring,
|
|
6
|
+
Easing,
|
|
7
|
+
runOnJS,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook for managing accordion animations
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {boolean} options.expanded - Whether item is expanded
|
|
14
|
+
* @param {number} options.contentHeight - Height of the content
|
|
15
|
+
* @param {Object} options.animationConfig - Animation configuration
|
|
16
|
+
* @param {Function} options.onAnimationStart - Callback when animation starts
|
|
17
|
+
* @param {Function} options.onAnimationEnd - Callback when animation ends
|
|
18
|
+
* @returns {Object} Animation values and utilities
|
|
19
|
+
*/
|
|
20
|
+
export const useAccordionAnimation = ({
|
|
21
|
+
expanded,
|
|
22
|
+
contentHeight,
|
|
23
|
+
animationConfig,
|
|
24
|
+
onAnimationStart,
|
|
25
|
+
onAnimationEnd,
|
|
26
|
+
}) => {
|
|
27
|
+
const height = useSharedValue(expanded ? contentHeight : 0);
|
|
28
|
+
const opacity = useSharedValue(expanded ? 1 : 0);
|
|
29
|
+
const rotation = useSharedValue(expanded ? 1 : 0);
|
|
30
|
+
const isAnimating = useRef(false);
|
|
31
|
+
|
|
32
|
+
const getAnimatedHeight = useCallback(
|
|
33
|
+
(toValue) => {
|
|
34
|
+
if (animationConfig.type === "spring") {
|
|
35
|
+
return withSpring(toValue, {
|
|
36
|
+
damping: animationConfig.damping ?? 10,
|
|
37
|
+
mass: animationConfig.mass ?? 1,
|
|
38
|
+
stiffness: animationConfig.stiffness ?? 100,
|
|
39
|
+
overshootClamping: animationConfig.overshootClamping ?? false,
|
|
40
|
+
restSpeedThreshold: animationConfig.restSpeedThreshold ?? 0.01,
|
|
41
|
+
restDisplacementThreshold:
|
|
42
|
+
animationConfig.restDisplacementThreshold ?? 0.01,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
return withTiming(toValue, {
|
|
46
|
+
duration: animationConfig.duration ?? 300,
|
|
47
|
+
easing: animationConfig.easing ?? Easing.inOut(Easing.ease),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[animationConfig],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const getAnimatedOpacity = useCallback(
|
|
55
|
+
(toValue) => {
|
|
56
|
+
if (animationConfig.type === "spring") {
|
|
57
|
+
return withSpring(toValue, {
|
|
58
|
+
damping: animationConfig.damping ?? 10,
|
|
59
|
+
mass: animationConfig.mass ?? 1,
|
|
60
|
+
stiffness: animationConfig.stiffness ?? 100,
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
return withTiming(toValue, {
|
|
64
|
+
duration: (animationConfig.duration ?? 300) * 0.6,
|
|
65
|
+
easing: animationConfig.easing ?? Easing.inOut(Easing.ease),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[animationConfig],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const getAnimatedRotation = useCallback(
|
|
73
|
+
(toValue) => {
|
|
74
|
+
const targetRotation = toValue ? 1 : 0;
|
|
75
|
+
if (animationConfig.type === "spring") {
|
|
76
|
+
return withSpring(targetRotation, {
|
|
77
|
+
damping: animationConfig.damping ?? 10,
|
|
78
|
+
mass: animationConfig.mass ?? 1,
|
|
79
|
+
stiffness: animationConfig.stiffness ?? 100,
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
return withTiming(targetRotation, {
|
|
83
|
+
duration: animationConfig.duration ?? 300,
|
|
84
|
+
easing: animationConfig.easing ?? Easing.inOut(Easing.ease),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
[animationConfig],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const animate = useCallback(() => {
|
|
92
|
+
if (onAnimationStart) {
|
|
93
|
+
runOnJS(onAnimationStart)();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isAnimating.current = true;
|
|
97
|
+
const toHeight = expanded ? contentHeight : 0;
|
|
98
|
+
const toOpacity = expanded ? 1 : 0;
|
|
99
|
+
const toRotation = expanded ? 1 : 0;
|
|
100
|
+
|
|
101
|
+
height.value = getAnimatedHeight(toHeight);
|
|
102
|
+
opacity.value = getAnimatedOpacity(toOpacity);
|
|
103
|
+
rotation.value = getAnimatedRotation(toRotation);
|
|
104
|
+
|
|
105
|
+
// Set up animation end callback
|
|
106
|
+
const timeoutId = setTimeout(
|
|
107
|
+
() => {
|
|
108
|
+
if (isAnimating.current && onAnimationEnd) {
|
|
109
|
+
runOnJS(onAnimationEnd)();
|
|
110
|
+
isAnimating.current = false;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
animationConfig.type === "spring"
|
|
114
|
+
? 500
|
|
115
|
+
: (animationConfig.duration ?? 300),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return () => clearTimeout(timeoutId);
|
|
119
|
+
}, [
|
|
120
|
+
expanded,
|
|
121
|
+
contentHeight,
|
|
122
|
+
animationConfig,
|
|
123
|
+
height,
|
|
124
|
+
opacity,
|
|
125
|
+
rotation,
|
|
126
|
+
getAnimatedHeight,
|
|
127
|
+
getAnimatedOpacity,
|
|
128
|
+
getAnimatedRotation,
|
|
129
|
+
onAnimationStart,
|
|
130
|
+
onAnimationEnd,
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const cleanup = animate();
|
|
135
|
+
return () => {
|
|
136
|
+
if (cleanup) cleanup();
|
|
137
|
+
isAnimating.current = false;
|
|
138
|
+
};
|
|
139
|
+
}, [animate]);
|
|
140
|
+
|
|
141
|
+
const resetAnimation = useCallback(() => {
|
|
142
|
+
height.value = 0;
|
|
143
|
+
opacity.value = 0;
|
|
144
|
+
rotation.value = 0;
|
|
145
|
+
isAnimating.current = false;
|
|
146
|
+
}, [height, opacity, rotation]);
|
|
147
|
+
|
|
148
|
+
const getRotatedStyle = useCallback(() => {
|
|
149
|
+
"worklet";
|
|
150
|
+
return {
|
|
151
|
+
transform: [{ rotate: `${rotation.value * 90}deg` }],
|
|
152
|
+
};
|
|
153
|
+
}, [rotation]);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
height,
|
|
157
|
+
opacity,
|
|
158
|
+
rotation,
|
|
159
|
+
isAnimating: isAnimating.current,
|
|
160
|
+
animatedHeightStyle: { height },
|
|
161
|
+
animatedOpacityStyle: { opacity },
|
|
162
|
+
animatedRotationStyle: getRotatedStyle,
|
|
163
|
+
resetAnimation,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
export const useAccordionKeyboard = (props) => {
|
|
5
|
+
const { onPress, disabled = false } = props;
|
|
6
|
+
const elementRef = useRef(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (Platform.OS !== "web") return;
|
|
10
|
+
|
|
11
|
+
const handleKeyPress = (event) => {
|
|
12
|
+
if (disabled) return;
|
|
13
|
+
|
|
14
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
onPress();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const element = elementRef.current;
|
|
21
|
+
if (element) {
|
|
22
|
+
element.addEventListener("keydown", handleKeyPress);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
if (element) {
|
|
27
|
+
element.removeEventListener("keydown", handleKeyPress);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [onPress, disabled]);
|
|
31
|
+
|
|
32
|
+
const keyboardProps =
|
|
33
|
+
Platform.OS === "web"
|
|
34
|
+
? { tabIndex: disabled ? -1 : 0, ref: elementRef }
|
|
35
|
+
: {};
|
|
36
|
+
|
|
37
|
+
return { keyboardProps };
|
|
38
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing accordion state with controlled/uncontrolled modes
|
|
5
|
+
* @param {Object} options
|
|
6
|
+
* @param {string[]} options.value - Controlled expanded values
|
|
7
|
+
* @param {string[]} options.defaultValue - Default expanded values for uncontrolled mode
|
|
8
|
+
* @param {Function} options.onChange - Callback when expanded values change
|
|
9
|
+
* @param {string} options.mode - 'single' or 'multiple' mode
|
|
10
|
+
* @returns {Object} State management utilities
|
|
11
|
+
*/
|
|
12
|
+
export const useAccordionState = ({
|
|
13
|
+
value,
|
|
14
|
+
defaultValue = [],
|
|
15
|
+
onChange,
|
|
16
|
+
mode = "multiple",
|
|
17
|
+
}) => {
|
|
18
|
+
const isControlled = value !== undefined;
|
|
19
|
+
const [internalExpandedValues, setInternalExpandedValues] =
|
|
20
|
+
useState(defaultValue);
|
|
21
|
+
|
|
22
|
+
const expandedValues = isControlled ? value : internalExpandedValues;
|
|
23
|
+
const expandedValuesRef = useRef(expandedValues);
|
|
24
|
+
expandedValuesRef.current = expandedValues;
|
|
25
|
+
|
|
26
|
+
const setExpandedValues = useCallback(
|
|
27
|
+
(newValues) => {
|
|
28
|
+
if (!isControlled) {
|
|
29
|
+
setInternalExpandedValues(newValues);
|
|
30
|
+
}
|
|
31
|
+
onChange?.(newValues);
|
|
32
|
+
},
|
|
33
|
+
[isControlled, onChange],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const isExpanded = useCallback(
|
|
37
|
+
(itemValue) => {
|
|
38
|
+
return expandedValues.includes(itemValue);
|
|
39
|
+
},
|
|
40
|
+
[expandedValues],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const openItem = useCallback(
|
|
44
|
+
(itemValue) => {
|
|
45
|
+
if (expandedValues.includes(itemValue)) return;
|
|
46
|
+
|
|
47
|
+
let newValues;
|
|
48
|
+
if (mode === "single") {
|
|
49
|
+
newValues = [itemValue];
|
|
50
|
+
} else {
|
|
51
|
+
newValues = [...expandedValues, itemValue];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setExpandedValues(newValues);
|
|
55
|
+
return newValues;
|
|
56
|
+
},
|
|
57
|
+
[expandedValues, mode, setExpandedValues],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const closeItem = useCallback(
|
|
61
|
+
(itemValue) => {
|
|
62
|
+
if (!expandedValues.includes(itemValue)) return;
|
|
63
|
+
|
|
64
|
+
const newValues = expandedValues.filter((v) => v !== itemValue);
|
|
65
|
+
setExpandedValues(newValues);
|
|
66
|
+
return newValues;
|
|
67
|
+
},
|
|
68
|
+
[expandedValues, setExpandedValues],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const toggleItem = useCallback(
|
|
72
|
+
(itemValue) => {
|
|
73
|
+
if (expandedValues.includes(itemValue)) {
|
|
74
|
+
return closeItem(itemValue);
|
|
75
|
+
} else {
|
|
76
|
+
return openItem(itemValue);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[expandedValues, openItem, closeItem],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const expandAll = useCallback(() => {
|
|
83
|
+
if (mode !== "multiple") return;
|
|
84
|
+
// Note: This requires access to all registered items
|
|
85
|
+
console.warn("expandAll requires all item values to be accessible");
|
|
86
|
+
}, [mode]);
|
|
87
|
+
|
|
88
|
+
const collapseAll = useCallback(() => {
|
|
89
|
+
setExpandedValues([]);
|
|
90
|
+
}, [setExpandedValues]);
|
|
91
|
+
|
|
92
|
+
const toggleAll = useCallback(() => {
|
|
93
|
+
if (mode !== "multiple") return;
|
|
94
|
+
if (expandedValues.length > 0) {
|
|
95
|
+
collapseAll();
|
|
96
|
+
} else {
|
|
97
|
+
expandAll();
|
|
98
|
+
}
|
|
99
|
+
}, [mode, expandedValues.length, collapseAll, expandAll]);
|
|
100
|
+
|
|
101
|
+
const getExpandedItems = useCallback(() => {
|
|
102
|
+
return [...expandedValues];
|
|
103
|
+
}, [expandedValues]);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
expandedValues,
|
|
107
|
+
expandedValuesRef,
|
|
108
|
+
setExpandedValues,
|
|
109
|
+
isExpanded,
|
|
110
|
+
openItem,
|
|
111
|
+
closeItem,
|
|
112
|
+
toggleItem,
|
|
113
|
+
expandAll,
|
|
114
|
+
collapseAll,
|
|
115
|
+
toggleAll,
|
|
116
|
+
getExpandedItems,
|
|
117
|
+
isControlled,
|
|
118
|
+
};
|
|
119
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/index.js
|
|
2
|
+
// export { Accordion } from "./Accordion";
|
|
3
|
+
// export { AccordionItem } from "./AccordionItem";
|
|
4
|
+
|
|
5
|
+
// Main components
|
|
6
|
+
export { Accordion } from "./components/Accordion";
|
|
7
|
+
export { AccordionItem } from "./components/AccordionItem";
|
|
8
|
+
export { AccordionContent } from "./components/AccordionContent";
|
|
9
|
+
export {
|
|
10
|
+
AccordionSeparator,
|
|
11
|
+
NestedAccordionSeparator,
|
|
12
|
+
DashedSeparator,
|
|
13
|
+
GradientSeparator,
|
|
14
|
+
} from "./components/AccordionSeparator";
|
|
15
|
+
|
|
16
|
+
// Context
|
|
17
|
+
export { useAccordion, AccordionContext } from "./core/AccordionContext";
|
|
18
|
+
|
|
19
|
+
// Hooks
|
|
20
|
+
export { useAccordionAccessibility } from "./hooks/useAccordionAccessibility";
|
|
21
|
+
export { useAccordionKeyboard } from "./hooks/useAccordionKeyboard";
|
|
22
|
+
export { useAccordionState } from "./hooks/useAccordionState";
|
|
23
|
+
export { useAccordionAnimation } from "./hooks/useAccordionAnimation";
|
|
24
|
+
|
|
25
|
+
// Theme
|
|
26
|
+
export { defaultLightTheme, defaultDarkTheme } from "./theme/defaultTheme";
|
|
27
|
+
export { ThemeProvider, useTheme } from "./theme/ThemeContext";
|
|
28
|
+
|
|
29
|
+
// Utilities
|
|
30
|
+
export { getAnimatedHeight, getAnimatedOpacity } from "./utils/animations";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
validateAnimationConfig,
|
|
34
|
+
validateAccordionMode,
|
|
35
|
+
} from "./utils/validators";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
useContentHeight,
|
|
39
|
+
useMultipleContentHeights,
|
|
40
|
+
animateLayout,
|
|
41
|
+
scrollToAccordionItem,
|
|
42
|
+
getElementPosition,
|
|
43
|
+
} from "./utils/layout";
|
|
44
|
+
|
|
45
|
+
// Re-export types for JSDoc (for documentation purposes)
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {import('./core/types').AccordionProps} AccordionProps
|
|
48
|
+
* @typedef {import('./core/types').AccordionItemProps} AccordionItemProps
|
|
49
|
+
* @typedef {import('./core/types').AccordionRef} AccordionRef
|
|
50
|
+
* @typedef {import('./core/types').AccordionTheme} AccordionTheme
|
|
51
|
+
* @typedef {import('./core/types').AnimationConfig} AnimationConfig
|
|
52
|
+
* @typedef {import('./core/types').TimingAnimationConfig} TimingAnimationConfig
|
|
53
|
+
* @typedef {import('./core/types').SpringAnimationConfig} SpringAnimationConfig
|
|
54
|
+
* @typedef {import('./core/types').AnimationType} AnimationType
|
|
55
|
+
* @typedef {import('./core/types').AccordionEventHandlers} AccordionEventHandlers
|
|
56
|
+
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { defaultLightTheme, defaultDarkTheme } from "./defaultTheme";
|
|
3
|
+
|
|
4
|
+
const ThemeContext = createContext(undefined);
|
|
5
|
+
|
|
6
|
+
export const ThemeProvider = ({
|
|
7
|
+
children,
|
|
8
|
+
theme: customTheme,
|
|
9
|
+
darkMode = false,
|
|
10
|
+
onDarkModeChange,
|
|
11
|
+
}) => {
|
|
12
|
+
const baseTheme = darkMode ? defaultDarkTheme : defaultLightTheme;
|
|
13
|
+
|
|
14
|
+
const theme = useMemo(
|
|
15
|
+
() => ({
|
|
16
|
+
...baseTheme,
|
|
17
|
+
...customTheme,
|
|
18
|
+
colors: { ...baseTheme.colors, ...customTheme?.colors },
|
|
19
|
+
}),
|
|
20
|
+
[baseTheme, customTheme],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const setDarkMode = (dark) => {
|
|
24
|
+
onDarkModeChange?.(dark);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ThemeContext.Provider value={{ theme, darkMode, setDarkMode }}>
|
|
29
|
+
{children}
|
|
30
|
+
</ThemeContext.Provider>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const useTheme = () => {
|
|
35
|
+
const context = useContext(ThemeContext);
|
|
36
|
+
if (!context) {
|
|
37
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
38
|
+
}
|
|
39
|
+
return context;
|
|
40
|
+
};
|