Skip to content

Commit

Permalink
Merge pull request #289 from Vizzuality/feature/help
Browse files Browse the repository at this point in the history
Help
  • Loading branch information
mbarrenechea authored Jun 28, 2021
2 parents ca94b82 + 1be897f commit 7600647
Show file tree
Hide file tree
Showing 26 changed files with 890 additions and 82 deletions.
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

0 comments on commit 7600647

Please sign in to comment.