Skip to content

Commit

Permalink
feat: add react util function
Browse files Browse the repository at this point in the history
  • Loading branch information
bobbychan committed Jun 19, 2024
1 parent caf5e04 commit 0f0a781
Show file tree
Hide file tree
Showing 9 changed files with 8,285 additions and 6,478 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-bottles-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alice-ui/react-utils': patch
---

Add utils function
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# auto-install-peers = true
strict-peer-dependencies=false
enable-pre-post-scripts=true
public-hoist-pattern[]=*tailwind-variants*
public-hoist-pattern[]=*framer-motion*
public-hoist-pattern[]=*@alice-ui/theme*
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@
"react-dom": "^18.2.0"
}
},
"packageManager": "pnpm@8.6.10"
"engines": {
"node": ">=20.x",
"pnpm": ">=8.x"
},
"packageManager": "pnpm@8.7.0"
}
6 changes: 5 additions & 1 deletion packages/utilities/react-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@
"peerDependencies": {
"react": ">=18"
},
"dependencies": {
"@alice-ui/shared-utils": "workspace:*"
},
"devDependencies": {
"clean-package": "2.2.0"
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json"
}
52 changes: 52 additions & 0 deletions packages/utilities/react-utils/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';

export interface CreateContextOptions {
/**
* If `true`, React will throw if context is `null` or `undefined`
* In some cases, you might want to support nested context, so you can set it to `false`
*/
strict?: boolean;
/**
* Error message to throw if the context is `undefined`
*/
errorMessage?: string;
/**
* The display name of the context
*/
name?: string;
}

export type CreateContextReturn<T> = [React.Provider<T>, () => T, React.Context<T>];

/**
* Creates a named context, provider, and hook.
*
* @param options create context options
*/
export function createContext<ContextType>(options: CreateContextOptions = {}) {
const {
strict = true,
errorMessage = 'useContext: `context` is undefined. Seems you forgot to wrap component within the Provider',
name,
} = options;

const Context = React.createContext<ContextType | undefined>(undefined);

Context.displayName = name;

function useContext() {
const context = React.useContext(Context);

if (!context && strict) {
const error = new Error(errorMessage);

error.name = 'ContextError';
// Error.captureStackTrace?.(error, useContext);
throw error;
}

return context;
}

return [Context.Provider, useContext, Context] as CreateContextReturn<ContextType>;
}
143 changes: 143 additions & 0 deletions packages/utilities/react-utils/src/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
MutableRefObject,
Ref,
RefObject,
useImperativeHandle,
useLayoutEffect,
useRef,
} from 'react';

export function canUseDOM(): boolean {
return !!(typeof window !== 'undefined' && window.document && window.document.createElement);
}

export const isBrowser = canUseDOM();

export function getUserAgentBrowser(navigator: Navigator) {
const { userAgent: ua, vendor } = navigator;
const android = /(android)/i.test(ua);

switch (true) {
case /CriOS/.test(ua):
return 'Chrome for iOS';
case /Edg\//.test(ua):
return 'Edge';
case android && /Silk\//.test(ua):
return 'Silk';
case /Chrome/.test(ua) && /Google Inc/.test(vendor):
return 'Chrome';
case /Firefox\/\d+\.\d+$/.test(ua):
return 'Firefox';
case android:
return 'AOSP';
case /MSIE|Trident/.test(ua):
return 'IE';
case /Safari/.test(navigator.userAgent) && /Apple Computer/.test(ua):
return 'Safari';
case /AppleWebKit/.test(ua):
return 'WebKit';
default:
return null;
}
}

export type UserAgentBrowser = NonNullable<ReturnType<typeof getUserAgentBrowser>>;

export function getUserAgentOS(navigator: Navigator) {
const { userAgent: ua, platform } = navigator;

switch (true) {
case /Android/.test(ua):
return 'Android';
case /iPhone|iPad|iPod/.test(platform):
return 'iOS';
case /Win/.test(platform):
return 'Windows';
case /Mac/.test(platform):
return 'Mac';
case /CrOS/.test(ua):
return 'Chrome OS';
case /Firefox/.test(ua):
return 'Firefox OS';
default:
return null;
}
}

export type UserAgentOS = NonNullable<ReturnType<typeof getUserAgentOS>>;

export function detectDeviceType(navigator: Navigator) {
const { userAgent: ua } = navigator;

if (/(tablet)|(iPad)|(Nexus 9)/i.test(ua)) return 'tablet';
if (/(mobi)/i.test(ua)) return 'phone';

return 'desktop';
}

export type UserAgentDeviceType = NonNullable<ReturnType<typeof detectDeviceType>>;

export function detectOS(os: UserAgentOS) {
if (!isBrowser) return false;

return getUserAgentOS(window.navigator) === os;
}

export function detectBrowser(browser: UserAgentBrowser) {
if (!isBrowser) return false;

return getUserAgentBrowser(window.navigator) === browser;
}

export function detectTouch() {
if (!isBrowser) return false;

return window.ontouchstart === null && window.ontouchmove === null && window.ontouchend === null;
}

export function useDOMRef<T extends HTMLElement = HTMLElement>(
ref?: RefObject<T | null> | Ref<T | null>,
) {
const domRef = useRef<T>(null);

useImperativeHandle(ref, () => domRef.current);

return domRef;
}

export interface ContextValue<T> {
ref?: MutableRefObject<T>;
}

// Syncs ref from context with ref passed to hook
export function useSyncRef<T>(context: ContextValue<T | null>, ref: RefObject<T>) {
useLayoutEffect(() => {
if (context && context.ref && ref && ref.current) {
context.ref.current = ref.current;

return () => {
if (context.ref?.current) {
context.ref.current = null;
}
};
}
}, [context, ref]);
}

/**
* Checks if two DOMRect objects intersect each other.
*
* @param rect1 - The first DOMRect object.
* @param rect2 - The second DOMRect object.
* @returns A boolean indicating whether the two DOMRect objects intersect.
*/
export function areRectsIntersecting(rect1: DOMRect, rect2: DOMRect) {
return (
rect1 &&
rect2 &&
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}
3 changes: 3 additions & 0 deletions packages/utilities/react-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './children';
export * from './context';
export * from './dom';
export * from './refs';
export * from './types';
40 changes: 40 additions & 0 deletions packages/utilities/react-utils/src/refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isFunction } from '@alice-ui/shared-utils';
import * as React from 'react';
import { MutableRefObject } from 'react';

export type ReactRef<T> = React.RefObject<T> | React.MutableRefObject<T> | React.Ref<T>;

/**
* Assigns a value to a ref function or object
*
* @param ref the ref to assign to
* @param value the value
*/
export function assignRef<T = any>(ref: ReactRef<T> | undefined, value: T) {
if (ref == null) return;

if (isFunction(ref)) {
ref(value);

return;
}

try {
(ref as MutableRefObject<T>).current = value;
} catch (error) {
throw new Error(`Cannot assign value '${value}' to ref '${ref}'`);
}
}

/**
* Combine multiple React refs into a single ref function.
* This is used mostly when you need to allow consumers forward refs to
* internal components
*
* @param refs refs to assign to value to
*/
export function mergeRefs<T>(...refs: (ReactRef<T> | undefined)[]) {
return (node: T | null) => {
refs.forEach((ref) => assignRef(ref, node));
};
}
Loading

0 comments on commit 0f0a781

Please sign in to comment.