diff --git a/docs/src/docs/core/Indicator.mdx b/docs/src/docs/core/Indicator.mdx index 79f42dec926..c048a300dd7 100644 --- a/docs/src/docs/core/Indicator.mdx +++ b/docs/src/docs/core/Indicator.mdx @@ -27,6 +27,24 @@ element to keep `display: block`. +## OverflowCount + +Set `overflowCount` to handle overflow cases: + + + +## Processing + +Set `processing` to indicate that it is processing: + + + +## ShowZero + +Set `showZero` to display 0: + + + ## Offset Set `offset` to change indicator position. It is useful when Indicator component is diff --git a/src/mantine-core/src/Indicator/Indicator.story.tsx b/src/mantine-core/src/Indicator/Indicator.story.tsx index 61a07f94b6a..352e2ac539b 100644 --- a/src/mantine-core/src/Indicator/Indicator.story.tsx +++ b/src/mantine-core/src/Indicator/Indicator.story.tsx @@ -12,7 +12,7 @@ const placements = ['start', 'center', 'end'] as const; export const Positions = () => { const items = positions.map((position) => { const _items = placements.map((placement) => ( - + )); @@ -25,7 +25,7 @@ export const Positions = () => { export const Inline = () => ( - + @@ -33,7 +33,7 @@ export const Inline = () => ( export const WithRadius = () => ( - + + keyframes({ + from: { + boxShadow: `0 0 0.5px 0 ${color}`, + opacity: 0.6, + }, + to: { + boxShadow: `0 0 0.5px 4.4px ${color}`, + opacity: 0, + }, + }); + function getPositionStyles(_position: IndicatorPosition, offset = 0) { const styles: CSSObject = {}; const [position, placement] = _position.split('-'); @@ -68,35 +86,54 @@ export default createStyles( withLabel, zIndex, }: IndicatorStylesParams - ) => ({ - root: { - position: 'relative', - display: inline ? 'inline-block' : 'block', - }, + ) => { + const { background } = theme.fn.variant({ + variant: 'filled', + primaryFallback: false, + color: color || theme.primaryColor, + }); + return { + root: { + position: 'relative', + display: inline ? 'inline-block' : 'block', + }, - indicator: { - ...getPositionStyles(position, offset), - zIndex, - position: 'absolute', - [withLabel ? 'minWidth' : 'width']: size, - height: size, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - fontSize: theme.fontSizes.xs, - paddingLeft: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0, - paddingRight: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0, - borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }), - backgroundColor: theme.fn.variant({ - variant: 'filled', - primaryFallback: false, - color: color || theme.primaryColor, - }).background, - border: withBorder - ? `2px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white}` - : undefined, - color: theme.white, - whiteSpace: 'nowrap', - }, - }) + indicator: { + ...getPositionStyles(position, offset), + zIndex, + position: 'absolute', + [withLabel ? 'minWidth' : 'width']: size, + height: size, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: theme.fontSizes.xs, + paddingLeft: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0, + paddingRight: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0, + borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }), + backgroundColor: theme.fn.variant({ + variant: 'filled', + primaryFallback: false, + color: color || theme.primaryColor, + }).background, + border: withBorder + ? `2px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white}` + : undefined, + color: theme.white, + whiteSpace: 'nowrap', + }, + + processing: { + animation: `${processingAnimation(background)} 1000ms linear infinite`, + }, + + common: { + ...getPositionStyles(position, offset), + position: 'absolute', + [withLabel ? 'minWidth' : 'width']: size, + height: size, + borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }), + }, + }; + } ); diff --git a/src/mantine-core/src/Indicator/Indicator.tsx b/src/mantine-core/src/Indicator/Indicator.tsx index c4f629d48cc..b186afeafd8 100644 --- a/src/mantine-core/src/Indicator/Indicator.tsx +++ b/src/mantine-core/src/Indicator/Indicator.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-unused-prop-types */ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import { Selectors, DefaultProps, @@ -8,9 +8,11 @@ import { useComponentDefaultProps, getDefaultZIndex, } from '@mantine/styles'; +import { isNumber, isUnDef } from '@mantine/utils'; import { Box } from '../Box'; import { IndicatorPosition } from './Indicator.types'; import useStyles, { IndicatorStylesParams } from './Indicator.styles'; +import { Machine } from './Machine/Machine'; export type IndicatorStylesNames = Selectors; @@ -35,6 +37,11 @@ export interface IndicatorProps /** Indicator label */ label?: React.ReactNode; + /** Indicator count overflowCount */ + overflowCount?: number; + + dot?: boolean; + /** border-radius from theme.radius or number value to set radius in px */ radius?: MantineNumberSize; @@ -47,6 +54,12 @@ export interface IndicatorProps /** When component is disabled it renders children without indicator */ disabled?: boolean; + /** When showZero is true and label is zero renders children with indicator*/ + showZero?: boolean; + + /** Indicator processing animation */ + processing?: boolean; + /** Indicator z-index */ zIndex?: React.CSSProperties['zIndex']; } @@ -57,7 +70,10 @@ const defaultProps: Partial = { inline: false, withBorder: false, disabled: false, + showZero: false, + processing: false, size: 10, + overflowCount: 99, radius: 1000, zIndex: getDefaultZIndex('app'), }; @@ -73,12 +89,16 @@ export const Indicator = forwardRef((props, ref) withBorder, className, color, + dot, styles, label, + overflowCount, + showZero, classNames, disabled, zIndex, unstyled, + processing, ...others } = useComponentDefaultProps('Indicator', defaultProps, props); @@ -87,9 +107,24 @@ export const Indicator = forwardRef((props, ref) { name: 'Indicator', classNames, styles, unstyled } ); + const renderLabel = useMemo(() => { + if (isNumber(label)) { + return ; + } + return label; + }, [label, overflowCount]); + + const isShowIndicator = useMemo( + () => !disabled && (dot || (!isUnDef(label) && !(label <= 0 && !showZero))), + [disabled, label, showZero] + ); + return ( - {!disabled &&
{label}
} + {isShowIndicator && ( +
{renderLabel}
+ )} + {processing &&
} {children} ); diff --git a/src/mantine-core/src/Indicator/Machine/Machine.styles.ts b/src/mantine-core/src/Indicator/Machine/Machine.styles.ts new file mode 100644 index 00000000000..bb3c08a8436 --- /dev/null +++ b/src/mantine-core/src/Indicator/Machine/Machine.styles.ts @@ -0,0 +1,9 @@ +import { createStyles } from '@mantine/styles'; + +export default createStyles(() => ({ + base: { + display: 'flex', + alignItems: 'center', + overflow: 'hidden', + }, +})); diff --git a/src/mantine-core/src/Indicator/Machine/Machine.tsx b/src/mantine-core/src/Indicator/Machine/Machine.tsx new file mode 100644 index 00000000000..5ab63d8014f --- /dev/null +++ b/src/mantine-core/src/Indicator/Machine/Machine.tsx @@ -0,0 +1,63 @@ +import React, { useState, forwardRef, useMemo, useEffect } from 'react'; +import { isString, isDef, usePrevious } from '@mantine/utils'; +import { MachineNumber } from './MachineNumber'; +import useStyles from './Machine.styles'; + +interface MachineNumberProps { + value: number | string; + max: number; +} + +export const Machine = forwardRef(({ value = 0, max }, ref) => { + const [oldValue, setOldValue] = useState(); + const [newValue, setNewValue] = useState(); + const prevValueRef = usePrevious(value); + + useEffect(() => { + if (isString(value)) { + setOldValue(undefined); + setNewValue(undefined); + } else if (isString(prevValueRef)) { + setOldValue(undefined); + setNewValue(value); + } else { + setOldValue(prevValueRef); + setNewValue(value); + } + }, [value, prevValueRef]); + + const numbers = useMemo(() => { + if (isString(value)) return []; + if (value < 1) return [0]; + const result: number[] = []; + let currentValue = value; + if (isDef(max)) { + currentValue = Math.min(max, currentValue); + } + while (currentValue >= 1) { + result.push(currentValue % 10); + currentValue /= 10; + currentValue = Math.floor(currentValue); + } + result.reverse(); + return result; + }, [value, max]); + + const { classes } = useStyles(null, { name: 'machine' }); + + return isString(value) ? ( + {value} + ) : ( + + {numbers.map((number, i) => ( + + ))} + {isDef(max) && value > max && +} + + ); +}); diff --git a/src/mantine-core/src/Indicator/Machine/MachineNumber.styles.ts b/src/mantine-core/src/Indicator/Machine/MachineNumber.styles.ts new file mode 100644 index 00000000000..90752c9c670 --- /dev/null +++ b/src/mantine-core/src/Indicator/Machine/MachineNumber.styles.ts @@ -0,0 +1,99 @@ +import { createStyles, keyframes } from '@mantine/styles'; + +const currentScrollDownKeyframes = keyframes({ + from: { + transform: 'translateY(-60%)', + opacity: 0, + }, + to: { + transform: 'translateY(0%)', + opacity: 1, + }, +}); + +const currentScrollUpKeyframes = keyframes({ + from: { + transform: 'translateY(60%)', + opacity: 0, + }, + to: { + transform: 'translateY(0%)', + opacity: 1, + }, +}); + +const oldNumberScrollUpKeyframes = keyframes({ + from: { + transform: 'translateY(0%)', + opacity: 1, + }, + to: { + transform: 'translateY(-60%)', + opacity: 0, + }, +}); + +const oldNumberScrollDownKeyframes = keyframes({ + from: { + transform: 'translateY(0%)', + opacity: 1, + }, + to: { + transform: 'translateY(60%)', + opacity: 0, + }, +}); + +export default createStyles(() => ({ + baseNumber: { + height: 18, + width: '0.6em', + maxWidth: '0.6em', + position: 'relative', + display: 'inline-block', + }, + oldNumberTop: { + transform: 'translateY(-100%);', + }, + oldNumberBottom: { + transform: 'translateY(100%);', + }, + oldNumber: { + display: 'inline-block', + opacity: 0, + position: 'absolute', + left: 0, + right: 0, + }, + currentNumberTop: { + transform: 'translateY(0%);', + }, + currentNumber: { + display: 'inline-block', + opacity: 1, + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + + currentNumberScrollDown: { + animation: `${currentScrollDownKeyframes} .2s cubic-bezier(0,0,.2, 1)`, + animationIterationCount: 1, + }, + + currentNumberScrollUp: { + animation: `${currentScrollUpKeyframes} .2s cubic-bezier(0,0,.2, 1)`, + animationIterationCount: 1, + }, + + oldNumberScrollUp: { + animation: `${oldNumberScrollUpKeyframes} .2s cubic-bezier(0,0,.2, 1)`, + animationIterationCount: 1, + }, + oldNumberScrollDown: { + animation: `${oldNumberScrollDownKeyframes} .2s cubic-bezier(0,0,.2, 1)`, + animationIterationCount: 1, + }, +})); diff --git a/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx b/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx new file mode 100644 index 00000000000..a8087985175 --- /dev/null +++ b/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx @@ -0,0 +1,87 @@ +import React, { useState, forwardRef, useEffect, useMemo } from 'react'; +import { isUnDef, usePrevious } from '@mantine/utils'; +import useStyles from './MachineNumber.styles'; + +interface MachineNumberProps { + value: number | string; + newOriginalNumber: number; + oldOriginalNumber: number; +} + +export const MachineNumber = forwardRef((props, ref) => { + const [oldNumber, setOldNumber] = useState(props.value); + const [newNumber, setNewNumber] = useState(props.value); + const [scrollAnimationDirection, setScrollAnimationDirection] = useState<'up' | 'down'>('up'); + const [isActive, setIsActive] = useState(false); + const prevValueRef = usePrevious(props.value); + + const scrollByDir = (dir: 'up' | 'down') => { + setIsActive(true); + setScrollAnimationDirection(dir); + setTimeout(() => { + setIsActive(false); + }, 180); + }; + + const scroll = () => { + const { newOriginalNumber, oldOriginalNumber } = props; + if (isUnDef(newOriginalNumber) || isUnDef(oldOriginalNumber)) return; + if (newOriginalNumber > oldOriginalNumber) { + scrollByDir('up'); + } else if (newOriginalNumber < oldOriginalNumber) { + scrollByDir('down'); + } + }; + + useEffect(() => { + setOldNumber(prevValueRef); + setNewNumber(props.value); + scroll(); + }, [props.value, prevValueRef]); + + const { classes, cx } = useStyles(null, { name: 'MachineNumber' }); + + const newNumberScrollAnimationClass = useMemo( + () => + isActive + ? scrollAnimationDirection === 'up' + ? classes.currentNumberScrollUp + : classes.currentNumberScrollDown + : null, + [isActive, scrollAnimationDirection] + ); + const oldNumberScrollAnimationClass = useMemo( + () => + isActive + ? scrollAnimationDirection === 'up' + ? classes.oldNumberScrollUp + : classes.oldNumberScrollDown + : null, + [isActive, scrollAnimationDirection] + ); + return ( + + {(oldNumber && ( + + {oldNumber} + + )) || + null} + + + {newNumber} + + + {(oldNumber && ( + + {oldNumber} + + )) || + null} + + ); +}); diff --git a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.configurator.tsx b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.configurator.tsx index 3229f8808d9..b7da0e06aba 100644 --- a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.configurator.tsx +++ b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.configurator.tsx @@ -20,7 +20,7 @@ import { Indicator, Avatar, Group } from '@mantine/core'; function Demo() { return ( - + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + + + + ); +} +`; + +function Demo() { + return ( + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + + + + ); +} + +export const count: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.offset.tsx b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.offset.tsx index 531b6de0d0f..ee5ad0edd2a 100644 --- a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.offset.tsx +++ b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.offset.tsx @@ -6,7 +6,7 @@ import { Avatar, Indicator } from '@mantine/core'; function Demo() { return ( - + + ({ + display: 'flex', + gap: 48, + alignItems: 'center', + })} + > + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + + + + + + + + + + ); +} +`; + +function Demo() { + const [demoValue, setDemoValue] = useState(9); + return ( + ({ + display: 'flex', + gap: 48, + alignItems: 'center', + })} + > + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + + + + + + + + + + ); +} + +export const overflowCount: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.processing.tsx b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.processing.tsx new file mode 100644 index 00000000000..8258ace01a1 --- /dev/null +++ b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.processing.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Avatar, Indicator } from '@mantine/core'; + +const code = ` +import { Avatar, Indicator } from '@mantine/core'; + +function Demo() { + return ( + + + + ); +} +`; + +function Demo() { + return ( + + + + ); +} + +export const processing: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.showZero.tsx b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.showZero.tsx new file mode 100644 index 00000000000..6d823e49569 --- /dev/null +++ b/src/mantine-demos/src/demos/core/Indicator/Indicator.demo.showZero.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Avatar, Indicator, Box } from '@mantine/core'; + +const code = ` +import { Avatar, Indicator, Box } from '@mantine/core'; + +function Demo() { + return ( + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + ); +} +`; + +function Demo() { + return ( + ({ + display: 'flex', + gap: 48, + })} + > + + + + + + + + ); +} + +export const showZero: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-demos/src/demos/core/Indicator/index.ts b/src/mantine-demos/src/demos/core/Indicator/index.ts index dfa460360ac..4eeb9f1277c 100644 --- a/src/mantine-demos/src/demos/core/Indicator/index.ts +++ b/src/mantine-demos/src/demos/core/Indicator/index.ts @@ -1,3 +1,7 @@ export { configurator } from './Indicator.demo.configurator'; export { inline } from './Indicator.demo.inline'; export { offset } from './Indicator.demo.offset'; +export { count } from './Indicator.demo.count'; +export { processing } from './Indicator.demo.processing'; +export { overflowCount } from './Indicator.demo.overflowCount'; +export { showZero } from './Indicator.demo.showZero'; diff --git a/src/mantine-styles-api/src/styles-api/Indicator.styles-api.ts b/src/mantine-styles-api/src/styles-api/Indicator.styles-api.ts index 91fd3a41f18..05dbf312438 100644 --- a/src/mantine-styles-api/src/styles-api/Indicator.styles-api.ts +++ b/src/mantine-styles-api/src/styles-api/Indicator.styles-api.ts @@ -2,5 +2,7 @@ import type { IndicatorStylesNames } from '@mantine/core'; export const Indicator: Record = { root: 'Root element', + common: 'Indicator Common', indicator: 'Indicator badge', + processing: 'Indicator Processing', }; diff --git a/src/mantine-utils/src/index.ts b/src/mantine-utils/src/index.ts index c9750ae65ad..e3c6667d08a 100644 --- a/src/mantine-utils/src/index.ts +++ b/src/mantine-utils/src/index.ts @@ -5,7 +5,6 @@ export { findElementAncestor } from './find-element-ancestor/find-element-ancest export { createSafeContext } from './create-safe-context/create-safe-context'; export { packSx } from './pack-sx/pack-sx'; export { getSafeId } from './get-safe-id/get-safe-id'; -export { isElement } from './is-element/is-element'; export { closeOnEscape } from './close-on-escape/close-on-escape'; export { createEventHandler } from './create-event-handler/create-event-handler'; export { noop } from './noop/noop'; @@ -13,6 +12,27 @@ export { keys } from './keys/keys'; export { useHovered } from './use-hovered/use-hovered'; export { groupOptions, getGroupedOptions } from './group-options/group-options'; export { createUseExternalEvents } from './create-use-external-events/create-use-external-events'; +export { + isArray, + isBoolean, + isString, + isDef, + isUnDef, + isDate, + isEmpty, + isNull, + isMap, + isNumber, + isFunction, + isObject, + isPromise, + isNullAndUnDef, + isNullOrUnDef, + isRegExp, + isWindow, + isElement, +} from './is/is'; +export { default as usePrevious } from './use-previous/usePrevious'; export type { PolymorphicComponentProps } from './create-polymorphic-component/create-polymorphic-component'; export type { ForwardRefWithStaticComponents } from './ForwardRefWithStaticComponents'; diff --git a/src/mantine-utils/src/is-element/is-element.test.tsx b/src/mantine-utils/src/is-element/is-element.test.tsx deleted file mode 100644 index 11ecf257b64..00000000000 --- a/src/mantine-utils/src/is-element/is-element.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { forwardRef } from 'react'; -import { isElement } from './is-element'; - -const TestComponent = () =>
; -const TextRefComponent = forwardRef((_props, ref) =>
); - -describe('@mantine/utils/is-element', () => { - it('correctly detects elements', () => { - expect(isElement(
Element
)).toBe(true); - expect(isElement()).toBe(true); - expect(isElement()).toBe(true); - }); - - it('correctly detects jsx parts that are not elements', () => { - expect(isElement(<>Element)).toBe(false); - expect(isElement('string')).toBe(false); - expect(isElement(2)).toBe(false); - expect(isElement(null)).toBe(false); - expect(isElement(undefined)).toBe(false); - expect(isElement(true)).toBe(false); - expect(isElement([
Element
])).toBe(false); - }); -}); diff --git a/src/mantine-utils/src/is-element/is-element.ts b/src/mantine-utils/src/is-element/is-element.ts deleted file mode 100644 index 60fced8a2f1..00000000000 --- a/src/mantine-utils/src/is-element/is-element.ts +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -/** - * Detects if provided argument is a React element: - * fragments, nulls, strings and numbers are not considered to be an element - * */ -export function isElement(value: any): value is React.ReactElement { - if (Array.isArray(value) || value === null) { - return false; - } - - if (typeof value === 'object') { - if (value.type === React.Fragment) { - return false; - } - - return true; - } - - return false; -} diff --git a/src/mantine-utils/src/is/is.test.tsx b/src/mantine-utils/src/is/is.test.tsx new file mode 100644 index 00000000000..9a4252f7722 --- /dev/null +++ b/src/mantine-utils/src/is/is.test.tsx @@ -0,0 +1,132 @@ +import React, { forwardRef } from 'react'; +import { + isArray, + isBoolean, + isString, + isDef, + isUnDef, + isDate, + isEmpty, + isNull, + isMap, + isNumber, + isFunction, + isObject, + isPromise, + isRegExp, + isWindow, + isElement, +} from './is'; + +const TestComponent = () =>
; +const TextRefComponent = forwardRef((_props, ref) =>
); + +describe('@mantine/utils/is', () => { + it('isArray', () => { + expect(isArray([])).toBe(true); + expect(isArray([1, 2])).toBe(true); + expect(isArray(true)).toBe(false); + expect(isArray({})).toBe(false); + }); + it('isBoolean', () => { + expect(isBoolean(false)).toBe(true); + expect(isBoolean(true)).toBe(true); + expect(isBoolean(['a', 'b'])).toBe(false); + expect(isBoolean({})).toBe(false); + }); + it('isString', () => { + expect(isString('')).toBe(true); + expect(isString('asd')).toBe(true); + expect(isString(['a', 'b'])).toBe(false); + expect(isString({})).toBe(false); + }); + it('isDef', () => { + const value = 0; + let unValue; + expect(isDef(value)).toBe(true); + expect(isDef(unValue)).toBe(false); + }); + it('isUnDef', () => { + const value = 0; + let unValue; + expect(isUnDef(unValue)).toBe(true); + expect(isUnDef(value)).toBe(false); + }); + it('isDate', () => { + const date = new Date(); + expect(isDate(date)).toBe(true); + expect(isDate(1)).toBe(false); + expect(isDate(date.getTime())).toBe(false); + }); + it('isEmpty', () => { + expect(isEmpty({})).toBe(true); + expect(isEmpty({ a: 1 })).toBe(false); + }); + it('isNull', () => { + expect(isNull(null)).toBe(true); + expect(isNull(1)).toBe(false); + }); + + it('isMap', () => { + const map = new Map(); + const set = new Set(); + expect(isMap(map)).toBe(true); + expect(isMap(set)).toBe(false); + }); + + it('isNumber', () => { + expect(isNumber(1)).toBe(true); + expect(isNumber(0)).toBe(true); + expect(isNumber('1')).toBe(false); + }); + + it('isFunction', () => { + const fn = () => {}; + expect(isFunction(fn)).toBe(true); + expect(isFunction('1')).toBe(false); + }); + + it('isObject', () => { + const fn = () => {}; + expect(isObject({})).toBe(true); + expect(isObject(fn)).toBe(false); + expect(isObject([])).toBe(false); + }); + + it('isPromise', () => { + const promise = new Promise(() => {}); + + expect(isPromise(promise)).toBe(true); + expect(isPromise({})).toBe(false); + expect(isObject([])).toBe(false); + }); + + it('isRegExp', () => { + const reg = /a/; + expect(isRegExp(reg)).toBe(true); + expect(isRegExp({})).toBe(false); + expect(isRegExp([])).toBe(false); + }); + + it('isWindow', () => { + expect(isWindow(window)).toBe(true); + expect(isWindow({})).toBe(false); + expect(isWindow([])).toBe(false); + }); + + it('correctly detects elements', () => { + expect(isElement(
Element
)).toBe(true); + expect(isElement()).toBe(true); + expect(isElement()).toBe(true); + }); + + it('correctly detects jsx parts that are not elements', () => { + expect(isElement(<>Element)).toBe(false); + expect(isElement('string')).toBe(false); + expect(isElement(2)).toBe(false); + expect(isElement(null)).toBe(false); + expect(isElement(undefined)).toBe(false); + expect(isElement(true)).toBe(false); + expect(isElement([
Element
])).toBe(false); + }); +}); diff --git a/src/mantine-utils/src/is/is.ts b/src/mantine-utils/src/is/is.ts new file mode 100644 index 00000000000..77a6df41db6 --- /dev/null +++ b/src/mantine-utils/src/is/is.ts @@ -0,0 +1,107 @@ +import React from 'react'; + +const { toString } = Object.prototype; + +export function is(val: unknown, type: string) { + return toString.call(val) === `[object ${type}]`; +} + +export function isDef(val?: T): val is T { + return typeof val !== 'undefined'; +} + +export function isUnDef(val?: T): val is T { + return !isDef(val); +} + +export function isArray(val: any): val is Array { + return val && Array.isArray(val); +} + +export function isObject(val: any): val is Record { + return val !== null && is(val, 'Object'); +} + +export function isString(val: unknown): val is string { + return is(val, 'String'); +} + +export function isFunction(val: unknown): val is Function { + return typeof val === 'function'; +} + +export function isEmpty(val: T): val is T { + if (isArray(val) || isString(val)) { + return val.length === 0; + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0; + } + + if (isObject(val)) { + return Object.keys(val).length === 0; + } + + return false; +} + +export function isDate(val: unknown): val is Date { + return is(val, 'Date'); +} + +export function isNull(val: unknown): val is null { + return val === null; +} + +export function isNullAndUnDef(val: unknown): val is null | undefined { + return isUnDef(val) && isNull(val); +} + +export function isNullOrUnDef(val: unknown): val is null | undefined { + return isUnDef(val) || isNull(val); +} + +export function isNumber(val: unknown): val is number { + return is(val, 'Number'); +} + +export function isPromise(val: unknown): val is Promise { + return ( + is(val, 'Promise') && + isFunction((val as Record).then) && + isFunction((val as Record).catch) + ); +} + +export function isBoolean(val: unknown): val is boolean { + return is(val, 'Boolean'); +} + +export function isRegExp(val: unknown): val is RegExp { + return is(val, 'RegExp'); +} + +export function isWindow(val: any): val is Window { + return typeof window !== 'undefined' && is(val, 'Window'); +} + +export function isElement(value: any): value is React.ReactElement { + if (Array.isArray(value) || value === null) { + return false; + } + + if (typeof value === 'object') { + if (value.type === React.Fragment) { + return false; + } + + return true; + } + + return false; +} + +export function isMap(val: unknown): val is Map { + return is(val, 'Map'); +} diff --git a/src/mantine-utils/src/use-previous/usePrevious.ts b/src/mantine-utils/src/use-previous/usePrevious.ts new file mode 100644 index 00000000000..a5233144ca6 --- /dev/null +++ b/src/mantine-utils/src/use-previous/usePrevious.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +export default function usePrevious(state: T): T | undefined { + const ref = useRef(); + + useEffect(() => { + ref.current = state; + }); + + return ref.current; +}