diff --git a/.eslintrc.js b/.eslintrc.js index 77bfc4e..e2138aa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { plugins: ['react'], rules: { // Possible errors - 'no-console': 'warn', + 'no-console': 'off', // Best practices 'dot-notation': 'error', 'no-else-return': 'error', @@ -36,9 +36,10 @@ module.exports = { // Stylistic 'array-bracket-spacing': 'error', 'computed-property-spacing': ['error', 'never'], + '@typescript-eslint/no-unused-vars': 'warn', curly: 'error', 'no-lonely-if': 'error', - 'no-unneeded-ternary': 'error', + 'no-unneeded-ternary': 'warn', 'one-var-declaration-per-line': 'error', quotes: [ 'error', diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e6591d4..f21e1f1 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/README.md b/README.md index db9f067..c18e587 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This repository is including and will be including components, enumerates in tab - [X] Switches - [ ] Chips - [x] Icon -- [x] Ripples Effect +- [x] Ripple Effect - [ ] Dividers (WIP) - [x] Badges - [ ] Select field diff --git a/src/primitive-components/button-components/button-layout/button-layout.tsx b/src/primitive-components/button-components/button-layout/button-layout.tsx index cb4253b..5d976df 100644 --- a/src/primitive-components/button-components/button-layout/button-layout.tsx +++ b/src/primitive-components/button-components/button-layout/button-layout.tsx @@ -1,7 +1,7 @@ 'use client'; import { bool, string } from 'prop-types'; -import { RippleArea } from '../../ripple/ripple-area'; +import { RippleEffect } from '../../ripple/ripple-effect'; import { ButtonLayoutProps } from './button-layout.types'; import useRippleEffect from '../../ripple/hooks/useRippleEffect'; import React, { forwardRef, useId, useRef, useState } from 'react'; @@ -26,7 +26,7 @@ export const ButtonLayout = forwardRef( ref={ref} > {props.children} - ( {ripples && ( - ( /> - ( cy={'50%'} /> - , + rippleDomainRef: MutableRefObject, + central: boolean, + forwardedRef: ForwardedRef, +) => { + const [ripples, setRipples] = useState({}); + const [pending, transition] = useTransition(); + const debounced = useRef(false); + const uniqueKey = useRef(0); + + const endLifetimeRipple = (keyRipple: number) => { + if (!pending && !isEmpty(ripples)) { + transition(() => { + setRipples(prevRipples => { + delete prevRipples[keyRipple]; + return prevRipples; + }); + }); + } + }; + + const createRipple = useCallback( + ( + event: InteractionEventsType, + changeStateCallback: (state: boolean) => void, + ): void => { + if (stateRef.current || debounced.current) { + return; + } + + debounced.current = true; + + setTimeout(() => { + debounced.current = false; + }, DEBOUNCE); + + stateRef.current = true; + changeStateCallback(stateRef.current); + + const rippleDomain: DOMRect | baseDOMRect = rippleDomainRef.current + ? rippleDomainRef.current.getBoundingClientRect() + : { + width: 0, + height: 0, + left: 0, + top: 0, + }; + + const [rippleX, rippleY, rippleS]: Array = + rippleSizeCalculator(event, rippleDomain, central); + + const prevRipples = ripples; + + prevRipples[uniqueKey.current] = ( + + ); + + setRipples(prevRipples); + + uniqueKey.current += 1; + }, + [uniqueKey, debounced], + ); + + const removeRipple = ( + event: InteractionEventsType, + changeStateCallback: (state: boolean) => void, + ) => { + if (!stateRef.current) { + return; + } + stateRef.current = false; + changeStateCallback(stateRef.current); + }; + + useImperativeHandle( + forwardedRef, + () => ({ + start: createRipple, + stop: removeRipple, + current: rippleDomainRef, + }), + [createRipple, removeRipple], + ); + + return ripples; +}; + +export default UseRippleBuilder; diff --git a/src/primitive-components/ripple/ripple-area.tsx b/src/primitive-components/ripple/ripple-area.tsx deleted file mode 100644 index 08da78e..0000000 --- a/src/primitive-components/ripple/ripple-area.tsx +++ /dev/null @@ -1,131 +0,0 @@ -'use client'; - -import React, { - forwardRef, - ReactElement, - useId, - useImperativeHandle, - useRef, - useState, -} from 'react'; -import { Ripples, Ripple } from './ripples'; -import { RippleAreaProps } from './ripple.types'; -import { InteractionEventsType } from './hooks/useRippleEffect'; - -const TIMEOUT: number = 550; -const rippleAreaContext = React.createContext(false); - -const RippleArea = forwardRef( - ({ central = false, ...props }: RippleAreaProps, ref) => { - const [ripples, setRipples] = useState>([]), - rippleDomain = useRef(null), - clicked = useRef(false), - uniqueKey = useRef(0), - uniqueId = useId(); - - const extraClassStyles = - `m3 m3-ripple-domain ${props.className ?? ''}`.trimEnd(); - - const createRipple = ( - event: InteractionEventsType, - cb: (state: boolean) => void, - ): void => { - clicked.current = true; - cb(clicked.current); - - const rippleDomainChar = rippleDomain.current - ? rippleDomain.current.getBoundingClientRect() - : { - width: 0, - height: 0, - left: 0, - top: 0, - }; - - const rippleX: number = !central - ? event.clientX - rippleDomainChar.left - : rippleDomainChar.width / 2, - rippleY: number = !central - ? event.clientY - rippleDomainChar.top - : rippleDomainChar.height / 2, - rippleSizeX: number = - Math.max( - Math.abs(rippleDomainChar.width - rippleX), - rippleX, - ) * - 2 + - 2, - rippleSizeY: number = - Math.max( - Math.abs(rippleDomainChar.height - rippleY), - rippleY, - ) * - 2 + - 2, - rippleS: number = (rippleSizeX ** 2 + rippleSizeY ** 2) ** 0.5; - - setRipples((prevRipples: Array) => { - if (prevRipples.length === 0) { - return [ - , - ]; - } - const old = [...prevRipples]; - old.push( - , - ); - return old; - }); - - uniqueKey.current += 1; - }; - - const removeRipple = ( - _event: InteractionEventsType, - cb: (state: boolean) => void, - ) => { - clicked.current = false; - cb(clicked.current); - - setRipples((prevRipples: Array) => { - if (prevRipples.length > 0) { - const old = [...prevRipples]; - old.shift(); - return old; - } - return prevRipples; - }); - }; - - useImperativeHandle( - ref, - () => ({ - start: createRipple, - stop: removeRipple, - }), - [createRipple, removeRipple], - ); - - return ( - - - {ripples} - - - ); - }, -); - -export { rippleAreaContext, RippleArea }; diff --git a/src/primitive-components/ripple/ripple-effect.tsx b/src/primitive-components/ripple/ripple-effect.tsx new file mode 100644 index 0000000..ab134c4 --- /dev/null +++ b/src/primitive-components/ripple/ripple-effect.tsx @@ -0,0 +1,30 @@ +'use client'; + +import useRippleBuilder from './hooks/useRippleBuilder'; +import React, { forwardRef, useId, useRef } from 'react'; +import { RippleAreaProps, RippleContainer } from './ripple.types'; + +const rippleAreaContext = React.createContext(false); + +const RippleEffect = forwardRef( + ({ central = false, ...props }, ref) => { + const uniqueId = useId(), + rippleDomain = useRef(null), + clicked = useRef(false), + ripples = useRippleBuilder(clicked, rippleDomain, central, ref); + + return ( + + + {Object.values(ripples)} + + + ); + }, +); + +export { rippleAreaContext, RippleEffect }; diff --git a/src/primitive-components/ripple/ripple.tsx b/src/primitive-components/ripple/ripple.tsx new file mode 100644 index 0000000..f886872 --- /dev/null +++ b/src/primitive-components/ripple/ripple.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { RippleProps } from './ripple.types'; +import { rippleAreaContext } from './ripple-effect'; +import React, { useContext, useLayoutEffect, useState } from 'react'; + +const Ripple = ({ + rippleX, + rippleY, + rippleS, + lifetime, + rippleKey, + endLifetime, +}: RippleProps) => { + const [classes, setClasses] = useState('m3 ripple visible'); + const rippleDomainContext = useContext(rippleAreaContext); + + useLayoutEffect(() => { + if (endLifetime !== null && !rippleDomainContext) { + setClasses('m3 ripple'); + setTimeout(() => endLifetime(rippleKey), lifetime); + } + }, [rippleDomainContext]); + + return ( + + ); +}; + +export { Ripple }; diff --git a/src/primitive-components/ripple/ripple.types.ts b/src/primitive-components/ripple/ripple.types.ts index 8fa9b6e..c27d50d 100644 --- a/src/primitive-components/ripple/ripple.types.ts +++ b/src/primitive-components/ripple/ripple.types.ts @@ -1,7 +1,28 @@ -import { Dispatch, HTMLAttributes, ReactElement, SetStateAction } from 'react'; +import { + Dispatch, + HTMLAttributes, + MutableRefObject, + SetStateAction, +} from 'react'; +import { InteractionEventsType } from './hooks/useRippleEffect'; -export interface RipplesProps extends HTMLAttributes { - children?: ReactElement[]; +export type baseDOMRect = { + width: number; + height: number; + left: number; + top: number; +}; + +export interface RippleContainer { + start: ( + event: InteractionEventsType, + changeStateCallback: (state: boolean) => void, + ) => void; + stop: ( + event: InteractionEventsType, + changeStateCallback: (state: boolean) => void, + ) => void; + current: MutableRefObject; } export interface RipplePropsForComponents extends HTMLAttributes { @@ -14,10 +35,10 @@ export interface RippleAreaProps extends HTMLAttributes { } export interface RippleProps extends HTMLAttributes { + rippleKey: number; rippleX: number; rippleY: number; rippleS: number; - endLifetime?: () => void; lifetime: number; - key?: number; + endLifetime?: (value: number) => void; } diff --git a/src/primitive-components/ripple/ripples.tsx b/src/primitive-components/ripple/ripples.tsx deleted file mode 100644 index 9cf81b9..0000000 --- a/src/primitive-components/ripple/ripples.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client'; - -import isEmpty from './utils/utils'; -import { rippleAreaContext } from './ripple-area'; -import { RippleProps, RipplesProps } from './ripple.types'; -import RippleEffectBuild from './utils/ripple-effect-builder'; -import React, { - ReactElement, - useContext, - useEffect, - useRef, - useState, - useTransition, -} from 'react'; - -const Ripples = (props: RipplesProps) => { - const [ripples, setRipples] = useState({}); - const firstRender = useRef(true); - const [pending, startTransition] = useTransition(); - - const endLifetime = (child: ReactElement) => { - if (child.props.endLifetime) { - child.props.endLifetime(); - } - - setRipples(state => { - const children = { ...state }; - delete children[child.key]; - return children; - }); - }; - - useEffect(() => { - if (props.children.length > 0 && !pending) { - startTransition(() => { - if (firstRender.current || isEmpty(ripples)) { - setRipples(RippleEffectBuild(props.children, endLifetime)); - firstRender.current = false; - } else { - setRipples( - RippleEffectBuild(props.children, endLifetime, ripples), - ); - } - }); - } - }, [props.children]); - - return <>{Object.values(ripples)}; -}; - -const Ripple = ({ - rippleX, - rippleY, - rippleS, - endLifetime, - lifetime, -}: RippleProps) => { - const clicked = useContext(rippleAreaContext); - const [classes, setClasses] = useState('m3 ripple visible'); - - useEffect(() => { - if (endLifetime !== null && !clicked) { - setClasses('m3 ripple'); - setTimeout(endLifetime, lifetime); - } - }, [clicked, endLifetime]); - - return ( - - ); -}; - -export { Ripples, Ripple }; diff --git a/src/primitive-components/ripple/utils/array-convert-to-obj.ts b/src/primitive-components/ripple/utils/array-convert-to-obj.ts deleted file mode 100644 index c1166ee..0000000 --- a/src/primitive-components/ripple/utils/array-convert-to-obj.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { cloneElement, ReactElement } from 'react'; - -export default function ArrayConvertToObj( - obj: object, - nextChildren: ReactElement[], - callback: (child: ReactElement) => void, -): void { - Object.values(nextChildren).forEach( - (child: ReactElement) => - (obj[child.key] = cloneElement(child, { - ...child.props, - endLifetime: callback.bind(null, child), - })), - ); -} diff --git a/src/primitive-components/ripple/utils/ripple-effect-builder.ts b/src/primitive-components/ripple/utils/ripple-effect-builder.ts deleted file mode 100644 index 0e9655c..0000000 --- a/src/primitive-components/ripple/utils/ripple-effect-builder.ts +++ /dev/null @@ -1,34 +0,0 @@ -import isEmpty from './utils'; -import ArrayConvertToObj from './array-convert-to-obj'; -import { cloneElement, ReactElement } from 'react'; - -export default function RippleEffectBuild( - nextRipples: ReactElement[], - callback: (child: ReactElement) => void, - prevRipples?: object | null, -) { - const empty: boolean = isEmpty(prevRipples); - const preparedRipples: object = empty ? {} : prevRipples; - - switch (empty) { - case true: - ArrayConvertToObj(preparedRipples, nextRipples, callback); - break; - - case false: - // eslint-disable-next-line no-case-declarations - const next: object = {}; - ArrayConvertToObj(next, nextRipples, callback); - for (const rippleKey of Object.keys(next)) { - if (preparedRipples[rippleKey] == undefined) { - preparedRipples[rippleKey] = cloneElement(next[rippleKey], { - ...next[rippleKey].props, - endLifetime: callback.bind(null, next[rippleKey]), - }); - } - } - break; - } - - return preparedRipples; -} diff --git a/src/primitive-components/ripple/utils/utils.ts b/src/primitive-components/ripple/utils/utils.ts index 7d663b9..e9f2a7f 100644 --- a/src/primitive-components/ripple/utils/utils.ts +++ b/src/primitive-components/ripple/utils/utils.ts @@ -1,6 +1,31 @@ +import { InteractionEventsType } from '../hooks/useRippleEffect'; +import { baseDOMRect } from '../ripple.types'; + export default function isEmpty(obj: object): boolean { for (const _i in obj) { return false; } return true; } + +export function maxSize(size: number, cordPos: number): number { + return Math.max(Math.abs(size - cordPos), cordPos); +} + +export function rippleSizeCalculator( + event: InteractionEventsType, + rippleDomain: DOMRect | baseDOMRect, + centralRipple: boolean, +): Array { + const rippleX: number = !centralRipple + ? event.clientX - rippleDomain.left + : rippleDomain.width / 2, + rippleY: number = !centralRipple + ? event.clientY - rippleDomain.top + : rippleDomain.height / 2, + rippleSizeX: number = maxSize(rippleDomain.width, rippleX) * 2, + rippleSizeY: number = maxSize(rippleDomain.height, rippleY) * 2, + rippleS: number = (rippleSizeX ** 2 + rippleSizeY ** 2) ** 0.5; + + return [rippleX, rippleY, rippleS]; +}