@onehat/ui
Version:
Base UI for OneHat apps
153 lines (141 loc) • 4.5 kB
JavaScript
import { useState, useEffect, useRef, } from 'react';
import {
HStack,
ScrollView,
Pressable,
VStack,
VStackNative,
} from '@project-components/Gluestack';
import clsx from 'clsx';
import inArray from '../../Functions/inArray.js';
import emptyFn from '../../Functions/emptyFn.js';
import _ from 'lodash';
// The Accordion has two modes.
// In the first (onlyOne), only one section can be expanded at a time.
// Otherwise, any section can be expanded—multiple at a time.
// In the first, the section which is newly expanded will scroll to the top,
// and extra padding down below will be added. In the other mode,
// no auto scrolling will take place
//
// If you want to scroll in the second mode…
// Every time I expand or collapse a section, get the new height of each section.
// That way you can still calculate the top position for any sections scrolling
// Might be able to do a delta since last calsulation.
export default function Accordion(props) {
const {
sections = [],
activeSections = [],
setActiveSections = emptyFn,
renderHeader = emptyFn,
renderContent = emptyFn,
onAnimationEnd = emptyFn,
onLayout,
onlyOne = true,
...propsToPass
} = props,
scrollViewRef = useRef(),
refs = {},
[isRendered, setIsRendered] = useState(false),
[containerInitialHeight, setContainerInitialHeight] = useState(0),
sectionHeight = containerInitialHeight / (sections?.length || 1), // protect against divide by zero
calculateExtraPadding = () => {
// this adds extra padding to the bottom, depending on what's active
// so that scrollTo can scroll the active header all the way downto the top
let extraPadding = 0;
const activeIx = activeSections[0];
if (isRendered && activeIx) {
// when the first section is active, we don't need to add any padding.
// For each subsequent section, we need to add more and more padding
// until when the last section is active, we need to add padding equal to
// the container height
extraPadding = (activeIx +2) * sectionHeight;
}
return extraPadding;
},
items = _.map(sections, (section, ix) => {
const itemVar = 'item' + ix;
refs[itemVar] = useRef(); // so we have refs for each section - e.g. refs.item0
const
isActive = inArray(ix, activeSections),
header = renderHeader(section, ix, isActive),
content = renderContent(section, ix, isActive, refs[itemVar]),
rowProps = {};
// TODO: Animate height. Possible help here: https://stackoverflow.com/a/57333550 and https://stackoverflow.com/a/64797961
if (isActive) {
rowProps.flex = 1;
} else {
rowProps.h = 0;
rowProps.overflow = 'hidden'; // otherwise some elements mistakenly show
}
return <VStack key={ix}>
<Pressable
onPress={(e) => {
let newActiveSections = [];
if (onlyOne) {
if (!isActive) {
newActiveSections = [ix];
}
} else {
if (isActive) {
newActiveSections = _.without(activeSections, ix);
} else {
newActiveSections = [...activeSections]; // clone
newActiveSections.push(ix);
}
}
setActiveSections(newActiveSections);
}}
>
{header}
</Pressable>
<HStack {...rowProps} className="bg-[#f00]">
{content}
</HStack>
</VStack>;
});
useEffect(() => {
if (!isRendered) {
return () => {};
}
if (!onlyOne) {
return () => {}; // Don't animate if !onlyOne
}
const
scrollView = scrollViewRef.current,
activeIx = activeSections[0];
let scrollTo = 0;
if (activeSections?.length && sections?.length) {
scrollTo = sectionHeight * activeIx;
}
if (scrollView) {
setTimeout(()=> {
scrollView.scrollTo({ x: 0, y: scrollTo, animated: true });
}, 10); // delay so render can finish
}
}, [activeSections, isRendered]);
return <ScrollView
ref={scrollViewRef}
keyboardShouldPersistTaps="always"
className="Accordion-ScrollView flex-1 w-full"
contentContainerStyle={{
height: '100%',
}}
>
<VStackNative
{...propsToPass}
onLayout={(e) => {
if (!containerInitialHeight) {
const { height } = e.nativeEvent.layout;
setContainerInitialHeight(height);
}
if (onLayout) {
onLayout(e);
}
setIsRendered(true);
}}
className={` pb-${(onlyOne ? calculateExtraPadding() : 0) + 'px'} `}
>
{items}
</VStackNative>
</ScrollView>;
}