ESLINT is holy crap
This commit is contained in:
37
src/primitive-components/badge/badge.tsx
Normal file
37
src/primitive-components/badge/badge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { BadgeProps } from './badges.types';
|
||||
import { bool, number, string } from 'prop-types';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
const Badge = forwardRef<SVGSVGElement, BadgeProps>(function Badge(
|
||||
{ disableValue = false, ...props },
|
||||
ref,
|
||||
) {
|
||||
const digitLength = props.children
|
||||
? 16 + (props.children.length - 1) * 6
|
||||
: 6,
|
||||
disableValueClassName =
|
||||
disableValue || (!props.children ?? true) ? 'disable-value' : '';
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
className={`m3 m3-badge ${'' ?? props.className}${disableValueClassName}`.trimEnd()}
|
||||
ref={ref}
|
||||
width={`${digitLength}px`}
|
||||
>
|
||||
{props.children && (
|
||||
<text x={'50%'} y={'50%'}>
|
||||
{props.children}
|
||||
</text>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
Badge.propTypes = {
|
||||
children: number,
|
||||
className: string,
|
||||
disableValue: bool,
|
||||
};
|
||||
|
||||
export { Badge };
|
||||
5
src/primitive-components/badge/badges.types.ts
Normal file
5
src/primitive-components/badge/badges.types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface BadgeProps extends PropsWithChildren<any> {
|
||||
disableValue?: boolean;
|
||||
}
|
||||
48
src/primitive-components/button-layout/button-layout.tsx
Normal file
48
src/primitive-components/button-layout/button-layout.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { RippleArea } from '../ripple/ripple-area';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import useRippleEffect from '../ripple/hooks/useRippleEffect';
|
||||
import React, {
|
||||
forwardRef,
|
||||
PropsWithChildren,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
const ButtonLayout = forwardRef<
|
||||
HTMLButtonElement,
|
||||
PropsWithChildren<any> & IRippleProps
|
||||
>(function ButtonBase({ centralRipple = false, ...props }, ref) {
|
||||
const [isActive, setIsActive] = useState<boolean>(false),
|
||||
ripplesRef = useRef(null),
|
||||
buttonId = useId(),
|
||||
events = useRippleEffect(ripplesRef, setIsActive);
|
||||
|
||||
const { variant, disabled, className } = props;
|
||||
|
||||
const classes = className
|
||||
? `m3 ${className} ${variant}${isActive ? ' is-active' : ''}`
|
||||
: `m3 ${variant}${isActive ? ' is-active' : ''}`;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
{...events}
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
id={buttonId}
|
||||
ref={ref}
|
||||
>
|
||||
{props.children}
|
||||
<RippleArea
|
||||
callback={setIsActive}
|
||||
central={centralRipple}
|
||||
ref={ripplesRef}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export { ButtonLayout };
|
||||
25
src/primitive-components/button-layout/button.types.ts
Normal file
25
src/primitive-components/button-layout/button.types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
type ToggleButtonType = {
|
||||
selected: string;
|
||||
unselected: string;
|
||||
};
|
||||
|
||||
export interface ButtonMainProps extends PropsWithChildren<any> {
|
||||
disabled?: boolean;
|
||||
variant?: 'filled' | 'outlined' | 'elevated' | 'tonal' | 'text';
|
||||
}
|
||||
|
||||
export interface FABMainProps extends PropsWithChildren<any> {
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
size?: 'small' | 'default' | 'large' | 'extended';
|
||||
variant?: 'surface' | 'primary' | 'secondary' | 'tertiary';
|
||||
}
|
||||
|
||||
export interface IconButtonMainProps extends PropsWithChildren<any> {
|
||||
icon: string;
|
||||
toggled?: false | ToggleButtonType;
|
||||
disabled?: boolean;
|
||||
variant?: 'default' | 'filled' | 'tonal' | 'outlined';
|
||||
}
|
||||
33
src/primitive-components/button/button.tsx
Normal file
33
src/primitive-components/button/button.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon } from '../material-you-components';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import { ButtonLayout } from '../button-layout/button-layout';
|
||||
import { ButtonMainProps } from '../button-layout/button.types';
|
||||
|
||||
/**
|
||||
* Button component
|
||||
** description
|
||||
*/
|
||||
|
||||
export const Button = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonMainProps & IRippleProps
|
||||
>(
|
||||
(
|
||||
{ centralRipple = false, variant, disabled = false, icon, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<ButtonLayout
|
||||
{...props}
|
||||
centralRipple={centralRipple}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
variant={variant ? variant : 'filled'}
|
||||
>
|
||||
{icon ? <Icon iconSize={20}>{icon}</Icon> : <></>}
|
||||
<span className={'label-large'}>{props.children}</span>
|
||||
</ButtonLayout>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { CheckboxLayoutProps } from './checkbox-layout.types';
|
||||
|
||||
export const CheckBoxLayout = forwardRef(function CheckBoxBase(
|
||||
{ indeterminate, typeInput, type, ...props }: CheckboxLayoutProps,
|
||||
ref,
|
||||
): JSX.Element {
|
||||
const checkboxRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkboxRef.current.indeterminate = indeterminate === true;
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => checkboxRef.current);
|
||||
|
||||
const classesType = typeInput || type;
|
||||
|
||||
const classes =
|
||||
props.className !== undefined
|
||||
? `m3 m3-${type} ${props.className}`
|
||||
: `m3 m3-${classesType}`;
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={checkboxRef}
|
||||
{...props}
|
||||
className={classes.trimEnd()}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface CheckboxLayoutProps extends PropsWithChildren<any> {
|
||||
indeterminate?: boolean;
|
||||
typeInput?: string;
|
||||
type?: string;
|
||||
}
|
||||
59
src/primitive-components/checkbox/checkbox.tsx
Normal file
59
src/primitive-components/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
'use client';
|
||||
|
||||
import { RippleArea } from '../ripple/ripple-area';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import useRippleEffect from '../ripple/hooks/useRippleEffect';
|
||||
import { CheckBoxLayout } from '../checkbox-layout/check-box-layout';
|
||||
import {
|
||||
forwardRef,
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
/**
|
||||
* Checkbox component
|
||||
** description
|
||||
*/
|
||||
|
||||
export const Checkbox = forwardRef<
|
||||
HTMLInputElement,
|
||||
PropsWithChildren<any> & IRippleProps
|
||||
>(({ centralRipple, ...props }, ref) => {
|
||||
const [isActive, setIsActive] = useState<boolean>(false),
|
||||
[checked, setChecked] = useState<boolean>(props.checked ?? false),
|
||||
ripplesRef = useRef(null),
|
||||
checkboxRef = useRef(null),
|
||||
events = useRippleEffect(ripplesRef, setIsActive);
|
||||
|
||||
const classes =
|
||||
`m3 m3-checkbox-label ${isActive === true ? 'visible' : ''}`.trimEnd();
|
||||
const indeterminate = (props.indeterminate === true).toString();
|
||||
|
||||
useImperativeHandle(ref, () => checkboxRef.current);
|
||||
|
||||
useEffect(() => {
|
||||
setChecked(!checked);
|
||||
}, [checkboxRef.current?.checked]);
|
||||
|
||||
return (
|
||||
<label {...events} className={classes}>
|
||||
<CheckBoxLayout
|
||||
{...props}
|
||||
indeterminate={indeterminate}
|
||||
ref={checkboxRef}
|
||||
type={'checkbox'}
|
||||
/>
|
||||
<span className={'m3 m3-checkbox-state-layer'} />
|
||||
<RippleArea
|
||||
callback={setIsActive}
|
||||
central={centralRipple}
|
||||
className={'m3-checkbox-ripple-layer'}
|
||||
ref={ripplesRef}
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
});
|
||||
18
src/primitive-components/divider/divider.tsx
Normal file
18
src/primitive-components/divider/divider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { forwardRef, PropsWithChildren } from 'react';
|
||||
|
||||
interface DividerProps extends PropsWithChildren<any> {
|
||||
orientation?: 'vertical' | 'horizontal';
|
||||
variant?: 'full-width' | 'inset' | 'middle-inset';
|
||||
}
|
||||
|
||||
const Divider = forwardRef<HTMLHRElement, DividerProps>(
|
||||
({ orientation, variant, ...props }, ref) => (
|
||||
<hr
|
||||
{...props}
|
||||
className={`m3 m3-divider ${orientation ?? 'horizontal'} ${variant ?? 'full-width'}`.trimEnd()}
|
||||
ref={ref}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
export { Divider };
|
||||
52
src/primitive-components/fab/fab.tsx
Normal file
52
src/primitive-components/fab/fab.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon } from '../material-you-components';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import { FABMainProps } from '../button-layout/button.types';
|
||||
import { ButtonLayout } from '../button-layout/button-layout';
|
||||
|
||||
/**
|
||||
* FABs component
|
||||
** description
|
||||
*/
|
||||
|
||||
const sizes = {
|
||||
small: 24,
|
||||
default: 24,
|
||||
large: 36,
|
||||
extended: 24,
|
||||
};
|
||||
|
||||
export const FAB = forwardRef<HTMLButtonElement, FABMainProps & IRippleProps>(
|
||||
(
|
||||
{
|
||||
variant,
|
||||
disabled,
|
||||
icon,
|
||||
centralRipple = false,
|
||||
size = 'default',
|
||||
elevated,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<ButtonLayout
|
||||
{...props}
|
||||
centralRipple={centralRipple}
|
||||
className={`m3-fab m3-${size}-fab ${!(elevated ?? false) && 'without-elevation'}`}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
variant={variant ? variant : 'surface'}
|
||||
>
|
||||
<Icon iconSize={sizes[size]} svgSize={sizes[size]}>
|
||||
{icon}
|
||||
</Icon>
|
||||
{size === 'extended' ? (
|
||||
<span className={'label-large'}>{props.children}</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ButtonLayout>
|
||||
),
|
||||
);
|
||||
86
src/primitive-components/icon-button/icon-button.tsx
Normal file
86
src/primitive-components/icon-button/icon-button.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '../material-you-components';
|
||||
import { toggleIconType } from './icon-button.types';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import { ButtonLayout } from '../button-layout/button-layout';
|
||||
import { IconButtonMainProps } from '../button-layout/button.types';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
/**
|
||||
* Icon button-layout component
|
||||
** description
|
||||
*/
|
||||
|
||||
export const IconButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
IconButtonMainProps & IRippleProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
variant,
|
||||
disabled,
|
||||
selected = false,
|
||||
toggled = false,
|
||||
centralRipple,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [toggleIcon, setToggleIcon] = useState<toggleIconType>({
|
||||
state: selected == true ? 'selected' : 'unselected',
|
||||
icon: toggled ? toggled.unselected ?? 'add_circle' : 'add_circle',
|
||||
});
|
||||
|
||||
const toggle = (classes: string, icon: string) => {
|
||||
setToggleIcon(() => ({
|
||||
state: classes,
|
||||
icon: icon,
|
||||
}));
|
||||
};
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const callback = useCallback(() => {
|
||||
if (toggled) {
|
||||
if (toggleIcon.state === 'selected') {
|
||||
toggle('', toggled.unselected ?? 'add_circle');
|
||||
} else {
|
||||
toggle('selected', toggled.selected ?? 'add_circle');
|
||||
}
|
||||
}
|
||||
if (props.onClick) {
|
||||
props.onClick();
|
||||
}
|
||||
}, [toggleIcon]);
|
||||
|
||||
useImperativeHandle(ref, () => buttonRef.current);
|
||||
|
||||
return (
|
||||
<ButtonLayout
|
||||
{...props}
|
||||
centralRipple={centralRipple}
|
||||
className={`m3-icon-button ${toggleIcon.state} ${toggled ? 'toggled' : ''}`.trimEnd()}
|
||||
disabled={disabled}
|
||||
onClick={callback}
|
||||
ref={buttonRef}
|
||||
variant={variant ? variant : 'default'}
|
||||
>
|
||||
<Icon
|
||||
fill={toggleIcon.state === 'selected' ? 1 : 0}
|
||||
iconSize={28}
|
||||
svgSize={40}
|
||||
>
|
||||
{toggled ? toggleIcon.icon : icon ? icon : undefined}
|
||||
</Icon>
|
||||
</ButtonLayout>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export type toggleIconType = {
|
||||
state: string;
|
||||
icon: string;
|
||||
};
|
||||
53
src/primitive-components/icon/icon.tsx
Normal file
53
src/primitive-components/icon/icon.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { IconProps } from './icon.types';
|
||||
import { bool, number, string } from 'prop-types';
|
||||
import React, { ForwardedRef, forwardRef } from 'react';
|
||||
|
||||
const Icon = forwardRef(function Icon(
|
||||
{
|
||||
grade = 0,
|
||||
weight = 500,
|
||||
svgSize = 20,
|
||||
fill = false,
|
||||
iconSize = 20,
|
||||
opticalSize = 24,
|
||||
type = 'outlined',
|
||||
...props
|
||||
}: IconProps,
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const fontVariation = {
|
||||
fontVariationSettings: `'FILL' ${fill ? 1 : 0}, 'wght' ${weight}, 'GRAD' ${grade}, 'optz' ${opticalSize}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
className={`m3 m3-svg-icon ${props.className ?? ''}`.trim()}
|
||||
height={svgSize}
|
||||
ref={ref}
|
||||
width={svgSize}
|
||||
>
|
||||
<text
|
||||
className={`m3-${type[0].toUpperCase() + type.slice(1)} m3-size-${iconSize}px`}
|
||||
style={fontVariation}
|
||||
x={'50%'}
|
||||
y={'50%'}
|
||||
>
|
||||
{props.children ?? 'add_circle'}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
|
||||
Icon.propTypes = {
|
||||
fill: bool,
|
||||
type: string,
|
||||
grade: number,
|
||||
weight: number,
|
||||
svgSize: number,
|
||||
iconSize: number,
|
||||
children: string,
|
||||
opticalSize: number,
|
||||
};
|
||||
|
||||
export { Icon };
|
||||
11
src/primitive-components/icon/icon.types.ts
Normal file
11
src/primitive-components/icon/icon.types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface IconProps extends PropsWithChildren<any> {
|
||||
fill?: boolean;
|
||||
grade?: number;
|
||||
svgSize?: number;
|
||||
iconSize?: number;
|
||||
opticalSize?: number;
|
||||
type?: 'outlined' | 'rounded' | 'sharp';
|
||||
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700;
|
||||
}
|
||||
13
src/primitive-components/material-you-components.tsx
Normal file
13
src/primitive-components/material-you-components.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export { FAB } from './fab/fab';
|
||||
export { Icon } from './icon/icon';
|
||||
export { Radio } from './radio/radio';
|
||||
export { Badge } from './badge/badge';
|
||||
export { Switch } from './switch/switch';
|
||||
export { Button } from './button/button';
|
||||
export { Divider } from './divider/divider';
|
||||
export { Checkbox } from './checkbox/checkbox';
|
||||
export { RippleArea } from './ripple/ripple-area';
|
||||
export { Ripples, Ripple } from './ripple/ripple';
|
||||
export { TextField } from './text-field/text-field';
|
||||
export { IconButton } from './icon-button/icon-button';
|
||||
export { ButtonLayout } from './button-layout/button-layout';
|
||||
38
src/primitive-components/radio/radio.tsx
Normal file
38
src/primitive-components/radio/radio.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import { RippleArea } from '../ripple/ripple-area';
|
||||
import { IRippleProps } from '../ripple/ripple.types';
|
||||
import useRippleEffect from '../ripple/hooks/useRippleEffect';
|
||||
import { CheckBoxLayout } from '../checkbox-layout/check-box-layout';
|
||||
import { forwardRef, PropsWithChildren, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Radio component
|
||||
** description
|
||||
*/
|
||||
|
||||
export const Radio = forwardRef<
|
||||
HTMLInputElement,
|
||||
PropsWithChildren<HTMLElement> & IRippleProps
|
||||
>(({ centralRipple, ...props }, ref) => {
|
||||
const [isActive, setIsActive] = useState<boolean>(false),
|
||||
ripplesRef = useRef(null),
|
||||
events = useRippleEffect(ripplesRef, setIsActive);
|
||||
|
||||
const classes =
|
||||
`m3 m3-radio-label ${isActive === true ? 'visible' : ''}`.trimEnd();
|
||||
|
||||
return (
|
||||
<label {...events} className={classes}>
|
||||
<CheckBoxLayout {...props} ref={ref} type={'radio'} />
|
||||
<span className={'m3 m3-radio-state-layer'} />
|
||||
<RippleArea
|
||||
callback={setIsActive}
|
||||
central={centralRipple}
|
||||
className={'m3-checkbox-ripple-layer'}
|
||||
ref={ripplesRef}
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
});
|
||||
43
src/primitive-components/ripple/hooks/useRippleEffect.ts
Normal file
43
src/primitive-components/ripple/hooks/useRippleEffect.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface RippleEventHandlers {
|
||||
onBlur: React.FocusEventHandler;
|
||||
onContextMenu: React.MouseEventHandler;
|
||||
onDragLeave: React.DragEventHandler;
|
||||
onMouseDown: React.MouseEventHandler;
|
||||
onMouseLeave: React.MouseEventHandler;
|
||||
onMouseUp: React.MouseEventHandler;
|
||||
onTouchEnd: React.TouchEventHandler;
|
||||
onTouchMove: React.TouchEventHandler;
|
||||
onTouchStart: React.TouchEventHandler;
|
||||
}
|
||||
|
||||
const UseRippleEffect = (ref, callback): undefined | RippleEventHandlers => {
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) {
|
||||
setMounted(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, stop } = ref.current;
|
||||
|
||||
return {
|
||||
onBlur: event => stop(event, callback),
|
||||
onContextMenu: event => start(event, callback),
|
||||
onDragLeave: event => stop(event, callback),
|
||||
onMouseDown: event => start(event, callback),
|
||||
onMouseLeave: event => stop(event, callback),
|
||||
onMouseUp: event => stop(event, callback),
|
||||
onTouchEnd: event => stop(event, callback),
|
||||
onTouchMove: event => stop(event, callback),
|
||||
onTouchStart: event => stop(event, callback),
|
||||
};
|
||||
};
|
||||
|
||||
export default UseRippleEffect;
|
||||
130
src/primitive-components/ripple/ripple-area.tsx
Normal file
130
src/primitive-components/ripple/ripple-area.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
'use client';
|
||||
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Ripple } from './ripple';
|
||||
import { Ripples } from './ripple';
|
||||
import { RippleAreaProps } from './ripple.types';
|
||||
|
||||
const TIMEOUT: number = 550;
|
||||
const rippleAreaContext = React.createContext(false);
|
||||
|
||||
const RippleArea = forwardRef(function RippleArea(
|
||||
{ central = false, callback, ...props }: RippleAreaProps,
|
||||
ref,
|
||||
) {
|
||||
const [ripples, setRipples] = useState<Array<JSX.Element>>([]),
|
||||
rippleDomain = useRef<any>(null),
|
||||
clicked = useRef<boolean>(false),
|
||||
uniqueKey = useRef<number>(0),
|
||||
uniqueId = useId();
|
||||
|
||||
const classes = props.className
|
||||
? `m3 m3-ripple-domain ${props.className}`.trimEnd()
|
||||
: 'm3 m3-ripple-domain';
|
||||
|
||||
const start = useCallback(
|
||||
(event: any, 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<JSX.Element>) => {
|
||||
if (prevRipples.length === 0) {
|
||||
return [
|
||||
<Ripple
|
||||
key={uniqueKey.current}
|
||||
lifetime={TIMEOUT}
|
||||
rippleS={rippleS}
|
||||
rippleX={rippleX}
|
||||
rippleY={rippleY}
|
||||
/>,
|
||||
];
|
||||
}
|
||||
const old = [...prevRipples];
|
||||
old.push(
|
||||
<Ripple
|
||||
key={uniqueKey.current}
|
||||
lifetime={TIMEOUT}
|
||||
rippleS={rippleS}
|
||||
rippleX={rippleX}
|
||||
rippleY={rippleY}
|
||||
/>,
|
||||
);
|
||||
return old;
|
||||
});
|
||||
|
||||
uniqueKey.current += 1;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const stop = useCallback((_event: any, cb: (state: boolean) => void) => {
|
||||
clicked.current = false;
|
||||
cb(clicked.current);
|
||||
|
||||
setRipples((prevRipples: Array<JSX.Element>) => {
|
||||
if (prevRipples.length > 0) {
|
||||
const old = [...prevRipples];
|
||||
old.shift();
|
||||
return old;
|
||||
}
|
||||
return prevRipples;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
start,
|
||||
stop,
|
||||
}),
|
||||
[start, stop],
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={classes} id={uniqueId} ref={rippleDomain}>
|
||||
<rippleAreaContext.Provider value={clicked.current}>
|
||||
<Ripples>{ripples}</Ripples>
|
||||
</rippleAreaContext.Provider>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
export { rippleAreaContext, RippleArea };
|
||||
86
src/primitive-components/ripple/ripple.tsx
Normal file
86
src/primitive-components/ripple/ripple.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import isEmpty from './utils/utils';
|
||||
import { rippleProps } from './ripple.types';
|
||||
import { rippleAreaContext } from './ripple-area';
|
||||
import RippleEffectBuild from './utils/ripple-effect-builder';
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
JSX,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
|
||||
const Ripples = forwardRef(function Ripples(
|
||||
props: any,
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const [ripples, setRipples] = useState({});
|
||||
const firstRender = useRef<boolean>(true);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const LifetimeEnd = useCallback((child: JSX.Element) => {
|
||||
if (child.props.endLifetime) {
|
||||
child.props.endLifetime();
|
||||
}
|
||||
|
||||
setRipples(state => {
|
||||
const children = { ...state };
|
||||
delete children[child.key];
|
||||
return children;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.children.length > 0) {
|
||||
startTransition(() => {
|
||||
if (firstRender.current || isEmpty(ripples)) {
|
||||
setRipples(RippleEffectBuild(props.children, LifetimeEnd));
|
||||
firstRender.current = false;
|
||||
} else {
|
||||
setRipples(
|
||||
RippleEffectBuild(props.children, LifetimeEnd, ripples),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [props.children]);
|
||||
|
||||
return <>{Object.values(ripples)}</>;
|
||||
});
|
||||
|
||||
const Ripple = forwardRef(function Ripple(
|
||||
props: rippleProps,
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const { rippleX, rippleY, rippleS, endLifetime, lifetime } = props;
|
||||
|
||||
const clicked = useContext<boolean>(rippleAreaContext);
|
||||
const [classes, setClasses] = useState<string>('m3 ripple visible');
|
||||
|
||||
useEffect(() => {
|
||||
if (endLifetime !== null && !clicked) {
|
||||
setClasses('m3 ripple');
|
||||
setTimeout(endLifetime, lifetime);
|
||||
}
|
||||
}, [clicked, endLifetime]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classes}
|
||||
style={{
|
||||
left: -(rippleS / 2) + rippleX,
|
||||
top: -(rippleS / 2) + rippleY,
|
||||
width: rippleS,
|
||||
aspectRatio: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export { Ripple, Ripples };
|
||||
21
src/primitive-components/ripple/ripple.types.ts
Normal file
21
src/primitive-components/ripple/ripple.types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
export interface IRippleProps extends PropsWithChildren<any> {
|
||||
centralRipple?: boolean;
|
||||
}
|
||||
|
||||
export interface RippleAreaProps extends PropsWithChildren<any> {
|
||||
callback: Dispatch<SetStateAction<boolean>>;
|
||||
central?: boolean;
|
||||
}
|
||||
|
||||
export type rippleProps = {
|
||||
rippleX: number;
|
||||
rippleY: number;
|
||||
rippleS: number;
|
||||
endLifetime?: () => void;
|
||||
lifetime: number;
|
||||
key?: number;
|
||||
};
|
||||
|
||||
import { PropsWithChildren } from 'react';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cloneElement, ReactElement } from 'react';
|
||||
|
||||
export default function ArrayConvertToObj(
|
||||
obj: Object,
|
||||
nextChildren: ReactElement[],
|
||||
callback: (child: any) => void,
|
||||
): void {
|
||||
Object.values(nextChildren).forEach(
|
||||
(child: JSX.Element) =>
|
||||
(obj[child.key] = cloneElement(child, {
|
||||
...child.props,
|
||||
endLifetime: callback.bind(null, child),
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import ArrayConvertToObj from './array-convert-to-obj';
|
||||
import { cloneElement, ReactElement } from 'react';
|
||||
import isEmpty from './utils';
|
||||
|
||||
export default function RippleEffectBuild(
|
||||
nextRipples: ReactElement[],
|
||||
callback: (child: any) => void,
|
||||
prevRipples?: any | 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;
|
||||
}
|
||||
6
src/primitive-components/ripple/utils/utils.ts
Normal file
6
src/primitive-components/ripple/utils/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function isEmpty(obj: Object): boolean {
|
||||
for (const _i in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
36
src/primitive-components/switch/switch.tsx
Normal file
36
src/primitive-components/switch/switch.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef } from 'react';
|
||||
import { SwitchMainProps } from './switch.types';
|
||||
import { CheckBoxLayout } from '../checkbox-layout/check-box-layout';
|
||||
|
||||
/**
|
||||
* Switch component
|
||||
** description
|
||||
*/
|
||||
|
||||
export const Switch = forwardRef<HTMLInputElement, SwitchMainProps>(
|
||||
({ icon, disabled, selected = false, ...props }, ref) => (
|
||||
<div className={'m3 m3-switch-exp'} ref={ref}>
|
||||
<CheckBoxLayout
|
||||
{...props}
|
||||
className={`m3 ${props.className ?? ''}`.trimEnd()}
|
||||
disabled={disabled}
|
||||
type={'checkbox'}
|
||||
/>
|
||||
<svg>
|
||||
<rect className={'m3 m3-switch-track'} />
|
||||
<circle className={'m3 m3-switch-handler'} />
|
||||
<circle className={'m3 m3-switch-handler-state-layer'} />
|
||||
<g>
|
||||
{icon && !selected && (
|
||||
<text className={'m3 m3-icon-unchecked'}>close</text>
|
||||
)}
|
||||
{icon && (
|
||||
<text className={'m3 m3-icon-checked'}>check</text>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
7
src/primitive-components/switch/switch.types.ts
Normal file
7
src/primitive-components/switch/switch.types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface SwitchMainProps extends PropsWithChildren<any> {
|
||||
disabled?: boolean;
|
||||
icon?: boolean;
|
||||
selected?: boolean;
|
||||
}
|
||||
103
src/primitive-components/text-field/text-field.tsx
Normal file
103
src/primitive-components/text-field/text-field.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import React, { forwardRef, useState } from 'react';
|
||||
import { bool, string } from 'prop-types';
|
||||
import { type TextFieldInterface } from './text-field.types';
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldInterface>(
|
||||
(
|
||||
{
|
||||
variant = 'filled',
|
||||
withAfterIcon,
|
||||
withBeforeIcon,
|
||||
supportingText,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [raised, setRaised] = useState<boolean>(
|
||||
props.placeholder ?? false,
|
||||
);
|
||||
|
||||
const callback = (e: any): void => {
|
||||
if (
|
||||
e.type === 'blur' &&
|
||||
e.target.value.length === 0 &&
|
||||
!props.placeholder
|
||||
) {
|
||||
setRaised(false);
|
||||
} else if (e.type === 'focus') {
|
||||
setRaised(true);
|
||||
}
|
||||
};
|
||||
|
||||
const iconStyles =
|
||||
withBeforeIcon && withAfterIcon
|
||||
? 'with-before-icon with-after-icon'
|
||||
: withBeforeIcon
|
||||
? 'with-before-icon'
|
||||
: withAfterIcon
|
||||
? 'with-after-icon'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div className={`m3 m3-text-field ${variant}`.trimEnd()}>
|
||||
{variant === 'outlined' && (
|
||||
<fieldset>
|
||||
<legend className={raised && 'raised'}>
|
||||
<span>Label</span>
|
||||
</legend>
|
||||
</fieldset>
|
||||
)}
|
||||
{withBeforeIcon && (
|
||||
<span className={'m3-icon icon-before'}>
|
||||
{withBeforeIcon && 'search'}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={`${props.className ?? ''} ${iconStyles}`.trim()}
|
||||
onBlur={event => {
|
||||
callback(event);
|
||||
if (props.onBlur) {
|
||||
props.onBlur(event);
|
||||
}
|
||||
}}
|
||||
onFocus={event => {
|
||||
callback(event);
|
||||
if (props.onFocus) {
|
||||
props.onFocus(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label className={raised ? 'raised' : ''}>
|
||||
{props.children ?? 'Label'}
|
||||
</label>
|
||||
<span className={'m3-text-field-state-layer'} />
|
||||
{withAfterIcon && (
|
||||
<span className={'m3-icon'}>
|
||||
{withAfterIcon && 'cancel'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{supportingText !== '' && (
|
||||
<span className={'m3-text-field-supporting-text'}>
|
||||
{supportingText}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
TextField.propTypes = {
|
||||
children: string,
|
||||
withBeforeIcon: bool,
|
||||
withAfterIcon: bool,
|
||||
className: string,
|
||||
variant: string,
|
||||
placeholder: string,
|
||||
supportingText: string,
|
||||
};
|
||||
8
src/primitive-components/text-field/text-field.types.ts
Normal file
8
src/primitive-components/text-field/text-field.types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
export interface TextFieldInterface extends PropsWithChildren<any> {
|
||||
variant: 'filled' | 'outlined';
|
||||
withAfterIcon?: boolean;
|
||||
withBeforeIcon?: boolean;
|
||||
supportingText?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user