Skip to content

Commit

Permalink
Merge pull request #2 from chenz24/feat/hooks
Browse files Browse the repository at this point in the history
Feat/hooks
  • Loading branch information
chenz24 authored Aug 27, 2021
2 parents 727cdb3 + 43c796b commit 8a16a7a
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 0 deletions.
21 changes: 21 additions & 0 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@kubed/hooks",
"version": "0.0.1",
"main": "cjs/index.js",
"module": "esm/index.js",
"browser": "lib/index.umd.js",
"types": "lib/index.d.ts",
"license": "MIT",
"sideEffects": false,
"homepage": "https://github.com/kubesphere/kube-design",
"repository": {
"url": "https://github.com/kubesphere/kube-design.git",
"type": "git",
"directory": "packages/hooks"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"dependencies": {},
"devDependencies": {}
}
9 changes: 9 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { useId } from './useId';
export { useToggle, useBooleanToggle } from './useToggle';
export { useUncontrolled } from './useUncontrolled';
export { useMediaQuery } from './useMediaQuery';
export { useReducedMotion } from './useReducedMotion';
export { useWindowEvent } from './useWindowEvent';
export { useLocalStorage } from './useLocalStorage';
export { useForceUpdate } from './useForceUpdate';
export { useClipboard } from './useClipboard';
32 changes: 32 additions & 0 deletions packages/hooks/src/useClipboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react';

export function useClipboard({ timeout = 2000 } = {}) {
const [error, setError] = useState<Error>(null);
const [copied, setCopied] = useState(false);
const [copyTimeout, setCopyTimeout] = useState(null);

const handleCopyResult = (value: boolean) => {
clearTimeout(copyTimeout);
setCopyTimeout(setTimeout(() => setCopied(false), timeout));
setCopied(value);
};

const copy = (valueToCopy: any) => {
if ('clipboard' in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult(true))
.catch((err) => setError(err));
} else {
setError(new Error('useClipboard: navigator.clipboard is not supported'));
}
};

const reset = () => {
setCopied(false);
setError(null);
clearTimeout(copyTimeout);
};

return { copy, reset, error, copied };
}
9 changes: 9 additions & 0 deletions packages/hooks/src/useDidMount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useEffect } from 'react';

export function useDidMount(callback: () => any): void {
useEffect(() => {
if (typeof callback === 'function') {
callback();
}
}, []);
}
8 changes: 8 additions & 0 deletions packages/hooks/src/useForceUpdate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useReducer } from 'react';

const reducer = (value: number) => (value + 1) % 1000000;

export function useForceUpdate(): () => void {
const [, update] = useReducer(reducer, 0);
return update;
}
7 changes: 7 additions & 0 deletions packages/hooks/src/useId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useRef } from 'react';
import { randomId } from '../utils';

export function useId(id?: string, generateId: () => string = randomId) {
const generatedId = useRef(generateId());
return id || generatedId.current;
}
47 changes: 47 additions & 0 deletions packages/hooks/src/useLocalStorage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// https://github.com/mantinedev/mantine/blob/master/src/mantine-hooks/src/use-local-storage-value/use-local-storage-value.ts
import { useState, useCallback, useEffect } from 'react';
import { useWindowEvent } from '../useWindowEvent';

export function useLocalStorage<T extends string>({
key,
defaultValue = undefined,
}: {
key: string;
defaultValue?: T;
}) {
const [value, setValue] = useState<T>(
typeof window !== 'undefined' && 'localStorage' in window
? (window.localStorage.getItem(key) as T)
: ((defaultValue ?? '') as T)
);

const setLocalStorageValue = useCallback(
(val: T | ((prevState: T) => T)) => {
if (typeof val === 'function') {
setValue((current) => {
const result = val(current);
window.localStorage.setItem(key, result);
return result;
});
} else {
window.localStorage.setItem(key, val);
setValue(val);
}
},
[key]
);

useWindowEvent('storage', (event) => {
if (event.storageArea === window.localStorage && event.key === key) {
setValue(event.newValue as T);
}
});

useEffect(() => {
if (defaultValue && !value) {
setLocalStorageValue(defaultValue);
}
}, [defaultValue, value, setLocalStorageValue]);

return [value, setLocalStorageValue] as const;
}
40 changes: 40 additions & 0 deletions packages/hooks/src/useMediaQuery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState, useEffect, useRef } from 'react';

type MediaQueryCallback = (event: { matches: boolean; media: string }) => void;

/**
* Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia
* https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
* */
function attachMediaListener(query: MediaQueryList, callback: MediaQueryCallback) {
try {
query.addEventListener('change', callback);
return () => query.removeEventListener('change', callback);
} catch (e) {
query.addListener(callback);
return () => query.removeListener(callback);
}
}

function getInitialValue(query: string) {
if (typeof window !== 'undefined' && 'matchMedia' in window) {
return window.matchMedia(query).matches;
}
return false;
}

export function useMediaQuery(query: string) {
const [matches, setMatches] = useState(getInitialValue(query));
const queryRef = useRef<MediaQueryList>();

// eslint-disable-next-line consistent-return
useEffect(() => {
if ('matchMedia' in window) {
queryRef.current = window.matchMedia(query);
setMatches(queryRef.current.matches);
return attachMediaListener(queryRef.current, (event) => setMatches(event.matches));
}
}, [query]);

return matches;
}
5 changes: 5 additions & 0 deletions packages/hooks/src/useReducedMotion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useMediaQuery } from '../useMediaQuery';

export function useReducedMotion() {
return useMediaQuery('(prefers-reduced-motion: reduce)');
}
26 changes: 26 additions & 0 deletions packages/hooks/src/useToggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// https://github.com/mantinedev/mantine/blob/master/src/mantine-hooks/src/use-toggle/use-toggle.ts
import React, { useState } from 'react';

export function useToggle<T>(initialValue: T, options: [T, T]) {
const [state, setState] = useState(initialValue);

const toggle = (value?: React.SetStateAction<T>) => {
if (typeof value !== 'undefined') {
setState(value);
} else {
setState((current) => {
if (current === options[0]) {
return options[1];
}

return options[0];
});
}
};

return [state, toggle] as const;
}

export function useBooleanToggle(initialValue = false) {
return useToggle(initialValue, [true, false]);
}
69 changes: 69 additions & 0 deletions packages/hooks/src/useUncontrolled/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// https://github.com/mantinedev/mantine/blob/master/src/mantine-hooks/src/use-uncontrolled/use-uncontrolled.ts
import { useEffect, useRef, useState } from 'react';

export type UncontrolledMode = 'initial' | 'controlled' | 'uncontrolled';

export interface UncontrolledOptions<T> {
value: T | null | undefined;
defaultValue: T | null | undefined;
finalValue: T | null;
onChange(value: T | null): void;
onValueUpdate?(value: T | null): void;
rule: (value: T | null | undefined) => boolean;
}

export function useUncontrolled<T>({
value,
defaultValue,
finalValue,
rule,
onChange,
onValueUpdate,
}: UncontrolledOptions<T>): readonly [T | null, (nextValue: T | null) => void, UncontrolledMode] {
// determine, whether new props indicate controlled state
const shouldBeControlled = rule(value);

// initialize state
const modeRef = useRef<UncontrolledMode>('initial');
const initialValue = rule(defaultValue) ? defaultValue : finalValue;
const [uncontrolledValue, setUncontrolledValue] = useState(initialValue);

// compute effective value
let effectiveValue = shouldBeControlled ? value : uncontrolledValue;

if (!shouldBeControlled && modeRef.current === 'controlled') {
// We are transitioning from controlled to uncontrolled
// this transition is special as it happens when clearing out
// the input using "invalid" value (typically null or undefined).
//
// Since the value is invalid, doing nothing would mean just
// transitioning to uncontrolled state and using whatever value
// it currently holds which is likely not the bavior
// user expects, so lets change the state to finalValue.
//
// The value will be propagated to internal state by useEffect below.

effectiveValue = finalValue;
}
modeRef.current = shouldBeControlled ? 'controlled' : 'uncontrolled';
const mode = modeRef.current;

const handleChange = (nextValue: T | null) => {
typeof onChange === 'function' && onChange(nextValue);

// Controlled input only triggers onChange event and expects
// the controller to propagate new value back.
if (mode === 'uncontrolled') {
setUncontrolledValue(nextValue);
}
};

useEffect(() => {
if (mode === 'uncontrolled') {
setUncontrolledValue(effectiveValue);
}
typeof onValueUpdate === 'function' && onValueUpdate(effectiveValue);
}, [mode, effectiveValue]);

return [effectiveValue, handleChange, modeRef.current] as const;
}
12 changes: 12 additions & 0 deletions packages/hooks/src/useWindowEvent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useEffect } from 'react';

export function useWindowEvent<K extends keyof WindowEventMap>(
type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
) {
useEffect(() => {
window.addEventListener(type, listener, options);
return () => window.removeEventListener(type, listener, options);
}, []);
}
3 changes: 3 additions & 0 deletions packages/hooks/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function randomId() {
return `kubed-${Math.random().toString(36).substr(2, 9)}`;
}
13 changes: 13 additions & 0 deletions packages/hooks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"include": ["./src",],
"compilerOptions": {
"rootDir": "src",
"baseUrl": ".",
"outDir": "lib",
"declaration": true,
"declarationMap": true,
"declarationDir": "lib",
"composite": true
}
}

0 comments on commit 8a16a7a

Please sign in to comment.