-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #289 from Vizzuality/feature/help
Help
- Loading branch information
Showing
26 changed files
with
890 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './component'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './component'; |
Oops, something went wrong.