From 537745abf85758fe8d79b77279b3fdd8ed5a3b8b Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Thu, 7 Mar 2024 17:17:48 +0100 Subject: [PATCH] fix: track approver children --- src/app/common/hooks/use-register-children.ts | 18 +++++++++++----- src/app/common/utils.ts | 6 ++++++ .../{components => }/approver-animation.tsx | 11 ++++------ src/app/features/approver/approver.context.ts | 21 +++++++++++++++++-- src/app/features/approver/approver.tsx | 7 +++++-- .../approver/components/approver-actions.tsx | 4 +++- .../approver/components/approver-advanced.tsx | 9 +++++--- .../components/approver-container.tsx | 2 +- .../approver/components/approver-header.tsx | 6 ++++-- .../approver/components/approver-section.tsx | 3 +++ .../components/approver-subheader.tsx | 3 +++ 11 files changed, 67 insertions(+), 23 deletions(-) rename src/app/features/approver/{components => }/approver-animation.tsx (75%) diff --git a/src/app/common/hooks/use-register-children.ts b/src/app/common/hooks/use-register-children.ts index 3e0f5d8ec23..2ff33b5fe76 100644 --- a/src/app/common/hooks/use-register-children.ts +++ b/src/app/common/hooks/use-register-children.ts @@ -1,19 +1,27 @@ import { useState } from 'react'; export function useRegisterChildren() { - const [children, setChildren] = useState>({} as Record); + const [childCount, setChildCount] = useState>({} as Record); function registerChild(child: T) { - setChildren(children => ({ ...children, [child]: (children[child] || 0) + 1 })); + setChildCount(children => ({ ...children, [child]: (children[child] || 0) + 1 })); } function deregisterChild(child: T) { - setChildren(children => ({ ...children, [child]: (children[child] || 0) - 1 })); + setChildCount(children => ({ ...children, [child]: (children[child] || 0) - 1 })); } function hasChild(child: T) { - return children[child] > 0; + return childCount[child] > 0; } - return { children: Object.keys(children) as T[], registerChild, deregisterChild, hasChild }; + return { + childCount, + children: Object.keys(childCount) as T[], + registerChild, + deregisterChild, + hasChild, + }; } + +export type ChildRegister = ReturnType>; diff --git a/src/app/common/utils.ts b/src/app/common/utils.ts index 8f7e1235e56..3b62ccb1cdd 100644 --- a/src/app/common/utils.ts +++ b/src/app/common/utils.ts @@ -319,3 +319,9 @@ export function removeMinusSign(value: string) { export function propIfDefined(prop: string, value: any) { return isBoolean(value) ? { [prop]: value } : {}; } + +export function getScrollParent(node: HTMLElement | null) { + if (node === null) return null; + if (node.scrollHeight > node.clientHeight) return node; + return getScrollParent(node.parentNode as HTMLElement); +} diff --git a/src/app/features/approver/components/approver-animation.tsx b/src/app/features/approver/approver-animation.tsx similarity index 75% rename from src/app/features/approver/components/approver-animation.tsx rename to src/app/features/approver/approver-animation.tsx index a2aa6d21060..f4a57d50c35 100644 --- a/src/app/features/approver/components/approver-animation.tsx +++ b/src/app/features/approver/approver-animation.tsx @@ -10,7 +10,7 @@ export const childElementInitialAnimationState = css({ [animationSelector]: { opacity: 0, transform: 'translateY(-16px)' }, }); -const staggerMenuItems = stagger(0.06, { startDelay: 0.32 }); +const staggerMenuItems = stagger(0.06, { startDelay: 0.36 }); export function useApproverChildrenEntryAnimation() { const [scope, animate] = useAnimate(); @@ -33,17 +33,14 @@ export function useApproverChildrenEntryAnimation() { return scope; } -const initialState = { x: -20, opacity: 0 } as const; -const animateState = { x: 0, opacity: 1, transition: { duration: 0.32 } } as const; - interface ApproverHeaderAnimationProps extends HasChildren { delay?: number; } export function ApproverHeaderAnimation({ delay = 0, ...props }: ApproverHeaderAnimationProps) { return ( ); @@ -53,7 +50,7 @@ export function ApproverActionsAnimation(props: HasChildren) { return ( ); diff --git a/src/app/features/approver/approver.context.ts b/src/app/features/approver/approver.context.ts index 1350250eb2b..82fe94f58e6 100644 --- a/src/app/features/approver/approver.context.ts +++ b/src/app/features/approver/approver.context.ts @@ -1,8 +1,11 @@ import { createContext, useContext } from 'react'; -export type ApproverChildren = 'header' | 'actions' | 'advanced' | 'section' | 'subheader'; +import { useOnMount } from '@app/common/hooks/use-on-mount'; +import { type ChildRegister, useRegisterChildren } from '@app/common/hooks/use-register-children'; -interface ApproverContext { +type ApproverChildren = 'header' | 'actions' | 'advanced' | 'section' | 'subheader'; + +interface ApproverContext extends ChildRegister { isDisplayingAdvancedView: boolean; setIsDisplayingAdvancedView(val: boolean): void; } @@ -16,3 +19,17 @@ export function useApproverContext() { if (!context) throw new Error('`useApproverContext` must be used within a `ApproverProvider`'); return context; } + +export function useRegisterApproverChildren() { + return useRegisterChildren(); +} + +export function useRegisterApproverChild(child: ApproverChildren) { + const { registerChild, deregisterChild, childCount } = useApproverContext(); + if (childCount.actions > 1) throw new Error('Only one `Approver.Actions` is allowed'); + if (childCount.advanced > 1) throw new Error('Only one `Approver.Advanced` is allowed'); + useOnMount(() => { + registerChild(child); + return () => deregisterChild(child); + }); +} diff --git a/src/app/features/approver/approver.tsx b/src/app/features/approver/approver.tsx index 8b7b7ad3eaa..fcaf22ad255 100644 --- a/src/app/features/approver/approver.tsx +++ b/src/app/features/approver/approver.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import type { HasChildren } from '@app/common/has-children'; -import { ApproverProvider } from './approver.context'; +import { ApproverProvider, useRegisterApproverChildren } from './approver.context'; import { ApproverActions } from './components/approver-actions'; import { ApproverAdvanced } from './components/approver-advanced'; import { ApproverContainer } from './components/approver-container'; @@ -12,9 +12,12 @@ import { ApproverSubheader } from './components/approver-subheader'; function Approver(props: HasChildren) { const [isDisplayingAdvancedView, setIsDisplayingAdvancedView] = useState(false); + const childRegister = useRegisterApproverChildren(); return ( - + ); diff --git a/src/app/features/approver/components/approver-actions.tsx b/src/app/features/approver/components/approver-actions.tsx index 6775d8efbce..dc626367b34 100644 --- a/src/app/features/approver/components/approver-actions.tsx +++ b/src/app/features/approver/components/approver-actions.tsx @@ -3,7 +3,8 @@ import { Flex, styled } from 'leather-styles/jsx'; import type { HasChildren } from '@app/common/has-children'; -import { ApproverActionsAnimation } from './approver-animation'; +import { ApproverActionsAnimation } from '../approver-animation'; +import { useRegisterApproverChild } from '../approver.context'; const stretchChildrenStyles = css({ '& > *': { flex: 1 } }); @@ -11,6 +12,7 @@ interface ApproverActionsProps extends HasChildren { actions: React.ReactNode; } export function ApproverActions({ children, actions }: ApproverActionsProps) { + useRegisterApproverChild('actions'); return ( diff --git a/src/app/features/approver/components/approver-advanced.tsx b/src/app/features/approver/components/approver-advanced.tsx index b53fd99da02..e7ac423355c 100644 --- a/src/app/features/approver/components/approver-advanced.tsx +++ b/src/app/features/approver/components/approver-advanced.tsx @@ -4,18 +4,19 @@ import { AnimatePresence, motion } from 'framer-motion'; import { Flex } from 'leather-styles/jsx'; import type { HasChildren } from '@app/common/has-children'; -import { createDelay } from '@app/common/utils'; +import { createDelay, getScrollParent } from '@app/common/utils'; import { Button } from '@app/ui/components/button/button'; import { Flag } from '@app/ui/components/flag/flag'; import { ChevronDownIcon } from '@app/ui/icons'; import { AnimateChangeInHeight } from '../../../components/animate-height'; -import { useApproverContext } from '../approver.context'; +import { useApproverContext, useRegisterApproverChild } from '../approver.context'; const slightPauseForContentEnterAnimation = createDelay(120); export function ApproverAdvanced({ children }: HasChildren) { const { isDisplayingAdvancedView, setIsDisplayingAdvancedView } = useApproverContext(); + useRegisterApproverChild('advanced'); const ref = useRef(null); @@ -23,7 +24,9 @@ export function ApproverAdvanced({ children }: HasChildren) { setIsDisplayingAdvancedView(!isDisplayingAdvancedView); if (ref.current && !isDisplayingAdvancedView) { await slightPauseForContentEnterAnimation(); - ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' }); + const scrollPosition = ref.current.offsetTop; + const scrollParent = getScrollParent(ref.current); + scrollParent?.parentElement?.scroll({ top: scrollPosition, behavior: 'smooth' }); } } diff --git a/src/app/features/approver/components/approver-container.tsx b/src/app/features/approver/components/approver-container.tsx index b67f5af38bc..4a3a010386e 100644 --- a/src/app/features/approver/components/approver-container.tsx +++ b/src/app/features/approver/components/approver-container.tsx @@ -6,7 +6,7 @@ import type { HasChildren } from '@app/common/has-children'; import { childElementInitialAnimationState, useApproverChildrenEntryAnimation, -} from './approver-animation'; +} from '../approver-animation'; const applyMarginsToLastApproverSection = css({ '& .approver-section:last-child': { mb: 'space.03' }, diff --git a/src/app/features/approver/components/approver-header.tsx b/src/app/features/approver/components/approver-header.tsx index 9ca3db6fe9f..3b1135e0a11 100644 --- a/src/app/features/approver/components/approver-header.tsx +++ b/src/app/features/approver/components/approver-header.tsx @@ -2,19 +2,21 @@ import type { ReactNode } from 'react'; import { styled } from 'leather-styles/jsx'; -import { ApproverHeaderAnimation } from './approver-animation'; +import { ApproverHeaderAnimation } from '../approver-animation'; +import { useRegisterApproverChild } from '../approver.context'; interface ApproverHeaderProps { title: ReactNode; requester: ReactNode; } export function ApproverHeader({ title, requester }: ApproverHeaderProps) { + useRegisterApproverChild('header'); return ( {title} - + Requested by {requester} diff --git a/src/app/features/approver/components/approver-section.tsx b/src/app/features/approver/components/approver-section.tsx index 7c597adf902..552d29a07dd 100644 --- a/src/app/features/approver/components/approver-section.tsx +++ b/src/app/features/approver/components/approver-section.tsx @@ -2,7 +2,10 @@ import { styled } from 'leather-styles/jsx'; import type { HasChildren } from '@app/common/has-children'; +import { useRegisterApproverChild } from '../approver.context'; + export function ApproverSection(props: HasChildren) { + useRegisterApproverChild('section'); return ( ; export function ApproverSubheader(props: ApproverSubheaderProps) { + useRegisterApproverChild('subheader'); return ; }