Animate the opening and exiting states of the JSON visualizer (#10965)

I used `overflow-y: clip` instead of `overflow-y: hidden` because of
this behavior:

> Setting overflow to visible in one direction (i.e. overflow-x or
overflow-y) when it isn't set to visible or clip in the other direction
results in the visible value behaving as auto.


## Demo


https://github.com/user-attachments/assets/b7975c99-58cc-4b63-b420-a54b27752188

Closes https://github.com/twentyhq/core-team-issues/issues/562
This commit is contained in:
Baptiste Devessier
2025-03-18 12:05:10 +01:00
committed by GitHub
parent 981308861d
commit eb5fb51c1b
3 changed files with 40 additions and 7 deletions

View File

@ -5,14 +5,15 @@ import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { ANIMATION } from '@ui/theme';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import { isDefined } from 'twenty-shared';
import { JsonValue } from 'type-fest';
const StyledContainer = styled.li`
list-style-type: none;
display: grid;
row-gap: ${({ theme }) => theme.spacing(2)};
list-style-type: none;
`;
const StyledLabelContainer = styled.div`
@ -29,6 +30,8 @@ const StyledEmptyState = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledJsonList = styled(JsonList)``.withComponent(motion.ul);
export const JsonNestedNode = ({
label,
Icon,
@ -51,7 +54,25 @@ export const JsonNestedNode = ({
const [isOpen, setIsOpen] = useState(true);
const renderedChildren = (
<JsonList depth={depth}>
<StyledJsonList
initial={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
animate={{
height: 'auto',
opacity: 1,
overflowY: 'clip',
}}
exit={{
height: 0,
opacity: 0,
overflowY: 'clip',
}}
transition={{ duration: ANIMATION.duration.normal }}
depth={depth}
>
{elements.length === 0 ? (
<StyledEmptyState>{emptyElementsText}</StyledEmptyState>
) : (
@ -71,7 +92,7 @@ export const JsonNestedNode = ({
);
})
)}
</JsonList>
</StyledJsonList>
);
const handleArrowClick = () => {
@ -79,7 +100,11 @@ export const JsonNestedNode = ({
};
if (hideRoot) {
return <StyledContainer>{renderedChildren}</StyledContainer>;
return (
<StyledContainer>
<AnimatePresence initial={false}>{renderedChildren}</AnimatePresence>
</StyledContainer>
);
}
return (
@ -96,7 +121,9 @@ export const JsonNestedNode = ({
)}
</StyledLabelContainer>
{isOpen && renderedChildren}
<AnimatePresence initial={false}>
{isOpen && renderedChildren}
</AnimatePresence>
</StyledContainer>
);
};

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled';
import { VisibilityHidden } from '@ui/accessibility';
import { IconChevronDown } from '@ui/display';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { ANIMATION } from '@ui/theme';
import { motion } from 'framer-motion';
const StyledButton = styled(motion.button)`
@ -45,7 +46,8 @@ export const JsonArrow = ({
size={theme.icon.size.md}
color={theme.font.color.secondary}
initial={false}
animate={{ rotate: isOpen ? -180 : 0 }}
animate={{ rotate: isOpen ? 0 : -90 }}
transition={{ duration: ANIMATION.duration.normal }}
/>
</StyledButton>
);

View File

@ -12,6 +12,10 @@ const StyledList = styled.ul<{ depth: number }>`
depth > 0 &&
css`
padding-left: ${theme.spacing(8)};
> :first-of-type {
margin-top: ${theme.spacing(2)};
}
`}
`;