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;
+}