cleanup & animation improvement

This commit is contained in:
thatmattlove 2021-12-18 00:29:51 -07:00
parent ba2ce4b930
commit 4ee97ec0b2
11 changed files with 57 additions and 74 deletions

View file

@ -1,5 +1,5 @@
import { chakra, Box, forwardRef } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import { motion, isValidMotionProp } from 'framer-motion'; import { motion } from 'framer-motion';
import type { BoxProps } from '@chakra-ui/react'; import type { BoxProps } from '@chakra-ui/react';
import type { CustomDomComponent, Transition, MotionProps } from 'framer-motion'; import type { CustomDomComponent, Transition, MotionProps } from 'framer-motion';
@ -10,19 +10,6 @@ type MakeMotionProps<P extends BoxProps> = React.PropsWithChildren<
Omit<P, 'transition'> & Omit<MotionProps, 'transition'> & { transition?: Transition } Omit<P, 'transition'> & Omit<MotionProps, 'transition'> & { transition?: Transition }
>; >;
/**
* Combined Chakra + Framer Motion component.
* @see https://chakra-ui.com/guides/integrations/with-framer
*/
export const AnimatedDiv = motion(
forwardRef<BoxProps, React.ElementType<BoxProps>>((props, ref) => {
const chakraProps = Object.fromEntries(
Object.entries(props).filter(([key]) => !isValidMotionProp(key)),
);
return <Box ref={ref} {...chakraProps} />;
}),
);
/** /**
* Combine `chakra` and `motion` factories. * Combine `chakra` and `motion` factories.
* *
@ -37,3 +24,5 @@ export function motionChakra<P extends BoxProps = BoxProps>(
// @ts-expect-error I don't know how to fix this. // @ts-expect-error I don't know how to fix this.
return motion<P>(chakra<MCComponent, P>(component, options)); return motion<P>(chakra<MCComponent, P>(component, options));
} }
export const AnimatedDiv = motionChakra('div');

View file

@ -1,21 +1,18 @@
import { useRef } from 'react';
import { Flex, ScaleFade } from '@chakra-ui/react'; import { Flex, ScaleFade } from '@chakra-ui/react';
import { AnimatedDiv } from '~/components'; import { motionChakra } from '~/components';
import { useBreakpointValue } from '~/context'; import { useBreakpointValue } from '~/context';
import { useBooleanValue, useFormState } from '~/hooks'; import { useBooleanValue, useFormInteractive } from '~/hooks';
import { Title } from './title'; import { Title } from './title';
import type { THeader } from './types'; const Wrapper = motionChakra('header', {
baseStyle: { display: 'flex', px: 4, pt: 6, minH: 16, w: 'full', flex: '0 1 auto' },
});
export const Header = (props: THeader): JSX.Element => { export const Header = (): JSX.Element => {
const { resetForm, ...rest } = props; const formInteractive = useFormInteractive();
const status = useFormState(s => s.status);
const titleRef = useRef({} as HTMLDivElement);
const titleWidth = useBooleanValue( const titleWidth = useBooleanValue(
status === 'results', formInteractive,
{ base: '75%', lg: '50%' }, { base: '75%', lg: '50%' },
{ base: '75%', lg: '75%' }, { base: '75%', lg: '75%' },
); );
@ -23,21 +20,18 @@ export const Header = (props: THeader): JSX.Element => {
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' }); const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
return ( return (
<Flex px={4} pt={6} minH={16} zIndex={4} as="header" width="full" flex="0 1 auto" {...rest}> <Wrapper layout="position">
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}> <ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
<AnimatedDiv <Flex
layout
height="100%" height="100%"
display="flex"
ref={titleRef}
maxW={titleWidth} maxW={titleWidth}
// This is here for the logo // This is here for the logo
justifyContent={justify} justifyContent={justify}
mx={{ base: status === 'results' ? 'auto' : 0, lg: 'auto' }} mx={{ base: formInteractive ? 'auto' : 0, lg: 'auto' }}
> >
<Title /> <Title />
</AnimatedDiv> </Flex>
</ScaleFade> </ScaleFade>
</Flex> </Wrapper>
); );
}; };

View file

@ -3,7 +3,7 @@ import { motion } from 'framer-motion';
import { isSafari } from 'react-device-detect'; import { isSafari } from 'react-device-detect';
import { Switch, Case } from 'react-if'; import { Switch, Case } from 'react-if';
import { useConfig, useMobile } from '~/context'; import { useConfig, useMobile } from '~/context';
import { useBooleanValue, useFormState } from '~/hooks'; import { useFormState, useFormInteractive } from '~/hooks';
import { SubtitleOnly } from './subtitleOnly'; import { SubtitleOnly } from './subtitleOnly';
import { TitleOnly } from './titleOnly'; import { TitleOnly } from './titleOnly';
import { Logo } from './logo'; import { Logo } from './logo';
@ -11,17 +11,18 @@ import { Logo } from './logo';
import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types'; import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types';
const AnimatedVStack = motion(VStack); const AnimatedVStack = motion(VStack);
const AnimatedFlex = motion(Flex);
/** /**
* Title wrapper for mobile devices, breakpoints sm & md. * Title wrapper for mobile devices, breakpoints sm & md.
*/ */
const MWrapper = (props: TMWrapper): JSX.Element => { const MWrapper = (props: TMWrapper): JSX.Element => {
const status = useFormState(s => s.status); const formInteractive = useFormInteractive();
return ( return (
<AnimatedVStack <AnimatedVStack
layout layout
spacing={1} spacing={1}
alignItems={status === 'results' ? 'center' : 'flex-start'} alignItems={formInteractive ? 'center' : 'flex-start'}
{...props} {...props}
/> />
); );
@ -31,13 +32,13 @@ const MWrapper = (props: TMWrapper): JSX.Element => {
* Title wrapper for desktop devices, breakpoints lg & xl. * Title wrapper for desktop devices, breakpoints lg & xl.
*/ */
const DWrapper = (props: TDWrapper): JSX.Element => { const DWrapper = (props: TDWrapper): JSX.Element => {
const status = useFormState(s => s.status); const formInteractive = useFormInteractive();
return ( return (
<AnimatedVStack <AnimatedVStack
spacing={1} spacing={1}
initial="main" initial="main"
alignItems="center" alignItems="center"
animate={status} animate={formInteractive}
transition={{ damping: 15, type: 'spring', stiffness: 100 }} transition={{ damping: 15, type: 'spring', stiffness: 100 }}
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }} variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
{...props} {...props}
@ -108,19 +109,15 @@ export const Title = (props: TTitle): JSX.Element => {
const { web } = useConfig(); const { web } = useConfig();
const { titleMode } = web.text; const { titleMode } = web.text;
const { status, reset } = useFormState(({ status, reset }) => ({ const reset = useFormState(s => s.reset);
status, const formInteractive = useFormInteractive();
reset,
}));
const titleHeight = useBooleanValue(status === 'results', undefined, { md: '20vh' });
return ( return (
<Flex <AnimatedFlex
px={0} px={0}
flexWrap="wrap" flexWrap="wrap"
flexDir="column" flexDir="column"
minH={titleHeight} animate={{ height: formInteractive ? undefined : '20vh' }}
justifyContent="center" justifyContent="center"
/* flexBasis /* flexBasis
This is a fix for Safari specifically. LMGTFY: Safari flex-basis width. Nutshell: Safari This is a fix for Safari specifically. LMGTFY: Safari flex-basis width. Nutshell: Safari
@ -129,7 +126,7 @@ export const Title = (props: TTitle): JSX.Element => {
div up to the parent's max-width. The fix is to hard-code a flex-basis width. div up to the parent's max-width. The fix is to hard-code a flex-basis width.
*/ */
flexBasis={{ base: '100%', lg: isSafari ? '33%' : '100%' }} flexBasis={{ base: '100%', lg: isSafari ? '33%' : '100%' }}
mt={{ md: status === 'results' ? undefined : 'auto' }} mt={{ md: formInteractive ? undefined : 'auto' }}
{...rest} {...rest}
> >
<Button <Button
@ -156,6 +153,6 @@ export const Title = (props: TTitle): JSX.Element => {
</Case> </Case>
</Switch> </Switch>
</Button> </Button>
</Flex> </AnimatedFlex>
); );
}; };

View file

@ -1,12 +1,12 @@
import { Heading } from '@chakra-ui/react'; import { Heading } from '@chakra-ui/react';
import { useConfig } from '~/context'; import { useConfig } from '~/context';
import { useBooleanValue, useFormState } from '~/hooks'; import { useBooleanValue, useFormInteractive } from '~/hooks';
import { useTitleSize } from './useTitleSize'; import { useTitleSize } from './useTitleSize';
export const TitleOnly = (): JSX.Element => { export const TitleOnly = (): JSX.Element => {
const { web } = useConfig(); const { web } = useConfig();
const status = useFormState(s => s.status); const formInteractive = useFormInteractive();
const margin = useBooleanValue(status === 'results', 0, 2); const margin = useBooleanValue(formInteractive, 0, 2);
const sizeSm = useTitleSize(web.text.title, '2xl', []); const sizeSm = useTitleSize(web.text.title, '2xl', []);
return ( return (

View file

@ -1,8 +1,10 @@
export * from './animated';
export * from './card'; export * from './card';
export * from './codeBlock'; export * from './codeBlock';
export * from './countdown'; export * from './countdown';
export * from './custom'; export * from './custom';
export * from './debugger'; export * from './debugger';
export * from './dynamic-icon';
export * from './favicon'; export * from './favicon';
export * from './footer'; export * from './footer';
export * from './form'; export * from './form';
@ -23,4 +25,3 @@ export * from './results';
export * from './select'; export * from './select';
export * from './submit'; export * from './submit';
export * from './table'; export * from './table';
export * from './util';

View file

@ -1,5 +1,6 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { isSafari } from 'react-device-detect'; import { isSafari } from 'react-device-detect';
import { If, Then } from 'react-if'; import { If, Then } from 'react-if';
import { Debugger, Greeting, Footer, Header } from '~/components'; import { Debugger, Greeting, Footer, Header } from '~/components';
@ -9,6 +10,8 @@ import { ResetButton } from './resetButton';
import type { TFrame } from './types'; import type { TFrame } from './types';
const AnimatedFlex = motion(Flex);
export const Frame = (props: TFrame): JSX.Element => { export const Frame = (props: TFrame): JSX.Element => {
const { developerMode } = useConfig(); const { developerMode } = useConfig();
const { setStatus, reset } = useFormState( const { setStatus, reset } = useFormState(
@ -38,19 +41,25 @@ export const Frame = (props: TFrame): JSX.Element => {
*/ */
minHeight={isSafari ? '-webkit-fill-available' : '100vh'} minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
> >
<Header resetForm={() => handleReset()} /> <Header />
<Flex <AnimatedFlex
layout
px={4} px={4}
py={0} py={0}
w="100%" w="100%"
as="main" as="main"
align="center"
flex="1 1 auto" flex="1 1 auto"
justify="start"
flexDir="column" flexDir="column"
textAlign="center" textAlign="center"
{...props} alignItems="center"
/> justifyContent="start"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
>
{props.children}
</AnimatedFlex>
<Footer /> <Footer />
<If condition={developerMode}> <If condition={developerMode}>
<Then> <Then>

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import isEqual from 'react-fast-compare'; import isEqual from 'react-fast-compare';
import { Flex, ScaleFade, SlideFade } from '@chakra-ui/react'; import { chakra, Flex, ScaleFade, SlideFade } from '@chakra-ui/react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { vestResolver } from '@hookform/resolvers/vest'; import { vestResolver } from '@hookform/resolvers/vest';
import vest, { test, enforce } from 'vest'; import vest, { test, enforce } from 'vest';
@ -9,7 +9,6 @@ import {
FormField, FormField,
HelpModal, HelpModal,
QueryType, QueryType,
AnimatedDiv,
QueryTarget, QueryTarget,
SubmitButton, SubmitButton,
QueryLocation, QueryLocation,
@ -169,17 +168,12 @@ export const LookingGlass = (): JSX.Element => {
return ( return (
<FormProvider {...formInstance}> <FormProvider {...formInstance}>
<AnimatedDiv <chakra.form
p={0} p={0}
my={4} my={4}
w="100%" w="100%"
as="form"
mx="auto" mx="auto"
textAlign="left" textAlign="left"
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}
maxW={{ base: '100%', lg: '75%' }} maxW={{ base: '100%', lg: '75%' }}
onSubmit={handleSubmit(submitHandler)} onSubmit={handleSubmit(submitHandler)}
> >
@ -232,7 +226,7 @@ export const LookingGlass = (): JSX.Element => {
</ScaleFade> </ScaleFade>
</Flex> </Flex>
</FormRow> </FormRow>
</AnimatedDiv> </chakra.form>
</FormProvider> </FormProvider>
); );
}; };

View file

@ -1,2 +0,0 @@
export * from './animated';
export * from './dynamic-icon';

View file

@ -1,4 +0,0 @@
export interface TIf {
c: boolean;
children?: React.ReactNode;
}

View file

@ -235,3 +235,8 @@ export function useView(): FormStatus {
return ready ? 'results' : 'form'; return ready ? 'results' : 'form';
}, [status, form]); }, [status, form]);
} }
export function useFormInteractive(): boolean {
const { status, selections } = useFormState(({ status, selections }) => ({ status, selections }));
return status === 'results' || selections.queryLocation.length > 0;
}