Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help #289

Merged
merged 10 commits into from
Jun 28, 2021
Merged

Help #289

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions app/hooks/help/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, {
createContext, useCallback, useContext, useState,
} from 'react';

import { BeaconProps, HelpContextProps, HelpProviderProps } from './types';

const HelpContext = createContext<HelpContextProps>({
active: false,
onActive: (active) => { console.info(active); },
beacons: {},
addBeacon: (beacon) => { console.info(beacon); },
removeBeacon: (beacon) => { console.info(beacon); },
});

// Hook for child components to get the toast object ...
// and re-render when it changes.
export const useHelp = () => {
const ctx = useContext(HelpContext);

if (!ctx) {
throw Error(
'The `useHelp` hook must be called from a descendent of the `HelpProvider`.',
);
}

return {
active: ctx.active,
onActive: ctx.onActive,
beacons: ctx.beacons,
addBeacon: ctx.addBeacon,
removeBeacon: ctx.removeBeacon,
};
};

// Provider component that wraps your app and makes toast object ...
// ... available to any child component that calls useHelp().
export function HelpProvider({
children,
}: HelpProviderProps) {
const [active, setActive] = useState(false);
const [beacons, setBeacons] = useState<Record<string, BeaconProps>>({});

const onActive = useCallback((a: boolean) => {
return setActive(a);
}, [setActive]);

const addBeacon = useCallback(({ id, state, update }) => {
setBeacons((prevBeacons) => {
return {
...prevBeacons,
[id]: {
id,
state,
update,
},
};
});
}, []);

const removeBeacon = useCallback((id: string) => {
const { [id]: omitted, ...rest } = beacons;
setBeacons(rest);
}, [beacons]);

return (
<HelpContext.Provider
value={{
active,
onActive,
beacons,
addBeacon,
removeBeacon,
}}
>
{children}
</HelpContext.Provider>
);
}
19 changes: 19 additions & 0 deletions app/hooks/help/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactNode } from 'react';

export interface BeaconProps {
id: string;
update: () => void;
state: any;
}

export interface HelpContextProps {
active: boolean;
onActive: (active: boolean) => void;
beacons: Record<string, BeaconProps>,
addBeacon: (beacon: Record<string, unknown>) => void,
removeBeacon: (beaconId: string) => void,
}

export interface HelpProviderProps {
children: ReactNode;
}
146 changes: 146 additions & 0 deletions app/layout/help/beacon/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, {
ReactNode, ReactElement, cloneElement, useState,
useEffect,
useCallback,
useRef,
} from 'react';
import { createPortal } from 'react-dom';

import cx from 'classnames';

import { useHelp } from 'hooks/help';
import { usePopper } from 'react-popper';
import { useResizeDetector } from 'react-resize-detector';

import { AnimatePresence } from 'framer-motion';

import Tooltip from 'components/tooltip';
import HelpTooltip from 'layout/help/tooltip';
import HelpSpotlight from 'layout/help/spotlight';

const flipModifier = {
name: 'flip',
enabled: false,
};

const hideModifier = {
name: 'hide',
enabled: true,
};
export interface HelpBeaconProps {
id: string;
title: string;
subtitle: string;
content: ReactNode;
children: ReactElement;
}

export const HelpBeacon: React.FC<HelpBeaconProps> = ({
id,
title,
subtitle,
content,
children,
}: HelpBeaconProps) => {
const { active, beacons, addBeacon } = useHelp();
const [visible, setVisible] = useState(false);
const childrenRef = useRef(null);
const [beaconRef, setBeaconRef] = useState(null);

const CHILDREN = cloneElement(children, {
ref: childrenRef,
});

const onResize = useCallback(() => {
Object.keys(beacons).forEach((k) => {
const b = beacons[k];
if (b.update) b.update();
});
}, [beacons]);

// 'usePopper'
const {
styles, attributes, state, update,
} = usePopper(childrenRef.current, beaconRef, {
placement: 'top-start',
modifiers: [
flipModifier,
hideModifier,
],
});

useResizeDetector({
targetRef: childrenRef,
onResize,
});

useEffect(() => {
addBeacon({
id,
state,
update,
});
}, [active, addBeacon, id, state, update, childrenRef, beaconRef]);

return (
<>
<Tooltip
arrow
placement="bottom"
visible={visible && active}
maxWidth={350}
onClickOutside={() => {
setVisible(false);
}}
content={(
<HelpTooltip
title={title}
subtitle={subtitle}
content={content}
/>
)}
>
{CHILDREN}
</Tooltip>

{typeof window !== 'undefined' && active && !visible && createPortal(
<div
ref={((el) => setBeaconRef(el))}
className={cx({
'z-50': true,
'visible pointer-events-auto': active,
'invisible pointer-events-none': !active || attributes?.popper?.['data-popper-reference-hidden'] || attributes?.popper?.['data-popper-escaped'],
})}
style={styles.popper}
{...attributes.popper}
>
<button
type="button"
className={cx({
'relative beacon flex items-center justify-center w-6 h-6 bg-primary-500 border-2 border-gray-700 transition rounded-full focus:outline-none transform translate-x-1/2 translate-y-1/2': true,
})}
onClick={() => {
setVisible(!visible);
}}
>
<div className="absolute top-0 bottom-0 left-0 right-0 border-2 rounded-full pointer-events-none animate-pulse border-primary-500" />
</button>
</div>,
document?.body,
)}

{typeof window !== 'undefined' && active && createPortal(
<AnimatePresence>
{visible && (
<HelpSpotlight
childrenRef={childrenRef}
/>
)}
</AnimatePresence>,
document?.body,
)}
</>
);
};

export default HelpBeacon;
1 change: 1 addition & 0 deletions app/layout/help/beacon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
101 changes: 101 additions & 0 deletions app/layout/help/button/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useCallback } from 'react';
import cx from 'classnames';

import Icon from 'components/icon';

import { motion } from 'framer-motion';

import { useHelp } from 'hooks/help';

import HELP_SVG from 'svgs/ui/help.svg?sprite';

export const HelpButton = () => {
const { active, onActive } = useHelp();

const onToggleActive = useCallback((e) => {
e.preventDefault();
onActive(!active);
}, [active, onActive]);

return (
<div>
<div
className="fixed bottom-0 right-0 z-50 p-2"
>
<div className="flex flex-col items-center justify-center space-y-2">
<button
type="button"
className={cx({
'relative w-8 focus:outline-none h-14 rounded-4xl p-1': true,
})}
style={{
boxShadow: '2px 1px 3px 0px rgba(0,0,0,0.5) inset',
}}
onClick={onToggleActive}
>
<div
className={cx({
'absolute z-10 transform -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 rounded-4xl transition-colors w-full h-full': true,
'bg-gray-500': !active,
'bg-primary-500': active,
})}
/>
<motion.div
className="absolute z-0 transform -translate-x-1/2 -translate-y-1/2 bg-black border border-white border-opacity-25 top-1/2 left-1/2 rounded-4xl"
style={{
width: 'calc(100% + 7px)',
height: 'calc(100% + 7px)',
}}
animate={active ? 'enter' : 'exit'}
transition={{
duration: 0.2,
}}
initial={{
opacity: '0%',
}}
variants={{
enter: {
opacity: '100%',
},
exit: {
opacity: '0%',
},
}}
/>
<div className="relative z-20 w-full h-full">
<motion.span
className="absolute flex items-center justify-center w-6 h-6 transform bg-white rounded-full shadow left-1/2"
animate={active ? 'enter' : 'exit'}
transition={{
duration: 0.2,
}}
initial={{
bottom: '0%',
y: '0%',
x: '-50%',
}}
variants={{
enter: {
bottom: '100%',
y: '100%',
x: '-50%',
},
exit: {
bottom: '0%',
y: '0%',
x: '-50%',
},
}}
>
<Icon icon={HELP_SVG} className="w-4 h-4" />
</motion.span>
</div>
</button>
<span className="block w-10 leading-tight text-center text-white text-xxs">Activate guide</span>
</div>
</div>
</div>
);
};

export default HelpButton;
1 change: 1 addition & 0 deletions app/layout/help/button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
Loading