UNPKG

react-native-unistyles

Version:
306 lines (267 loc) 10.9 kB
import { NativeEventEmitter, NativeModules } from 'react-native' import type { UnistylesThemes, UnistylesBreakpoints } from 'react-native-unistyles' import type { ColorSchemeName, ScreenInsets, StatusBar, NavigationBar, Color } from '../types' import { normalizeWebStylesPlugin } from '../plugins' import { isServer } from '../common' export class UnistylesBridgeWeb { #timerRef?: ReturnType<typeof setTimeout> = undefined #windowResizeDebounceTimeMs: number = 100 #hasAdaptiveThemes: boolean = false #supportsAutomaticColorScheme = false #screenWidth = isServer ? undefined : window.innerWidth #screenHeight = isServer ? undefined : window.innerHeight #themes: Array<keyof UnistylesThemes> = [] #breakpoints: UnistylesBreakpoints = {} as UnistylesBreakpoints #colorScheme: ColorSchemeName = this.getPreferredColorScheme() #themeName: keyof UnistylesThemes = '' as keyof UnistylesThemes #enabledPlugins: Array<string> = [normalizeWebStylesPlugin.name] #unistylesEvents = new NativeEventEmitter(NativeModules.Unistyles) #sortedBreakpointPairs: Array<[keyof UnistylesBreakpoints, number]> = [] #breakpoint: keyof UnistylesBreakpoints = '' as keyof UnistylesBreakpoints #contentSizeCategory: string = 'unspecified' #insets: ScreenInsets = { top: 0, right: 0, bottom: 0, left: 0 } #statusBar: StatusBar = { height: 0, width: 0, setColor: () => {}, setHidden: () => {} } #navigationBar: NavigationBar = { height: 0, width: 0, setColor: () => {}, setHidden: () => {} } #pixelRatio = 1.0 #fontScale = 1.0 #hairlineWidth = 1 #rtl = false constructor() { if (!isServer) { this.setupListeners() this.#screenWidth = window.innerWidth this.#screenHeight = window.innerHeight this.#rtl = document.documentElement.dir === 'rtl' } } public install() { // @ts-ignore // eslint-disable-next-line no-undef globalThis.__UNISTYLES__ = new Proxy({}, { get: (_target, prop) => { switch (prop) { case 'themeName': return this.getTheme() case 'screenWidth': return this.#screenWidth case 'screenHeight': return this.#screenHeight case 'contentSizeCategory': return this.#contentSizeCategory case 'breakpoint': return this.#breakpoint || undefined case 'breakpoints': return this.#breakpoints case 'hasAdaptiveThemes': return this.#hasAdaptiveThemes case 'sortedBreakpointPairs': return this.#sortedBreakpointPairs case 'enabledPlugins': return this.#enabledPlugins case 'colorScheme': return this.#colorScheme case 'insets': return this.#insets case 'statusBar': return this.#statusBar case 'navigationBar': return this.#navigationBar case 'pixelRatio': return this.#pixelRatio case 'fontScale': return this.#fontScale case 'hairlineWidth': return this.#hairlineWidth case 'rtl': return this.#rtl case 'useTheme': return (themeName: keyof UnistylesThemes) => this.useTheme(themeName) case 'updateTheme': return (themeName: keyof UnistylesThemes) => this.updateTheme(themeName) case 'useBreakpoints': return (breakpoints: UnistylesBreakpoints) => this.useBreakpoints(breakpoints) case 'useAdaptiveThemes': return (enable: boolean) => this.useAdaptiveThemes(enable) case 'addPlugin': return (pluginName: string, notify: boolean) => this.addPlugin(pluginName, notify) case 'removePlugin': return (pluginName: string) => this.removePlugin(pluginName) case 'setRootViewBackgroundColor': return (color: Color | string) => this.setRootViewBackgroundColor(color) case 'setImmersiveMode': return () => {} case 'setWindowResizeDebounceTimeMs': return (timeMs: number) => { if (timeMs >= 0) { this.#windowResizeDebounceTimeMs = timeMs } } default: return Reflect.get(this, prop) } }, set: (target, prop, newValue, receiver) => { switch (prop) { case 'themes': { this.#themes = newValue this.#supportsAutomaticColorScheme = newValue.includes('light') && newValue.includes('dark') return true } case 'themeName': { this.#themeName = newValue as keyof UnistylesThemes this.emitThemeChange() return true } default: return Reflect.set(target, prop, newValue, receiver) } } }) return true } private useTheme(themeName: keyof UnistylesThemes) { this.#themeName = themeName this.emitThemeChange() } private updateTheme(themeName: keyof UnistylesThemes) { if (!this.#themeName) { this.#themeName = this.getTheme() } if (this.#themeName === themeName) { this.emitThemeChange() } } private useBreakpoints(breakpoints: UnistylesBreakpoints) { this.#breakpoints = breakpoints this.#sortedBreakpointPairs = Object .entries(breakpoints) .sort(([, a], [, b]) => (a ?? 0) - (b ?? 0)) as Array<[keyof UnistylesBreakpoints, number]> if (!isServer) { this.#breakpoint = this.getBreakpointFromScreenWidth(this.#screenWidth as number) } } private useAdaptiveThemes(enable: boolean) { this.#hasAdaptiveThemes = enable if (!this.#hasAdaptiveThemes || !this.#supportsAutomaticColorScheme) { return } if (this.#themeName !== this.#colorScheme) { this.#themeName = this.#colorScheme as keyof UnistylesThemes this.emitThemeChange() } } private addPlugin(pluginName: string, notify: boolean) { this.#enabledPlugins = [pluginName].concat(this.#enabledPlugins) if (notify) { this.emitPluginChange() } } private removePlugin(pluginName: string) { this.#enabledPlugins = this.#enabledPlugins.filter(name => name !== pluginName) this.emitPluginChange() } private getTheme(): keyof UnistylesThemes { if (this.#themes.length === 1) { return this.#themes.at(0) as keyof UnistylesThemes } return this.#themeName } private setupListeners() { const onResize = () => { this.#screenWidth = window.innerWidth this.#screenHeight = window.innerHeight this.#breakpoint = this.getBreakpointFromScreenWidth(this.#screenWidth) this.emitLayoutChange() } window.addEventListener('resize', () => { if (this.#windowResizeDebounceTimeMs === 0) { return onResize() } clearTimeout(this.#timerRef) this.#timerRef = setTimeout(onResize, this.#windowResizeDebounceTimeMs) }) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { this.#colorScheme = event.matches ? 'dark' : 'light' if (!this.#supportsAutomaticColorScheme || !this.#hasAdaptiveThemes) { return } if (this.#colorScheme !== this.#themeName) { this.#themeName = this.#colorScheme as keyof UnistylesThemes this.emitThemeChange() } }) new MutationObserver(() => { this.#rtl = document.documentElement.dir === 'rtl' }).observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] }) } private getBreakpointFromScreenWidth(width: number): keyof UnistylesBreakpoints { const breakpoint = this.#sortedBreakpointPairs .find(([, value], index, otherBreakpoints) => { const minVal = value const maxVal = otherBreakpoints[index + 1]?.[1] if (!maxVal) { return true } return width >= minVal && width < maxVal }) return breakpoint?.at(0) as keyof UnistylesBreakpoints } private getPreferredColorScheme() { if (!isServer && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark' } return 'light' } private setRootViewBackgroundColor(color?: Color | string) { document.body.style.backgroundColor = color as string } private emitPluginChange() { this.#unistylesEvents.emit('__unistylesOnChange', { type: 'plugin' }) } private emitThemeChange() { this.#unistylesEvents.emit('__unistylesOnChange', { type: 'theme', payload: { themeName: this.#themeName } }) } private emitLayoutChange() { this.#unistylesEvents.emit('__unistylesOnChange', { type: 'layout', payload: { breakpoint: this.#breakpoint, orientation: (this.#screenWidth as number) > (this.#screenHeight as number) ? 'landscape' : 'portrait', screen: { width: this.#screenWidth, height: this.#screenHeight } } }) } } export const UnistylesModule = new UnistylesBridgeWeb()