Skip to content

Commit

Permalink
feat: open/close events
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrieljablonski committed Oct 31, 2023
1 parent d7f98e8 commit 9606cfe
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 40 deletions.
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ function App() {
<Tooltip
anchorSelect="section[id='section-anchor-select'] > p > button"
place="bottom"
events={['click']}
openEvents={{ click: true }}
closeEvents={{ click: true }}
globalCloseEvents={{ clickOutsideAnchor: true }}
>
Tooltip content
</Tooltip>
Expand Down
154 changes: 119 additions & 35 deletions src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import { getScrollParent } from 'utils/get-scroll-parent'
import { computeTooltipPosition } from 'utils/compute-positions'
import coreStyles from './core-styles.module.css'
import styles from './styles.module.css'
import type { IPosition, ITooltip, PlacesType } from './TooltipTypes'
import type {
AnchorCloseEvents,
AnchorOpenEvents,
GlobalCloseEvents,
IPosition,
ITooltip,
PlacesType,
} from './TooltipTypes'

const Tooltip = ({
// props
Expand All @@ -34,6 +41,9 @@ const Tooltip = ({
closeOnEsc = false,
closeOnScroll = false,
closeOnResize = false,
openEvents,
closeEvents,
globalCloseEvents,
style: externalStyles,
position,
afterShow,
Expand Down Expand Up @@ -68,7 +78,49 @@ const Tooltip = ({
const [anchorsBySelect, setAnchorsBySelect] = useState<HTMLElement[]>([])
const mounted = useRef(false)

/**
* @todo Update when deprecated stuff gets removed.
*/
const shouldOpenOnClick = openOnClick || events.includes('click')
const hasClickEvent =
shouldOpenOnClick || openEvents?.click || openEvents?.dblclick || openEvents?.mousedown
const actualOpenEvents: AnchorOpenEvents = openEvents
? { ...openEvents }
: {
mouseenter: true,
focus: true,
click: false,
dblclick: false,
mousedown: false,
}
if (!openEvents && shouldOpenOnClick) {
Object.assign(actualOpenEvents, {
mouseenter: false,
focus: false,
click: true,
})
}
const actualCloseEvents: AnchorCloseEvents = closeEvents
? { ...closeEvents }
: {
mouseleave: true,
blur: true,
click: false,
}
if (!closeEvents && shouldOpenOnClick) {
Object.assign(actualCloseEvents, {
mouseleave: false,
blur: false,
})
}
const actualGlobalCloseEvents: GlobalCloseEvents = globalCloseEvents
? { ...globalCloseEvents }
: {
escape: closeOnEsc || false,
scroll: closeOnScroll || false,
resize: closeOnResize || false,
clickOutsideAnchor: hasClickEvent || false,
}

/**
* useLayoutEffect runs before useEffect,
Expand Down Expand Up @@ -266,13 +318,6 @@ const Tooltip = ({
lastFloatPosition.current = mousePosition
}

const handleClickTooltipAnchor = (event?: Event) => {
handleShowTooltip(event)
if (delayHide) {
handleHideTooltipDelayed()
}
}

const handleClickOutsideAnchors = (event: MouseEvent) => {
const anchorById = document.querySelector<HTMLElement>(`[id='${anchorId}']`)
const anchors = [anchorById, ...anchorsBySelect]
Expand Down Expand Up @@ -371,13 +416,13 @@ const Tooltip = ({
const anchorScrollParent = getScrollParent(activeAnchor)
const tooltipScrollParent = getScrollParent(tooltipRef.current)

if (closeOnScroll) {
if (actualGlobalCloseEvents.scroll) {
window.addEventListener('scroll', handleScrollResize)
anchorScrollParent?.addEventListener('scroll', handleScrollResize)
tooltipScrollParent?.addEventListener('scroll', handleScrollResize)
}
let updateTooltipCleanup: null | (() => void) = null
if (closeOnResize) {
if (actualGlobalCloseEvents.resize) {
window.addEventListener('resize', handleScrollResize)
} else if (activeAnchor && tooltipRef.current) {
updateTooltipCleanup = autoUpdate(
Expand All @@ -398,29 +443,63 @@ const Tooltip = ({
}
handleShow(false)
}

if (closeOnEsc) {
if (actualGlobalCloseEvents.escape) {
window.addEventListener('keydown', handleEsc)
}

if (actualGlobalCloseEvents.clickOutsideAnchor) {
window.addEventListener('click', handleClickOutsideAnchors)
}

const enabledEvents: { event: string; listener: (event?: Event) => void }[] = []

if (shouldOpenOnClick) {
window.addEventListener('click', handleClickOutsideAnchors)
enabledEvents.push({ event: 'click', listener: handleClickTooltipAnchor })
} else {
enabledEvents.push(
{ event: 'mouseenter', listener: debouncedHandleShowTooltip },
{ event: 'mouseleave', listener: debouncedHandleHideTooltip },
{ event: 'focus', listener: debouncedHandleShowTooltip },
{ event: 'blur', listener: debouncedHandleHideTooltip },
)
if (float) {
enabledEvents.push({
event: 'mousemove',
listener: handleMouseMove,
})
const handleClickOpenTooltipAnchor = (event?: Event) => {
if (show) {
return
}
handleShowTooltip(event)
}
const handleClickCloseTooltipAnchor = () => {
if (!show) {
return
}
handleHideTooltip()
}

const regularEvents = ['mouseenter', 'mouseleave', 'focus', 'blur']
const clickEvents = ['click', 'dblclick', 'mousedown', 'mouseup']

Object.entries(actualOpenEvents).forEach(([event, enabled]) => {
if (!enabled) {
return
}
if (regularEvents.includes(event)) {
enabledEvents.push({ event, listener: debouncedHandleShowTooltip })
} else if (clickEvents.includes(event)) {
enabledEvents.push({ event, listener: handleClickOpenTooltipAnchor })
} else {
// never happens
}
})

Object.entries(actualCloseEvents).forEach(([event, enabled]) => {
if (!enabled) {
return
}
if (regularEvents.includes(event)) {
enabledEvents.push({ event, listener: debouncedHandleHideTooltip })
} else if (clickEvents.includes(event)) {
enabledEvents.push({ event, listener: handleClickCloseTooltipAnchor })
} else {
// never happens
}
})

if (float) {
enabledEvents.push({
event: 'mousemove',
listener: handleMouseMove,
})
}

const handleMouseEnterTooltip = () => {
Expand All @@ -431,7 +510,9 @@ const Tooltip = ({
handleHideTooltip()
}

if (clickable && !shouldOpenOnClick) {
if (clickable && !hasClickEvent) {
// used to keep the tooltip open when hovering content.
// not needed if using click events.
tooltipRef.current?.addEventListener('mouseenter', handleMouseEnterTooltip)
tooltipRef.current?.addEventListener('mouseleave', handleMouseLeaveTooltip)
}
Expand All @@ -443,23 +524,23 @@ const Tooltip = ({
})

return () => {
if (closeOnScroll) {
if (actualGlobalCloseEvents.scroll) {
window.removeEventListener('scroll', handleScrollResize)
anchorScrollParent?.removeEventListener('scroll', handleScrollResize)
tooltipScrollParent?.removeEventListener('scroll', handleScrollResize)
}
if (closeOnResize) {
if (actualGlobalCloseEvents.resize) {
window.removeEventListener('resize', handleScrollResize)
} else {
updateTooltipCleanup?.()
}
if (shouldOpenOnClick) {
if (actualGlobalCloseEvents.clickOutsideAnchor) {
window.removeEventListener('click', handleClickOutsideAnchors)
}
if (closeOnEsc) {
if (actualGlobalCloseEvents.escape) {
window.removeEventListener('keydown', handleEsc)
}
if (clickable && !shouldOpenOnClick) {
if (clickable && !hasClickEvent) {
tooltipRef.current?.removeEventListener('mouseenter', handleMouseEnterTooltip)
tooltipRef.current?.removeEventListener('mouseleave', handleMouseLeaveTooltip)
}
Expand All @@ -479,8 +560,11 @@ const Tooltip = ({
rendered,
anchorRefs,
anchorsBySelect,
closeOnEsc,
events,
// the effect uses the `actual*Events` objects, but this should work
openEvents,
closeEvents,
globalCloseEvents,
shouldOpenOnClick,
])

useEffect(() => {
Expand Down
24 changes: 24 additions & 0 deletions src/components/Tooltip/TooltipTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export interface IPosition {
y: number
}

export type AnchorOpenEvents = {
mouseenter?: boolean
focus?: boolean
click?: boolean
dblclick?: boolean
mousedown?: boolean
}
export type AnchorCloseEvents = {
mouseleave?: boolean
blur?: boolean
click?: boolean
dblclick?: boolean
mouseup?: boolean
}
export type GlobalCloseEvents = {
escape?: boolean
scroll?: boolean
resize?: boolean
clickOutsideAnchor?: boolean
}

export interface ITooltip {
className?: string
classNameArrow?: string
Expand Down Expand Up @@ -81,6 +102,9 @@ export interface ITooltip {
closeOnEsc?: boolean
closeOnScroll?: boolean
closeOnResize?: boolean
openEvents?: AnchorOpenEvents
closeEvents?: AnchorCloseEvents
globalCloseEvents?: GlobalCloseEvents
style?: CSSProperties
position?: IPosition
isOpen?: boolean
Expand Down
6 changes: 6 additions & 0 deletions src/components/TooltipController/TooltipController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const TooltipController = ({
closeOnEsc = false,
closeOnScroll = false,
closeOnResize = false,
openEvents,
closeEvents,
globalCloseEvents,
style,
position,
isOpen,
Expand Down Expand Up @@ -330,6 +333,9 @@ const TooltipController = ({
closeOnEsc,
closeOnScroll,
closeOnResize,
openEvents,
closeEvents,
globalCloseEvents,
style,
position,
isOpen,
Expand Down
23 changes: 19 additions & 4 deletions src/components/TooltipController/TooltipControllerTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type {
PositionStrategy,
IPosition,
Middleware,
AnchorOpenEvents,
AnchorCloseEvents,
GlobalCloseEvents,
} from 'components/Tooltip/TooltipTypes'

export interface ITooltipController {
Expand All @@ -33,7 +36,7 @@ export interface ITooltipController {
wrapper?: WrapperType
children?: ChildrenType
/**
* @deprecated Use `openOnClick` instead.
* @deprecated Use `openOnClick` or `openEvents`/`closeEvents` instead.
*/
events?: EventsType[]
openOnClick?: boolean
Expand All @@ -46,17 +49,29 @@ export interface ITooltipController {
noArrow?: boolean
clickable?: boolean
/**
* @todo refactor to `hideOnEsc` for naming consistency
* @deprecated Use `globalCloseEvents={{ escape: true }}` instead.
*/
closeOnEsc?: boolean
/**
* @todo refactor to `hideOnScroll` for naming consistency
* @deprecated Use `globalCloseEvents={{ scroll: true }}` instead.
*/
closeOnScroll?: boolean
/**
* @todo refactor to `hideOnResize` for naming consistency
* @deprecated Use `globalCloseEvents={{ resize: true }}` instead.
*/
closeOnResize?: boolean
/**
* @description The events to be listened on anchor elements to open the tooltip.
*/
openEvents?: AnchorOpenEvents
/**
* @description The events to be listened on anchor elements to close the tooltip.
*/
closeEvents?: AnchorCloseEvents
/**
* @description The global events listened to close the tooltip.
*/
globalCloseEvents?: GlobalCloseEvents
style?: CSSProperties
position?: IPosition
isOpen?: boolean
Expand Down

0 comments on commit 9606cfe

Please sign in to comment.