Skip to content

Commit

Permalink
refactor(core): unify focusManager and onlineManager
Browse files Browse the repository at this point in the history
into an eventManager, which is function based
  • Loading branch information
TkDodo committed Dec 8, 2021
1 parent 96b7895 commit f1cb1a6
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 152 deletions.
78 changes: 78 additions & 0 deletions src/core/eventManager.ts
@@ -0,0 +1,78 @@
import { isServer } from './utils'

type ListenerFn = () => void

export function createEventManager(
events: ReadonlyArray<Parameters<typeof window['addEventListener']>[0]>
) {
let value: boolean | undefined
let removeEventListener: ListenerFn | undefined
let listeners: ListenerFn[] = []
let setupFn: (
param: (newValue?: boolean) => void
) => ListenerFn | undefined = onEvent => {
if (!isServer && window?.addEventListener) {
const listener = () => onEvent()
events.forEach(eventName => {
window.addEventListener(eventName, listener, false)
})

return () => {
events.forEach(eventName => {
window.removeEventListener(eventName, listener)
})
}
}
}

const subscribe = (listener: ListenerFn): ListenerFn => {
listeners.push(listener)

if (!removeEventListener) {
setEventListener(setupFn)
}

return () => {
listeners = listeners.filter(x => x !== listener)
if (listeners.length === 0) {
removeEventListener?.()
removeEventListener = undefined
}
}
}

const setValue = (newValue?: boolean): void => {
value = newValue

if (newValue) {
onEvent()
}
}

const setEventListener = (
setup: (param: (newValue?: boolean) => void) => ListenerFn | undefined
): void => {
removeEventListener?.()
setupFn = setup
removeEventListener = setupFn(newValue => {
if (typeof newValue === 'boolean') {
setValue(newValue)
} else {
onEvent()
}
})
}

const onEvent = (): void => {
listeners.forEach(listener => {
listener()
})
}

return {
setEventListener,
subscribe,
setValue,
getValue: () => value,
} as const
}
95 changes: 25 additions & 70 deletions src/core/focusManager.ts
@@ -1,76 +1,31 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'

class FocusManager extends Subscribable {
private focused?: boolean
private removeEventListener?: () => void

protected onSubscribe(): void {
if (!this.removeEventListener) {
this.setDefaultEventListener()
}
}

setEventListener(
setup: (setFocused: (focused?: boolean) => void) => () => void
): void {
if (this.removeEventListener) {
this.removeEventListener()
}
this.removeEventListener = setup(focused => {
if (typeof focused === 'boolean') {
this.setFocused(focused)
} else {
this.onFocus()
import { createEventManager } from './eventManager'

export const createFocusManager = () => {
const { setEventListener, subscribe, ...manager } = createEventManager([
'visibilitychange',
'focus',
])

return {
subscribe,
setEventListener,
setFocused: manager.setValue,
isFocused: (): boolean => {
const value = manager.getValue()
if (typeof value === 'boolean') {
return value
}
})
}

setFocused(focused?: boolean): void {
this.focused = focused

if (focused) {
this.onFocus()
}
}

onFocus(): void {
this.listeners.forEach(listener => {
listener()
})
}

isFocused(): boolean {
if (typeof this.focused === 'boolean') {
return this.focused
}

// document global can be unavailable in react native
if (typeof document === 'undefined') {
return true
}

return [undefined, 'visible', 'prerender'].includes(
document.visibilityState
)
}

private setDefaultEventListener() {
if (!isServer && window?.addEventListener) {
this.setEventListener(onFocus => {
const listener = () => onFocus()
// Listen to visibillitychange and focus
window.addEventListener('visibilitychange', listener, false)
window.addEventListener('focus', listener, false)
// document global can be unavailable in react native
if (typeof document === 'undefined') {
return true
}

return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('visibilitychange', listener)
window.removeEventListener('focus', listener)
}
})
}
return [undefined, 'visible', 'prerender'].includes(
document.visibilityState
)
},
}
}

export const focusManager = new FocusManager()
export const focusManager = createFocusManager()
95 changes: 25 additions & 70 deletions src/core/onlineManager.ts
@@ -1,76 +1,31 @@
import { Subscribable } from './subscribable'
import { isServer } from './utils'

class OnlineManager extends Subscribable {
private online?: boolean
private removeEventListener?: () => void

protected onSubscribe(): void {
if (!this.removeEventListener) {
this.setDefaultEventListener()
}
}

setEventListener(
setup: (setOnline: (online?: boolean) => void) => () => void
): void {
if (this.removeEventListener) {
this.removeEventListener()
}
this.removeEventListener = setup((online?: boolean) => {
if (typeof online === 'boolean') {
this.setOnline(online)
} else {
this.onOnline()
import { createEventManager } from './eventManager'

export const createOnlineManager = () => {
const { setEventListener, subscribe, ...manager } = createEventManager([
'online',
'offline',
])

return {
subscribe,
setEventListener,
setOnline: manager.setValue,
isOnline: (): boolean => {
const value = manager.getValue()
if (typeof value === 'boolean') {
return value
}
})
}

setOnline(online?: boolean): void {
this.online = online

if (online) {
this.onOnline()
}
}

onOnline(): void {
this.listeners.forEach(listener => {
listener()
})
}

isOnline(): boolean {
if (typeof this.online === 'boolean') {
return this.online
}

if (
typeof navigator === 'undefined' ||
typeof navigator.onLine === 'undefined'
) {
return true
}

return navigator.onLine
}

private setDefaultEventListener() {
if (!isServer && window?.addEventListener) {
this.setEventListener(onOnline => {
const listener = () => onOnline()
// Listen to online
window.addEventListener('online', listener, false)
window.addEventListener('offline', listener, false)
if (
typeof navigator === 'undefined' ||
typeof navigator.onLine === 'undefined'
) {
return true
}

return () => {
// Be sure to unsubscribe if a new handler is set
window.removeEventListener('online', listener)
window.removeEventListener('offline', listener)
}
})
}
return navigator.onLine
},
}
}

export const onlineManager = new OnlineManager()
export const onlineManager = createOnlineManager()
12 changes: 6 additions & 6 deletions src/core/tests/focusManager.test.tsx
@@ -1,10 +1,10 @@
import { sleep } from '../utils'
import { focusManager } from '../focusManager'
import { createFocusManager } from '../focusManager'

describe('focusManager', () => {
afterEach(() => {
// Reset removeEventListener private property to avoid side effects between tests
focusManager['removeEventListener'] = undefined
let focusManager: ReturnType<typeof createFocusManager>
beforeEach(() => {
focusManager = createFocusManager()
})

it('should call previous remove handler when replacing an event listener', () => {
Expand Down Expand Up @@ -69,7 +69,7 @@ describe('focusManager', () => {

const setEventListenerSpy = jest.spyOn(focusManager, 'setEventListener')

const unsubscribe = focusManager.subscribe()
const unsubscribe = focusManager.subscribe(() => undefined)
expect(setEventListenerSpy).toHaveBeenCalledTimes(0)

unsubscribe()
Expand All @@ -88,7 +88,7 @@ describe('focusManager', () => {
)

// Should set the default event listener with window event listeners
const unsubscribe = focusManager.subscribe()
const unsubscribe = focusManager.subscribe(() => undefined)
expect(addEventListenerSpy).toHaveBeenCalledTimes(2)

// Should replace the window default event listener by a new one
Expand Down
12 changes: 6 additions & 6 deletions src/core/tests/onlineManager.test.tsx
@@ -1,10 +1,10 @@
import { onlineManager } from '../onlineManager'
import { createOnlineManager } from '../onlineManager'
import { sleep } from '../utils'

describe('onlineManager', () => {
afterEach(() => {
// Reset removeEventListener private property to avoid side effects between tests
onlineManager['removeEventListener'] = undefined
let onlineManager: ReturnType<typeof createOnlineManager>
beforeEach(() => {
onlineManager = createOnlineManager()
})

test('isOnline should return true if navigator is undefined', () => {
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('onlineManager', () => {

const setEventListenerSpy = jest.spyOn(onlineManager, 'setEventListener')

const unsubscribe = onlineManager.subscribe()
const unsubscribe = onlineManager.subscribe(() => undefined)
expect(setEventListenerSpy).toHaveBeenCalledTimes(0)

unsubscribe()
Expand All @@ -83,7 +83,7 @@ describe('onlineManager', () => {
)

// Should set the default event listener with window event listeners
const unsubscribe = onlineManager.subscribe()
const unsubscribe = onlineManager.subscribe(() => undefined)
expect(addEventListenerSpy).toHaveBeenCalledTimes(2)

// Should replace the window default event listener by a new one
Expand Down

0 comments on commit f1cb1a6

Please sign in to comment.