From 50e7921acc5cec721c4f1352aa639eaf3660c53f Mon Sep 17 00:00:00 2001 From: Aster Date: Fri, 26 Aug 2022 10:38:14 +0800 Subject: [PATCH 1/6] [@mantine/core] Indicator: count support --- docs/src/docs/core/Indicator.mdx | 18 +++ .../src/Indicator/Indicator.story.tsx | 6 +- .../src/Indicator/Indicator.styles.ts | 90 ++++++++---- src/mantine-core/src/Indicator/Indicator.tsx | 47 ++++++- .../src/Indicator/Machine/Machine.styles.ts | 9 ++ .../src/Indicator/Machine/Machine.tsx | 63 +++++++++ .../Indicator/Machine/MachineNumber.styles.ts | 99 +++++++++++++ .../src/Indicator/Machine/MachineNumber.tsx | 88 ++++++++++++ .../Indicator/Indicator.demo.configurator.tsx | 4 +- .../core/Indicator/Indicator.demo.count.tsx | 72 ++++++++++ .../core/Indicator/Indicator.demo.offset.tsx | 4 +- .../Indicator.demo.overflowCount.tsx | 111 +++++++++++++++ .../Indicator/Indicator.demo.processing.tsx | 34 +++++ .../Indicator/Indicator.demo.showZero.tsx | 60 ++++++++ .../src/demos/core/Indicator/index.ts | 4 + src/mantine-utils/src/index.ts | 22 ++- .../src/is-element/is-element.test.tsx | 23 --- .../src/is-element/is-element.ts | 21 --- src/mantine-utils/src/is/is.test.tsx | 132 ++++++++++++++++++ src/mantine-utils/src/is/is.ts | 107 ++++++++++++++ .../src/use-previous/usePrevious.ts | 11 ++ 21 files changed, 937 insertions(+), 88 deletions(-) create mode 100644 src/mantine-core/src/Indicator/Machine/Machine.styles.ts create mode 100644 src/mantine-core/src/Indicator/Machine/Machine.tsx create mode 100644 src/mantine-core/src/Indicator/Machine/MachineNumber.styles.ts create mode 100644 src/mantine-core/src/Indicator/Machine/MachineNumber.tsx create mode 100644 src/mantine-demos/src/demos/core/Indicator/Indicator.demo.count.tsx create mode 100644 src/mantine-demos/src/demos/core/Indicator/Indicator.demo.overflowCount.tsx create mode 100644 src/mantine-demos/src/demos/core/Indicator/Indicator.demo.processing.tsx create mode 100644 src/mantine-demos/src/demos/core/Indicator/Indicator.demo.showZero.tsx delete mode 100644 src/mantine-utils/src/is-element/is-element.test.tsx delete mode 100644 src/mantine-utils/src/is-element/is-element.ts create mode 100644 src/mantine-utils/src/is/is.test.tsx create mode 100644 src/mantine-utils/src/is/is.ts create mode 100644 src/mantine-utils/src/use-previous/usePrevious.ts diff --git a/docs/src/docs/core/Indicator.mdx b/docs/src/docs/core/Indicator.mdx index 79f42dec926..d26546bee30 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,45 @@ 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 ? theme.spacing.xs / 2 : 0, - paddingRight: withLabel ? theme.spacing.xs / 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', - }, - }) + common: { + ...getPositionStyles(position, offset), + position: 'absolute', + [withLabel ? 'minWidth' : 'width']: size, + height: size, + borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }), + }, + + indicator: { + zIndex, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: theme.fontSizes.xs, + paddingLeft: withLabel ? theme.spacing.xs / 2 : 0, + paddingRight: withLabel ? theme.spacing.xs / 2 : 0, + backgroundColor: 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`, + }, + }; + } ); diff --git a/src/mantine-core/src/Indicator/Indicator.tsx b/src/mantine-core/src/Indicator/Indicator.tsx index c4f629d48cc..a6c9a994526 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; @@ -44,8 +51,14 @@ export interface IndicatorProps /** Determines whether indicator should have border */ withBorder?: boolean; - /** When component is disabled it renders children without indicator */ - disabled?: boolean; + /** When isShow is false renders children without indicator */ + isShow?: 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']; @@ -56,8 +69,11 @@ const defaultProps: Partial = { offset: 0, inline: false, withBorder: false, - disabled: false, + isShow: true, + 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, + isShow, 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( + () => isShow && (dot || (!isUnDef(label) && !(label <= 0 && !showZero))), + [isShow, 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..63059d358a0 --- /dev/null +++ b/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx @@ -0,0 +1,88 @@ +import React, { useState, forwardRef, useRef, 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) => { + 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 numberRef = useRef(); + + 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..df445e1d965 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-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; +} From ed75805eaca71657c53b7de4b687caf5f7564064 Mon Sep 17 00:00:00 2001 From: Aster Date: Fri, 26 Aug 2022 17:56:04 +0800 Subject: [PATCH 2/6] [@mantine/core] Indicatoe: isShow back to disabled --- src/mantine-core/src/Indicator/Indicator.tsx | 12 ++++++------ .../core/Indicator/Indicator.demo.configurator.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mantine-core/src/Indicator/Indicator.tsx b/src/mantine-core/src/Indicator/Indicator.tsx index a6c9a994526..b186afeafd8 100644 --- a/src/mantine-core/src/Indicator/Indicator.tsx +++ b/src/mantine-core/src/Indicator/Indicator.tsx @@ -51,8 +51,8 @@ export interface IndicatorProps /** Determines whether indicator should have border */ withBorder?: boolean; - /** When isShow is false renders children without indicator */ - isShow?: boolean; + /** 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; @@ -69,7 +69,7 @@ const defaultProps: Partial = { offset: 0, inline: false, withBorder: false, - isShow: true, + disabled: false, showZero: false, processing: false, size: 10, @@ -95,7 +95,7 @@ export const Indicator = forwardRef((props, ref) overflowCount, showZero, classNames, - isShow, + disabled, zIndex, unstyled, processing, @@ -115,8 +115,8 @@ export const Indicator = forwardRef((props, ref) }, [label, overflowCount]); const isShowIndicator = useMemo( - () => isShow && (dot || (!isUnDef(label) && !(label <= 0 && !showZero))), - [isShow, label, showZero] + () => !disabled && (dot || (!isUnDef(label) && !(label <= 0 && !showZero))), + [disabled, label, showZero] ); return ( 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 df445e1d965..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 @@ -54,10 +54,10 @@ export const configurator: MantineDemo = { initialValue: 'top-end', defaultValue: 'top-end', }, - { name: 'radius', type: 'size', initialValue: 'xl', defaultValue: 'xl' }, { name: 'size', type: 'number', initialValue: 10, defaultValue: 10, step: 1, min: 6, max: 30 }, { name: 'dot', type: 'boolean', initialValue: true, defaultValue: true }, + { name: 'disabled', type: 'boolean', initialValue: false, defaultValue: false }, { name: 'withBorder', type: 'boolean', initialValue: false, defaultValue: false }, { name: 'processing', type: 'boolean', initialValue: false, defaultValue: false }, ], From c3269a6d26c07eadd358f8866a3977763e627675 Mon Sep 17 00:00:00 2001 From: Aster Date: Fri, 26 Aug 2022 18:47:06 +0800 Subject: [PATCH 3/6] [@mantine/core] Indicator: typescript error fix --- src/mantine-styles-api/src/styles-api/Indicator.styles-api.ts | 2 ++ 1 file changed, 2 insertions(+) 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', }; From b4be539a99705eed46b501d23816c9a407eed249 Mon Sep 17 00:00:00 2001 From: Aster Date: Fri, 26 Aug 2022 19:04:54 +0800 Subject: [PATCH 4/6] [@mantine/core] MachineNumber: test pass --- src/mantine-core/src/Indicator/Machine/MachineNumber.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx b/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx index 63059d358a0..a8087985175 100644 --- a/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx +++ b/src/mantine-core/src/Indicator/Machine/MachineNumber.tsx @@ -1,4 +1,4 @@ -import React, { useState, forwardRef, useRef, useEffect, useMemo } from 'react'; +import React, { useState, forwardRef, useEffect, useMemo } from 'react'; import { isUnDef, usePrevious } from '@mantine/utils'; import useStyles from './MachineNumber.styles'; @@ -8,13 +8,12 @@ interface MachineNumberProps { oldOriginalNumber: number; } -export const MachineNumber = forwardRef((props) => { +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 numberRef = useRef(); const scrollByDir = (dir: 'up' | 'down') => { setIsActive(true); @@ -61,7 +60,7 @@ export const MachineNumber = forwardRef((pro [isActive, scrollAnimationDirection] ); return ( - + {(oldNumber && ( Date: Fri, 26 Aug 2022 19:14:57 +0800 Subject: [PATCH 5/6] [docs] Indicator docs optimization --- docs/src/docs/core/Indicator.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/core/Indicator.mdx b/docs/src/docs/core/Indicator.mdx index d26546bee30..c048a300dd7 100644 --- a/docs/src/docs/core/Indicator.mdx +++ b/docs/src/docs/core/Indicator.mdx @@ -27,7 +27,7 @@ element to keep `display: block`. -## overflowCount +## OverflowCount Set `overflowCount` to handle overflow cases: From 916fd6e7ea91d7e5b27865a0eb838633d6f07516 Mon Sep 17 00:00:00 2001 From: Aster Date: Fri, 9 Sep 2022 10:36:39 +0800 Subject: [PATCH 6/6] [@mantine/core] Indicator: Resolve conflicts --- docs/package.json | 2 +- docs/src/components/Layout/LayoutInner.tsx | 6 +- docs/src/docs/changelog/5-3-0.mdx | 81 +++++++ docs/src/docs/core/AppShell.mdx | 17 ++ docs/src/docs/core/Grid.mdx | 16 +- docs/src/docs/core/ScrollArea.mdx | 4 + docs/src/docs/form/create-form-context.mdx | 111 +++++++++ docs/src/docs/form/recipes.mdx | 2 +- docs/src/docs/form/status.mdx | 2 +- docs/src/docs/form/use-form.mdx | 229 ++++++++++++++---- docs/src/docs/hooks/use-local-storage.mdx | 14 +- docs/src/docs/hooks/use-previous.mdx | 27 +++ docs/yarn.lock | 8 +- package.json | 5 +- src/mantine-carousel/src/Carousel.tsx | 12 + .../AccordionControl.styles.ts | 2 +- .../AccordionPanel/AccordionPanel.styles.ts | 2 +- .../src/AppShell/AppShell.story.tsx | 1 + src/mantine-core/src/AppShell/AppShell.tsx | 8 + src/mantine-core/src/Badge/Badge.styles.ts | 4 +- src/mantine-core/src/Burger/Burger.styles.ts | 6 +- src/mantine-core/src/Code/Code.styles.ts | 2 +- src/mantine-core/src/Dialog/Dialog.styles.ts | 4 +- src/mantine-core/src/Grid/Col/Col.styles.ts | 54 +++-- src/mantine-core/src/Grid/Col/Col.tsx | 19 +- .../src/Indicator/Indicator.styles.ts | 15 +- src/mantine-core/src/Input/Input.styles.ts | 4 +- src/mantine-core/src/Kbd/Kbd.styles.ts | 2 +- .../Menu/MenuDivider/MenuDivider.styles.ts | 2 +- .../src/Menu/MenuLabel/MenuLabel.styles.ts | 2 +- .../src/MultiSelect/MultiSelect.styles.ts | 6 +- .../src/NumberInput/NumberInput.tsx | 7 +- src/mantine-core/src/Radio/Radio.story.tsx | 17 ++ src/mantine-core/src/Radio/Radio.tsx | 1 + .../src/Radio/RadioGroup.context.ts | 1 + .../src/Radio/RadioGroup/RadioGroup.tsx | 6 +- .../src/Slider/Marks/Marks.styles.ts | 2 +- .../src/Slider/Thumb/Thumb.styles.ts | 2 +- .../src/Stepper/Step/Step.styles.ts | 2 +- .../src/Stepper/Stepper.styles.ts | 4 +- .../src/Tabs/TabsList/TabsList.styles.ts | 2 +- .../TimelineItem/TimelineItem.styles.ts | 2 +- .../src/Tooltip/Tooltip.styles.ts | 2 +- .../RenderList/RenderList.styles.ts | 8 +- .../TypographyStylesProvider.styles.ts | 6 +- .../src/components/Month/Month.styles.ts | 2 +- .../Demo/CodeDemo/CodeDemo.styles.ts | 2 +- .../src/demos/core/Grid/Grid.demo.auto.tsx | 33 +++ .../src/demos/core/Grid/Grid.demo.content.tsx | 31 +++ .../src/demos/core/Grid/index.ts | 2 + .../ScrollArea/ScrollArea.demo.stylesApi.tsx | 87 +++++++ .../src/demos/core/ScrollArea/index.ts | 1 + src/mantine-demos/src/demos/hooks/index.ts | 1 + .../demos/hooks/use-previous.demo.usage.tsx | 52 ++++ .../src/FormProvider/FormProvider.tsx | 33 +++ src/mantine-form/src/index.ts | 1 + .../src/resolvers/test-resolver.ts | 111 --------- .../zod-resolver/zod-resolver.test.ts | 26 -- .../src/stories/Form.context.story.tsx | 35 +++ .../src/stories/Form.dirty.story.tsx | 2 +- .../src/stories/Form.rerendering2.story.tsx | 51 ++++ src/mantine-form/src/types.ts | 4 + src/mantine-hooks/src/index.ts | 2 + .../src/use-local-storage/create-storage.ts | 110 +++++++++ .../use-local-storage/use-local-storage.ts | 107 +------- .../src/use-network/use-network.ts | 8 +- .../src/use-previous/use-previous.test.ts | 19 ++ .../src/use-previous/use-previous.ts | 11 + .../use-resize-observer.ts | 4 +- .../use-session-storage.story.tsx | 91 +++++++ .../use-session-storage.ts | 5 + .../src/TagInput/TagInput.styles.ts | 10 +- .../RichTextEditor/RichTextEditor.styles.ts | 16 +- .../src/components/Toolbar/Toolbar.styles.ts | 4 +- .../src/ActionsList/ActionsList.styles.ts | 2 +- yarn.lock | 8 +- 76 files changed, 1217 insertions(+), 385 deletions(-) create mode 100644 docs/src/docs/changelog/5-3-0.mdx create mode 100644 docs/src/docs/form/create-form-context.mdx create mode 100644 docs/src/docs/hooks/use-previous.mdx create mode 100644 src/mantine-demos/src/demos/core/Grid/Grid.demo.auto.tsx create mode 100644 src/mantine-demos/src/demos/core/Grid/Grid.demo.content.tsx create mode 100644 src/mantine-demos/src/demos/core/ScrollArea/ScrollArea.demo.stylesApi.tsx create mode 100644 src/mantine-demos/src/demos/hooks/use-previous.demo.usage.tsx create mode 100644 src/mantine-form/src/FormProvider/FormProvider.tsx delete mode 100644 src/mantine-form/src/resolvers/test-resolver.ts delete mode 100644 src/mantine-form/src/resolvers/zod-resolver/zod-resolver.test.ts create mode 100644 src/mantine-form/src/stories/Form.context.story.tsx create mode 100644 src/mantine-form/src/stories/Form.rerendering2.story.tsx create mode 100644 src/mantine-hooks/src/use-local-storage/create-storage.ts create mode 100644 src/mantine-hooks/src/use-previous/use-previous.test.ts create mode 100644 src/mantine-hooks/src/use-previous/use-previous.ts create mode 100644 src/mantine-hooks/src/use-session-storage/use-session-storage.story.tsx create mode 100644 src/mantine-hooks/src/use-session-storage/use-session-storage.ts diff --git a/docs/package.json b/docs/package.json index c917c3e0b68..3af6c795711 100644 --- a/docs/package.json +++ b/docs/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.14.3", "@types/github-slugger": "^1.3.0", "lmdb-store": "^1.6.8", - "typescript": "4.7.4" + "typescript": "4.8.2" }, "license": "MIT", "scripts": { diff --git a/docs/src/components/Layout/LayoutInner.tsx b/docs/src/components/Layout/LayoutInner.tsx index 4726952cdca..1209d0160f0 100644 --- a/docs/src/components/Layout/LayoutInner.tsx +++ b/docs/src/components/Layout/LayoutInner.tsx @@ -107,7 +107,11 @@ export function LayoutInner({ children, location }: LayoutProps) { searchPlaceholder="Search documentation" shortcut={['mod + K', 'mod + P', '/']} highlightQuery - searchInputProps={{ id: 'search-mantine-docs' }} + searchInputProps={{ + id: 'search-mantine-docs', + name: 'no-autocomplete', + autoComplete: 'nope', + }} transition={{ in: { transform: 'translateY(0)', opacity: 1 }, out: { transform: 'translateY(-20px)', opacity: 0 }, diff --git a/docs/src/docs/changelog/5-3-0.mdx b/docs/src/docs/changelog/5-3-0.mdx new file mode 100644 index 00000000000..a5e80ec2b2e --- /dev/null +++ b/docs/src/docs/changelog/5-3-0.mdx @@ -0,0 +1,81 @@ +--- +group: 'changelog' +title: 'Version 5.3.0' +order: 1 +slug: /changelog/5-3-0/ +release: https://github.com/mantinedev/mantine/releases/tag/5.3.0 +date: 'August 18th, 2022' +--- + +import { HooksDemos, GridDemos } from '@mantine/demos'; + +## Form context + +`@mantine/form` package now exports `createFormContext` function to create provider component, +hook to get form object from context and [use-form](/form/use-form/) hook with predefined type: + +```tsx +import { createFormContext } from '@mantine/form'; +import { TextInput } from '@mantine/core'; + +// Definition of form values is required +interface FormValues { + age: number; + name: string; +} + +// createFormContext returns a tuple with 3 items: +// FormProvider is a component that sets form context +// useFromContext hook return form object that was previously set in FormProvider +// useForm hook works the same way as useForm exported from the package but has predefined type +const [FormProvider, useFormContext, useForm] = createFormContext(); + +function ContextField() { + const form = useFormContext(); + return ; +} + +export function Context() { + // Create form as described in use-form documentation + const form = useForm({ + initialValues: { + age: 0, + name: '', + }, + }); + + // Wrap your form with FormProvider + return ( + +
{})}> + + +
+ ); +} +``` + +## Grid improvements + +[Grid.Col](https://mantine.dev/core/grid/) component now supports setting column span (and other related responsive props) to `auto` and `content`: + + + + + +## use-previous hook + +[use-previous](https://mantine.dev/hooks/use-previous/) hook stores the previous value of a state in a ref, +it returns undefined on initial render and the previous value of a state after rerender: + + + +## Other changes + +- [ColorSwatch](https://mantine.dev/core/color-swatch/) now supports `withShadow` prop +- [MultiSelect](https://mantine.dev/core/multi-select/) dropdown is no longer opened when selected item is removed +- [Radio.Group](https://mantine.dev/core/radio) component now supports `name` prop to set name on every child Radio component +- [AppShell](https://mantine.dev/core/app-shell/) component now supports `hidden` prop to hide Header, Footer, Navbar and Aside components +- [Carousel](https://mantine.dev/others/carousel/) component now supports `skipSnaps` and `containScroll` props +- [NumberInput](https://mantine.dev/core/number-input/) `type` can now be changed +- New [use-session-storage](https://mantine.dev/hooks/use-local-storage/#use-session-storage) hook diff --git a/docs/src/docs/core/AppShell.mdx b/docs/src/docs/core/AppShell.mdx index 1db36dbf2dc..b1fed9ba05f 100644 --- a/docs/src/docs/core/AppShell.mdx +++ b/docs/src/docs/core/AppShell.mdx @@ -96,6 +96,23 @@ export default function AppShellDemo() { } ``` +## hidden prop + +To hide all AppShell components and render only children set `hidden` prop: + +```tsx +import { AppShell, Navbar, Header } from '@mantine/core'; + +function Demo() { + // Navbar and Header will not be rendered when hidden prop is set + return ( + } header={
} hidden> + App content + + ); +} +``` + ## Navbar and Aside components Navbar and Aside components can be used outside of AppShell context ([View full source code](https://github.com/mantinedev/mantine/tree/master/src/mantine-demos/src/demos/core/AppShell)): diff --git a/docs/src/docs/core/Grid.mdx b/docs/src/docs/core/Grid.mdx index 37144371184..dabe1009097 100644 --- a/docs/src/docs/core/Grid.mdx +++ b/docs/src/docs/core/Grid.mdx @@ -31,7 +31,7 @@ Use xs, sm, md, lg, xl values to set spacing from `theme.spacing` or number to s ## Grow -Set `grow` prop on Grid component to force last row take 100% of container width: +Set `grow` prop on Grid component to force all rows to take 100% of container width: @@ -70,6 +70,20 @@ In this example up to `md` there will be 1 column, from `md` to `lg` there will +## Auto sized columns + +All columns in a row with `span` or a breakpoint of `auto` will have equal size, growing as much as they can to fill the row. + +In this example, the second column takes up 50% of the row while the other two columns automatically resize to fill the remaining space: + + + +## Fit column content + +If you set `span` or a breakpoint to `content`, the column's size will automatically adjust to match the width of its content: + + + ## Change columns count By default, grid uses 12 columns layout, you can change it by setting `columns` prop on Grid component. diff --git a/docs/src/docs/core/ScrollArea.mdx b/docs/src/docs/core/ScrollArea.mdx index 80b539bad62..003ee433e03 100644 --- a/docs/src/docs/core/ScrollArea.mdx +++ b/docs/src/docs/core/ScrollArea.mdx @@ -57,6 +57,10 @@ For example, it can be used with [Navbar.Section](/core/app-shell/) component: +## Styles API + + + ## ScrollArea.Autosize `ScrollArea.Autosize` component allows to create scrollable containers when given `maxHeight` is reached: diff --git a/docs/src/docs/form/create-form-context.mdx b/docs/src/docs/form/create-form-context.mdx new file mode 100644 index 00000000000..d29023923c8 --- /dev/null +++ b/docs/src/docs/form/create-form-context.mdx @@ -0,0 +1,111 @@ +--- +group: 'mantine-form' +package: '@mantine/form' +title: 'Form context' +order: 5 +slug: /form/create-form-context/ +description: 'Add context support to use-form with createFormContext' +docs: 'form/create-form-provider.mdx' +source: 'mantine-form/src' +--- + +## Usage + +`createFormContext` function creates context provider and hook to get form object from context: + +```tsx +import { createFormContext } from '@mantine/form'; +import { TextInput } from '@mantine/core'; + +// Definition of form values is required +interface FormValues { + age: number; + name: string; +} + +// createFormContext returns a tuple with 3 items: +// FormProvider is a component that sets form context +// useFromContext hook return form object that was previously set in FormProvider +// useForm hook works the same way as useForm exported from the package but has predefined type +const [FormProvider, useFormContext, useForm] = createFormContext(); + +function ContextField() { + const form = useFormContext(); + return ; +} + +export function Context() { + // Create form as described in use-form documentation + const form = useForm({ + initialValues: { + age: 0, + name: '', + }, + }); + + // Wrap your form with FormProvider + return ( + +
{})}> + + +
+ ); +} +``` + +## Store context in separate file + +Usually it is a good idea to store form context in separate file to avoid dependencies cycle: + +```tsx +// form-context.ts file +import { createFormContext } from '@mantine/form'; + +interface UserFormValues { + age: number; + name: string; +} + +// You can give context variables any name +export const [UserFormProvider, useUserFormContext, useUserForm] = + createFormContext(); +``` + +Then you can import context variables from anywhere: + +```tsx +// NameInput.tsx +import { TextInput } from '@mantine/core'; +import { useUserFormContext } from './form-context'; + +export function NameInput() { + const form = useUserFormContext(); + return ; +} +``` + +```tsx +// UserForm.tsx +import { NumberInput } from '@mantine/core'; +import { UserFormProvider, useUserForm } from './form-context'; +import { NameInput } from './NameInput'; + +function UserForm() { + const form = useUserForm({ + initialValues: { + age: 0, + name: '', + }, + }); + + return ( + +
{})}> + + + +
+ ); +} +``` diff --git a/docs/src/docs/form/recipes.mdx b/docs/src/docs/form/recipes.mdx index 6ec280c3fee..ef8428ea8a9 100644 --- a/docs/src/docs/form/recipes.mdx +++ b/docs/src/docs/form/recipes.mdx @@ -2,7 +2,7 @@ group: 'mantine-form' package: '@mantine/form' title: 'Recipes' -order: 2 +order: 6 slug: /form/recipes/ description: 'use-form usage examples' docs: 'form/recipes.mdx' diff --git a/docs/src/docs/form/status.mdx b/docs/src/docs/form/status.mdx index 488912144d5..cc8e5a0da80 100644 --- a/docs/src/docs/form/status.mdx +++ b/docs/src/docs/form/status.mdx @@ -2,7 +2,7 @@ group: 'mantine-form' package: '@mantine/form' title: 'Touched & dirty' -order: 1 +order: 3 slug: /form/status/ description: 'Get fields and form touched and dirty status' docs: 'form/status.mdx' diff --git a/docs/src/docs/form/use-form.mdx b/docs/src/docs/form/use-form.mdx index e5abf117caf..1a82e758641 100644 --- a/docs/src/docs/form/use-form.mdx +++ b/docs/src/docs/form/use-form.mdx @@ -35,40 +35,192 @@ yarn add @mantine/form ## API overview -`useForm` hook accepts a single argument with a configuration object that includes the following properties (all of them are optional): - -- `initialValues` – initial form values, form types are generated based on this value -- `initialErrors` – initial form errors, object of React nodes -- `initialTouched` – initial touched state -- `initialDirty` – initial dirty state -- `validate` – an object with validation rules, schema or a validation function that receives form values as an argument and returns object with validation errors -- `clearInputErrorOnChange` – boolean value that determines whether input error should be clear when its value changes, true by default - -Hook returns an object with the following properties: - -- `values` – current form values -- `setValues` – handler to set all form values -- `setFieldValue` – handler to set value of the specified form field -- `errors` – current validation errors -- `setErrors` – sets validation errors -- `clearErrors` – clears all validation errors -- `clearFieldError` – clears validation error of the specified field -- `setFieldError` – sets validation error of the specified field -- `removeListItem` – removes list item at given field and index -- `insertListItem` – inserts list item at given index -- `reorderListItem` – reorders list item with given position at specified field -- `validate` – validates all fields, returns validation results -- `validateField` – validates specified field, returns validation results -- `onSubmit` – wrapper function for form `onSubmit` event handler -- `onReset` – wrapper function for form `onReset` event handler -- `reset` – resets `values` to initial state, clears all validation errors -- `isTouched` – returns boolean value that indicates that user focused or modified field -- `isDirty` – returns boolean value that indicates that field value is not the same as specified in `initialValues` -- `setDirty` – sets fields dirty state -- `setTouched` – sets fields touched state -- `resetDirty` – clears dirty state -- `resetTouched` – clears touched state -- `getInputProps` – returns an object with value, onChange and error that should be spread on input +All examples below use the following example use-form hook. + +```tsx +import { useForm } from '@mantine/form'; + +const form = useForm({ + initialValues: { + path: '', + path2: '', + user: { + firstName: 'John', + lastName: 'Doe', + }, + fruits: [ + { name: 'Banana', available: true }, + { name: 'Orange', available: false }, + ], + accepted: false, + }, +}); +``` + +### Values + +[Form values guide](/form/values/) + +```tsx +// get current form values +form.values; + +// Set all form values +form.setValues(values); + +// Set value of single field +form.setFieldValue('path', value); + +// Set value of nested field +form.setFieldValue('user.firstName', 'Jane'); + +// Resets form.values to initialValues, +// clears all validation errors, +// resets touched and dirty state +form.reset(); +``` + +### List items + +[Nested fields guide](/form/nested/) + +```tsx +// Inserts given list item at the specified path +form.insertListItem('fruits', { name: 'Apple', available: true }); + +// An optional index may be provided to specify the position in a nested field. +// If the index is passed where an item already exists, it will be replaced. +// If the index is larger than the current list, the element is inserted at the last position. +form.insertListItem('fruits', { name: 'Orange', available: true }, 1); + +// Removes the list item at the specified path and index. +form.removeListItem('fruits', 1); + +// Swaps two items of the list at the specified path. +// If no element exists at the `from` or `to` index, the list doesn't change. +form.reorderListItem('fruits', { from: 1, to: 0 }); +``` + +### Validation + +[Form validation guide](/form/validation/) + +```tsx +import { useForm } from '@mantine/form'; + +const form = useForm({ + initialValues: { + email: '', + user: { + firstName: '', + lastName: '', + }, + }, + validate: { + email: (value) => (value.length < 2 ? 'Invalid email' : null), + user: { + firstName: (value) => (value.length < 2 ? 'First name must have at least 2 letters' : null), + }, + }, +}); + +// Validates all fields with specified `validate` function or schema, sets form.errors +form.validate(); + +// Validates single field at specified path, sets form.errors +form.validateField('user.firstName'); + +// Works the same way as form.validate but does not set form.errors +form.isValid(); +form.isValid('user.firstName'); +``` + +### Errors + +[Form errors guide](/form/errors/) + +Validation errors occur when defined validation rules were violated, `initialErrors` were specified in useForm properties +or validation errors were set manually. + +```tsx +// get current errors state +form.errors; + +// Set all errors +form.setErrors({ path: 'Error message', path2: 'Another error' }); + +// Set error message at specified path +form.setFieldError('user.lastName', 'No special characters allowed'); + +// Clears all errors +form.clearErrors(); + +// Clears error of field at specified path +form.clearFieldError('path'); +``` + +### onReset and onSubmit + +Wrapper function for form `onSubmit` and `onReset` event handler. `onSubmit` handler accepts as second argument a function +that will be called with errors object when validation fails. + +```tsx +
+
{ setFormValues(values) }, + (validationErrors, _values, _event) => { console.log(validationErrors) } +)}>
+
+``` + +### Touched and dirty + +[Touched & dirty guide](/form/status/) + +```tsx +// Returns true if user interacted with any field inside form in any way +form.isTouched(); + +// Returns true if user interacted with field at specified path +form.isTouched('path'); + +// Set all touched values +form.setTouched({ 'user.firstName': true, 'user.lastName': false }); + +// Clears touched status of all fields +form.resetTouched(); + +// Returns true if form values are not deep equal to initialValues +form.isDirty(); + +// Returns true if field value is not deep equal to initialValues +form.isDirty('path'); + +// Sets dirty status of all fields +form.setDirty({ 'user.firstName': true, 'user.lastName': false }); + +// Clears dirty status of all fields, saves form.values snapshot +// After form.resetDirty is called, form.isDirty will compare +// form.values to snapshot instead of initialValues +form.resetDirty(); +``` + +### getInputProps + +`form.getInputProps` returns an object with `value`, `onChange`, `onFocus`, `onBlur` and `error` that should be spread on input. + +As second parameter options can be passed. + +- `type`: default `input`. Needs to be configured to `checkbox` if input requires `checked` to be set instead of `value`. +- `withError`: default `type === 'input'`. Specifies if the returned object contains an `error` property with + `form.errors[path]` value. +- `withFocus`: default `true`. Specifies if the returned object contains an `onFocus` handler. If disabled, the touched state + of the form can only be used if all values are set with `setFieldValue`. + +```tsx + + +``` ## UseFormReturnType @@ -101,12 +253,3 @@ function Demo() { ); } ``` - -## Documentation - -- [Form values](/form/values/) -- [Form errors](/form/errors/) -- [Touched & dirty status](/form/status/) -- [Form validation](/form/validation/) -- [Nested fields](/form/nested/) -- [Recipes](/form/recipes/) diff --git a/docs/src/docs/hooks/use-local-storage.mdx b/docs/src/docs/hooks/use-local-storage.mdx index 1f8489d7a35..70de08d939b 100644 --- a/docs/src/docs/hooks/use-local-storage.mdx +++ b/docs/src/docs/hooks/use-local-storage.mdx @@ -13,8 +13,8 @@ source: 'mantine-hooks/src/use-local-storage/use-local-storage.ts' ## Usage -use-local-storage allows you to use value from `localStorage` as react state. -Hook works exactly the same as `useState`, but also writes the value to local storage: +`use-local-storage` allows you to use value from `localStorage` as react state. +Hook works the same way as `useState`, but also writes the value to `localStorage`: ```tsx import { useLocalStorage } from '@mantine/hooks'; @@ -97,6 +97,16 @@ const [value, setValue] = useLocalStorage({ }); ``` +## use-session-storage + +`use-session-storage` hook works the same way as `use-local-storage` hook but uses `window.sessionStorage` instead of `window.localStorage`: + +```tsx +import { useSessionStorage } from '@mantine/hooks'; + +const [value, setValue] = useSessionStorage({ key: 'session-key', defaultValue: 'mantine' }); +``` + ## TypeScript ### Definition diff --git a/docs/src/docs/hooks/use-previous.mdx b/docs/src/docs/hooks/use-previous.mdx new file mode 100644 index 00000000000..bf7bc4ac179 --- /dev/null +++ b/docs/src/docs/hooks/use-previous.mdx @@ -0,0 +1,27 @@ +--- +group: 'mantine-hooks' +package: '@mantine/hooks' +category: 'state' +title: 'use-previous' +order: 1 +slug: /hooks/use-previous/ +description: 'Get the previous value of a state' +import: "import { usePrevious } from '@mantine/hooks';" +docs: 'hooks/use-previous.mdx' +source: 'mantine-hooks/src/use-previous/use-previous.ts' +--- + +import { HooksDemos } from '@mantine/demos'; + +## Usage + +`use-previous` hook stores the previous value of a state in a ref, +it returns undefined on initial render and the previous value of a state after rerender: + + + +## Definition + +```tsx +function usePrevious(value: T): T | undefined; +``` diff --git a/docs/yarn.lock b/docs/yarn.lock index b26bd017513..f546d19fbff 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -11907,10 +11907,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" + integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== ua-parser-js@^0.7.30: version "0.7.31" diff --git a/package.json b/package.json index a4729527dea..52b22632357 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "stylis": "^4.0.10", "syncpack": "^5.7.11", "tsconfig-paths-webpack-plugin": "^3.5.1", - "typescript": "4.7.4", + "typescript": "4.8.2", "yargs": "^17.0.1" }, "dependencies": { @@ -147,5 +147,8 @@ "react-transition-group": "4.4.2", "stylis-plugin-rtl": "^2.1.1", "zod": "^3.13.4" + }, + "volta": { + "node": "14.17.0" } } diff --git a/src/mantine-carousel/src/Carousel.tsx b/src/mantine-carousel/src/Carousel.tsx index 109b4831ae3..39ab41976e7 100644 --- a/src/mantine-carousel/src/Carousel.tsx +++ b/src/mantine-carousel/src/Carousel.tsx @@ -104,6 +104,12 @@ export interface CarouselProps /** Previous control icon */ previousControlIcon?: React.ReactNode; + + /** Allow the carousel to skip scroll snaps if it's dragged vigorously. Note that this option will be ignored if the dragFree option is set to true, false by default */ + skipSnaps?: boolean; + + /** Clear leading and trailing empty space that causes excessive scrolling. Use trimSnaps to only use snap points that trigger scrolling or keepSnaps to keep them. */ + containScroll?: 'trimSnaps' | 'keepSnaps' | ''; } const defaultProps: Partial = { @@ -123,6 +129,8 @@ const defaultProps: Partial = { inViewThreshold: 0, withControls: true, withIndicators: false, + skipSnaps: false, + containScroll: '', }; export const _Carousel = forwardRef((props, ref) => { @@ -158,6 +166,8 @@ export const _Carousel = forwardRef((props, ref) nextControlIcon, previousControlIcon, breakpoints, + skipSnaps, + containScroll, ...others } = useComponentDefaultProps('Carousel', defaultProps, props); @@ -178,6 +188,8 @@ export const _Carousel = forwardRef((props, ref) dragFree, speed, inViewThreshold, + skipSnaps, + containScroll, }, plugins ); diff --git a/src/mantine-core/src/Accordion/AccordionControl/AccordionControl.styles.ts b/src/mantine-core/src/Accordion/AccordionControl/AccordionControl.styles.ts index e26ecfac373..dc33d1d97ee 100644 --- a/src/mantine-core/src/Accordion/AccordionControl/AccordionControl.styles.ts +++ b/src/mantine-core/src/Accordion/AccordionControl/AccordionControl.styles.ts @@ -67,7 +67,7 @@ export default createStyles( alignItems: 'center', flexDirection: chevronPosition === 'right' ? 'row-reverse' : 'row', padding: `${theme.spacing.md}px ${theme.spacing.md / 2}px`, - paddingLeft: chevronPosition === 'right' ? theme.spacing.sm + 4 : null, + paddingLeft: chevronPosition === 'right' ? `calc(${theme.spacing.sm}px + 4px)` : null, textAlign: 'left', color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black, diff --git a/src/mantine-core/src/Accordion/AccordionPanel/AccordionPanel.styles.ts b/src/mantine-core/src/Accordion/AccordionPanel/AccordionPanel.styles.ts index 87f877709b3..4cc3d6a6c3f 100644 --- a/src/mantine-core/src/Accordion/AccordionPanel/AccordionPanel.styles.ts +++ b/src/mantine-core/src/Accordion/AccordionPanel/AccordionPanel.styles.ts @@ -11,6 +11,6 @@ export default createStyles((theme, _params: AccordionStylesParams) => ({ content: { padding: theme.spacing.md, - paddingTop: theme.spacing.xs / 2, + paddingTop: `calc(${theme.spacing.xs}px / 2)`, }, })); diff --git a/src/mantine-core/src/AppShell/AppShell.story.tsx b/src/mantine-core/src/AppShell/AppShell.story.tsx index f6dfb968810..5f59303777f 100644 --- a/src/mantine-core/src/AppShell/AppShell.story.tsx +++ b/src/mantine-core/src/AppShell/AppShell.story.tsx @@ -114,6 +114,7 @@ storiesOf('AppShell/AppShell', module) Header
} navbar={Navbar} + hidden > {content}
diff --git a/src/mantine-core/src/AppShell/AppShell.tsx b/src/mantine-core/src/AppShell/AppShell.tsx index 0406b3d384a..f415afa1d00 100644 --- a/src/mantine-core/src/AppShell/AppShell.tsx +++ b/src/mantine-core/src/AppShell/AppShell.tsx @@ -33,6 +33,9 @@ export interface AppShellProps /** true to switch from static layout to fixed */ fixed?: boolean; + /** true to hide all AppShell parts and render only children */ + hidden?: boolean; + /** AppShell content */ children: React.ReactNode; @@ -68,6 +71,7 @@ export const AppShell = forwardRef((props: AppShe styles, classNames, unstyled, + hidden, ...others } = useComponentDefaultProps('AppShell', defaultProps, props); const { classes, cx } = useStyles( @@ -75,6 +79,10 @@ export const AppShell = forwardRef((props: AppShe { styles, classNames, unstyled, name: 'AppShell' } ); + if (hidden) { + return <>{children}; + } + return ( diff --git a/src/mantine-core/src/Badge/Badge.styles.ts b/src/mantine-core/src/Badge/Badge.styles.ts index fc0760592d0..dbf8ebe5024 100644 --- a/src/mantine-core/src/Badge/Badge.styles.ts +++ b/src/mantine-core/src/Badge/Badge.styles.ts @@ -84,11 +84,11 @@ export default createStyles( return { leftSection: { - marginRight: theme.spacing.xs / 2, + marginRight: `calc(${theme.spacing.xs}px / 2)`, }, rightSection: { - marginLeft: theme.spacing.xs / 2, + marginLeft: `calc(${theme.spacing.xs}px / 2)`, }, inner: { diff --git a/src/mantine-core/src/Burger/Burger.styles.ts b/src/mantine-core/src/Burger/Burger.styles.ts index a6b6e4165e2..ab59b100a87 100644 --- a/src/mantine-core/src/Burger/Burger.styles.ts +++ b/src/mantine-core/src/Burger/Burger.styles.ts @@ -21,9 +21,9 @@ export default createStyles((theme, { size, color, transitionDuration }: BurgerS return { root: { borderRadius: theme.radius.sm, - width: sizeValue + theme.spacing.xs, - height: sizeValue + theme.spacing.xs, - padding: theme.spacing.xs / 2, + width: `calc(${sizeValue}px + ${theme.spacing.xs}px)`, + height: `calc(${sizeValue}px + ${theme.spacing.xs}px)`, + padding: `calc(${theme.spacing.xs}px / 2)`, cursor: 'pointer', }, diff --git a/src/mantine-core/src/Code/Code.styles.ts b/src/mantine-core/src/Code/Code.styles.ts index c638cfbbdeb..edc063d433b 100644 --- a/src/mantine-core/src/Code/Code.styles.ts +++ b/src/mantine-core/src/Code/Code.styles.ts @@ -12,7 +12,7 @@ export default createStyles((theme, { color: _color }: CodeStylesParams) => { root: { ...theme.fn.fontStyles(), lineHeight: theme.lineHeight, - padding: `2px ${theme.spacing.xs / 2}px`, + padding: `2px calc(${theme.spacing.xs}px / 2)`, borderRadius: theme.radius.sm, color: theme.colorScheme === 'dark' diff --git a/src/mantine-core/src/Dialog/Dialog.styles.ts b/src/mantine-core/src/Dialog/Dialog.styles.ts index b69c19466d8..a10f0547c41 100644 --- a/src/mantine-core/src/Dialog/Dialog.styles.ts +++ b/src/mantine-core/src/Dialog/Dialog.styles.ts @@ -24,7 +24,7 @@ export default createStyles((theme, { size }: DialogStylesParams) => ({ closeButton: { position: 'absolute', - top: theme.spacing.md / 2, - right: theme.spacing.md / 2, + top: `calc(${theme.spacing.md}px / 2)`, + right: `calc(${theme.spacing.md}px / 2)`, }, })); diff --git a/src/mantine-core/src/Grid/Col/Col.styles.ts b/src/mantine-core/src/Grid/Col/Col.styles.ts index 903b287bc49..c36a794c1f7 100644 --- a/src/mantine-core/src/Grid/Col/Col.styles.ts +++ b/src/mantine-core/src/Grid/Col/Col.styles.ts @@ -6,6 +6,8 @@ import { MantineTheme, } from '@mantine/styles'; +export type ColSpan = number | 'auto' | 'content'; + interface ColStyles { gutter: MantineNumberSize; columns: number; @@ -16,12 +18,12 @@ interface ColStyles { offsetMd: number; offsetLg: number; offsetXl: number; - span: number; - xs: number; - sm: number; - md: number; - lg: number; - xl: number; + span: ColSpan; + xs: ColSpan; + sm: ColSpan; + md: ColSpan; + lg: ColSpan; + xl: ColSpan; order: React.CSSProperties['order']; orderXs: React.CSSProperties['order']; orderSm: React.CSSProperties['order']; @@ -30,8 +32,29 @@ interface ColStyles { orderXl: React.CSSProperties['order']; } -const getColumnWidth = (colSpan: number, columns: number) => - colSpan ? `${100 / (columns / colSpan)}%` : undefined; +const getColumnFlexBasis = (colSpan: ColSpan, columns: number) => { + if (colSpan === 'content') { + return 'auto'; + } + if (colSpan === 'auto') { + return '0px'; + } + return colSpan ? `${100 / (columns / colSpan)}%` : undefined; +}; + +const getColumnMaxWidth = (colSpan: ColSpan, columns: number, grow: boolean) => { + if (grow || colSpan === 'auto' || colSpan === 'content') { + return 'unset'; + } + return getColumnFlexBasis(colSpan, columns); +}; + +const getColumnFlexGrow = (colSpan: ColSpan, grow: boolean) => { + if (!colSpan) { + return undefined; + } + return colSpan === 'auto' || grow ? 1 : 0; +}; const getColumnOffset = (offset: number, columns: number) => offset ? `${100 / (columns / offset)}%` : undefined; @@ -44,7 +67,7 @@ function getBreakpointsStyles({ columns, grow, }: { - sizes: Record; + sizes: Record; offsets: Record; orders: Record; grow: boolean; @@ -54,10 +77,12 @@ function getBreakpointsStyles({ return MANTINE_SIZES.reduce((acc, size) => { acc[`@media (min-width: ${theme.breakpoints[size] + 1}px)`] = { order: orders[size], - flexBasis: getColumnWidth(sizes[size], columns), + flexBasis: getColumnFlexBasis(sizes[size], columns), flexShrink: 0, - maxWidth: grow ? 'unset' : getColumnWidth(sizes[size], columns), + width: sizes[size] === 'content' ? 'auto' : undefined, + maxWidth: getColumnMaxWidth(sizes[size], columns, grow), marginLeft: getColumnOffset(offsets[size], columns), + flexGrow: getColumnFlexGrow(sizes[size], grow), }; return acc; }, {}); @@ -92,13 +117,14 @@ export default createStyles( ) => ({ root: { boxSizing: 'border-box', - flexGrow: grow ? 1 : 0, + flexGrow: getColumnFlexGrow(span, grow), order, padding: theme.fn.size({ size: gutter, sizes: theme.spacing }) / 2, marginLeft: getColumnOffset(offset, columns), - flexBasis: getColumnWidth(span, columns), + flexBasis: getColumnFlexBasis(span, columns), flexShrink: 0, - maxWidth: grow ? 'unset' : getColumnWidth(span, columns), + width: span === 'content' ? 'auto' : undefined, + maxWidth: getColumnMaxWidth(span, columns, grow), ...getBreakpointsStyles({ sizes: { xs, sm, md, lg, xl }, offsets: { xs: offsetXs, sm: offsetSm, md: offsetMd, lg: offsetLg, xl: offsetXl }, diff --git a/src/mantine-core/src/Grid/Col/Col.tsx b/src/mantine-core/src/Grid/Col/Col.tsx index 37e490df53d..6b9d82ada72 100644 --- a/src/mantine-core/src/Grid/Col/Col.tsx +++ b/src/mantine-core/src/Grid/Col/Col.tsx @@ -2,11 +2,11 @@ import React, { forwardRef } from 'react'; import { DefaultProps, useComponentDefaultProps } from '@mantine/styles'; import { Box } from '../../Box'; import { useGridContext } from '../Grid.context'; -import useStyles from './Col.styles'; +import useStyles, { ColSpan } from './Col.styles'; export interface ColProps extends DefaultProps, React.ComponentPropsWithoutRef<'div'> { /** Default col span */ - span?: number; + span?: ColSpan; /** Column left offset */ offset?: number; @@ -45,19 +45,19 @@ export interface ColProps extends DefaultProps, React.ComponentPropsWithoutRef<' offsetXl?: number; /** Col span at (min-width: theme.breakpoints.xs) */ - xs?: number; + xs?: ColSpan; /** Col span at (min-width: theme.breakpoints.sm) */ - sm?: number; + sm?: ColSpan; /** Col span at (min-width: theme.breakpoints.md) */ - md?: number; + md?: ColSpan; /** Col span at (min-width: theme.breakpoints.lg) */ - lg?: number; + lg?: ColSpan; /** Col span at (min-width: theme.breakpoints.xl) */ - xl?: number; + xl?: ColSpan; } const defaultProps: Partial = { @@ -69,7 +69,10 @@ const defaultProps: Partial = { offsetXl: 0, }; -function isValidSpan(span: number) { +function isValidSpan(span: ColSpan) { + if (span === 'auto' || span === 'content') { + return true; + } return typeof span === 'number' && span > 0 && span % 1 === 0; } diff --git a/src/mantine-core/src/Indicator/Indicator.styles.ts b/src/mantine-core/src/Indicator/Indicator.styles.ts index 0899e3dbc1f..131a7f8d486 100644 --- a/src/mantine-core/src/Indicator/Indicator.styles.ts +++ b/src/mantine-core/src/Indicator/Indicator.styles.ts @@ -107,14 +107,23 @@ export default createStyles( }, 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 ? theme.spacing.xs / 2 : 0, - paddingRight: withLabel ? theme.spacing.xs / 2 : 0, - backgroundColor: background, + 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, diff --git a/src/mantine-core/src/Input/Input.styles.ts b/src/mantine-core/src/Input/Input.styles.ts index 3a557fefc9c..34f8a6c0449 100644 --- a/src/mantine-core/src/Input/Input.styles.ts +++ b/src/mantine-core/src/Input/Input.styles.ts @@ -111,8 +111,8 @@ export default createStyles( return { wrapper: { position: 'relative', - marginTop: offsetTop ? theme.spacing.xs / 2 : undefined, - marginBottom: offsetBottom ? theme.spacing.xs / 2 : undefined, + marginTop: offsetTop ? `calc(${theme.spacing.xs}px / 2)` : undefined, + marginBottom: offsetBottom ? `calc(${theme.spacing.xs}px / 2)` : undefined, }, input: { diff --git a/src/mantine-core/src/Kbd/Kbd.styles.ts b/src/mantine-core/src/Kbd/Kbd.styles.ts index edb7437f45c..d95f84ac57d 100644 --- a/src/mantine-core/src/Kbd/Kbd.styles.ts +++ b/src/mantine-core/src/Kbd/Kbd.styles.ts @@ -8,7 +8,7 @@ export default createStyles((theme) => ({ fontWeight: 700, backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0], color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.gray[7], - padding: `3px ${theme.spacing.xs / 2}px`, + padding: `3px calc(${theme.spacing.xs}px / 2)`, borderRadius: theme.radius.sm, border: `1px solid ${ theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3] diff --git a/src/mantine-core/src/Menu/MenuDivider/MenuDivider.styles.ts b/src/mantine-core/src/Menu/MenuDivider/MenuDivider.styles.ts index e071fe70f9f..c7a4bb5cae7 100644 --- a/src/mantine-core/src/Menu/MenuDivider/MenuDivider.styles.ts +++ b/src/mantine-core/src/Menu/MenuDivider/MenuDivider.styles.ts @@ -2,7 +2,7 @@ import { createStyles } from '@mantine/styles'; export default createStyles((theme) => ({ divider: { - margin: `${theme.spacing.xs / 2}px -5px`, + margin: `calc(${theme.spacing.xs}px / 2) -5px`, borderTop: `1px solid ${ theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2] }`, diff --git a/src/mantine-core/src/Menu/MenuLabel/MenuLabel.styles.ts b/src/mantine-core/src/Menu/MenuLabel/MenuLabel.styles.ts index 35b033824d9..a2476d8c5db 100644 --- a/src/mantine-core/src/Menu/MenuLabel/MenuLabel.styles.ts +++ b/src/mantine-core/src/Menu/MenuLabel/MenuLabel.styles.ts @@ -5,7 +5,7 @@ export default createStyles((theme) => ({ color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6], fontWeight: 500, fontSize: theme.fontSizes.xs, - padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`, + padding: `calc(${theme.spacing.xs}px / 2) ${theme.spacing.sm}px`, cursor: 'default', }, })); diff --git a/src/mantine-core/src/MultiSelect/MultiSelect.styles.ts b/src/mantine-core/src/MultiSelect/MultiSelect.styles.ts index 195405e634d..22d15edb521 100644 --- a/src/mantine-core/src/MultiSelect/MultiSelect.styles.ts +++ b/src/mantine-core/src/MultiSelect/MultiSelect.styles.ts @@ -16,12 +16,12 @@ export default createStyles((theme, { size, invalid }: MultiSelectStylesParams) display: 'flex', alignItems: 'center', flexWrap: 'wrap', - marginLeft: -theme.spacing.xs / 2, + marginLeft: `calc(${-theme.spacing.xs}px / 2)`, boxSizing: 'border-box', }, value: { - margin: `${theme.spacing.xs / 2 - 2}px ${theme.spacing.xs / 2}px`, + margin: `calc(${theme.spacing.xs}px / 2 - 2px) calc(${theme.spacing.xs}px / 2)`, }, searchInput: { @@ -33,7 +33,7 @@ export default createStyles((theme, { size, invalid }: MultiSelectStylesParams) outline: 0, fontSize: theme.fn.size({ size, sizes: theme.fontSizes }), padding: 0, - marginLeft: theme.spacing.xs / 2, + marginLeft: `calc(${theme.spacing.xs}px / 2)`, appearance: 'none', color: 'inherit', lineHeight: `${theme.fn.size({ size, sizes: INPUT_SIZES }) - 2}px`, diff --git a/src/mantine-core/src/NumberInput/NumberInput.tsx b/src/mantine-core/src/NumberInput/NumberInput.tsx index f7abf6a4823..5c5439b8002 100644 --- a/src/mantine-core/src/NumberInput/NumberInput.tsx +++ b/src/mantine-core/src/NumberInput/NumberInput.tsx @@ -74,6 +74,9 @@ export interface NumberInputProps /** Parses the value from formatter, should be used with formatter at the same time */ parser?: Parser; + + /** Input type, defaults to text */ + type?: 'text' | 'number'; } const defaultFormatter: Formatter = (value) => value || ''; @@ -113,6 +116,7 @@ const defaultProps: Partial = { noClampOnBlur: false, formatter: defaultFormatter, parser: defaultParser, + type: 'text', }; export const NumberInput = forwardRef((props, ref) => { @@ -147,6 +151,7 @@ export const NumberInput = forwardRef((props parser, inputMode, unstyled, + type, ...others } = useComponentDefaultProps('NumberInput', defaultProps, props); @@ -394,11 +399,11 @@ export const NumberInput = forwardRef((props return ( ); } + +export function WithNameAttribute() { + return ( +
+ + + + + + + + + + +
+ ); +} diff --git a/src/mantine-core/src/Radio/Radio.tsx b/src/mantine-core/src/Radio/Radio.tsx index 831610332e1..d3b04621b64 100644 --- a/src/mantine-core/src/Radio/Radio.tsx +++ b/src/mantine-core/src/Radio/Radio.tsx @@ -82,6 +82,7 @@ export const Radio: RadioComponent = forwardRef((p const contextProps = ctx ? { checked: ctx.value === rest.value, + name: rest.name ?? ctx.name, onChange: ctx.onChange, } : {}; diff --git a/src/mantine-core/src/Radio/RadioGroup.context.ts b/src/mantine-core/src/Radio/RadioGroup.context.ts index 70249a99671..fa8d21a94eb 100644 --- a/src/mantine-core/src/Radio/RadioGroup.context.ts +++ b/src/mantine-core/src/Radio/RadioGroup.context.ts @@ -5,6 +5,7 @@ interface RadioGroupContextValue { size: MantineSize; value: string; onChange(event: React.ChangeEvent): void; + name: string; } const RadioGroupContext = createContext(null); diff --git a/src/mantine-core/src/Radio/RadioGroup/RadioGroup.tsx b/src/mantine-core/src/Radio/RadioGroup/RadioGroup.tsx index 4dc8be47fe9..03e458bf2fe 100644 --- a/src/mantine-core/src/Radio/RadioGroup/RadioGroup.tsx +++ b/src/mantine-core/src/Radio/RadioGroup/RadioGroup.tsx @@ -42,6 +42,9 @@ export interface RadioGroupProps /** Props spread to root element */ wrapperProps?: Record; + + /* Name attribute of radio inputs */ + name?: string; } const defaultProps: Partial = { @@ -64,6 +67,7 @@ export const RadioGroup = forwardRef( wrapperProps, unstyled, offset, + name, ...others } = useComponentDefaultProps('RadioGroup', defaultProps, props); @@ -78,7 +82,7 @@ export const RadioGroup = forwardRef( setValue(event.currentTarget.value); return ( - + ( transform: 'translate(-50%, 0)', fontSize: theme.fontSizes.sm, color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6], - marginTop: theme.spacing.xs / 2, + marginTop: `calc(${theme.spacing.xs}px / 2)`, whiteSpace: 'nowrap', }, })); diff --git a/src/mantine-core/src/Slider/Thumb/Thumb.styles.ts b/src/mantine-core/src/Slider/Thumb/Thumb.styles.ts index 0cba65bd085..3929245e1ae 100644 --- a/src/mantine-core/src/Slider/Thumb/Thumb.styles.ts +++ b/src/mantine-core/src/Slider/Thumb/Thumb.styles.ts @@ -15,7 +15,7 @@ export default createStyles((theme, { color, size, disabled, thumbSize }: ThumbS backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[9], fontSize: theme.fontSizes.xs, color: theme.white, - padding: theme.spacing.xs / 2, + padding: `calc(${theme.spacing.xs}px / 2)`, borderRadius: theme.radius.sm, whiteSpace: 'nowrap', pointerEvents: 'none', diff --git a/src/mantine-core/src/Stepper/Step/Step.styles.ts b/src/mantine-core/src/Stepper/Step/Step.styles.ts index a24286831e6..80fff6a3b61 100644 --- a/src/mantine-core/src/Stepper/Step/Step.styles.ts +++ b/src/mantine-core/src/Stepper/Step/Step.styles.ts @@ -32,7 +32,7 @@ export default createStyles( const verticalOrientationStyles = { step: { justifyContent: 'flex-start', - minHeight: `${_iconSize + theme.spacing.xl + separatorDistanceFromIcon}px`, + minHeight: `calc(${_iconSize}px + ${theme.spacing.xl}px + ${separatorDistanceFromIcon}px)`, marginTop: `${separatorDistanceFromIcon}px`, overflow: 'hidden', diff --git a/src/mantine-core/src/Stepper/Stepper.styles.ts b/src/mantine-core/src/Stepper/Stepper.styles.ts index 4eca690ca12..81546894770 100644 --- a/src/mantine-core/src/Stepper/Stepper.styles.ts +++ b/src/mantine-core/src/Stepper/Stepper.styles.ts @@ -42,8 +42,8 @@ export default createStyles( minHeight: theme.spacing.xl, marginLeft: iconPosition === 'left' ? separatorOffset : 0, marginRight: iconPosition === 'right' ? separatorOffset : 0, - marginTop: theme.spacing.xs / 2, - marginBottom: theme.spacing.xs - 2, + marginTop: `calc(${theme.spacing.xs}px / 2)`, + marginBottom: `calc(${theme.spacing.xs}px - 2px)`, }, } as const; diff --git a/src/mantine-core/src/Tabs/TabsList/TabsList.styles.ts b/src/mantine-core/src/Tabs/TabsList/TabsList.styles.ts index 9bcd9c54dc6..39387d4547f 100644 --- a/src/mantine-core/src/Tabs/TabsList/TabsList.styles.ts +++ b/src/mantine-core/src/Tabs/TabsList/TabsList.styles.ts @@ -31,7 +31,7 @@ function getVariantStyles( if (variant === 'pills') { return { - gap: theme.spacing.sm / 2, + gap: `calc(${theme.spacing.sm}px / 2)`, }; } diff --git a/src/mantine-core/src/Timeline/TimelineItem/TimelineItem.styles.ts b/src/mantine-core/src/Timeline/TimelineItem/TimelineItem.styles.ts index 7875aec78cf..f01d26601f8 100644 --- a/src/mantine-core/src/Timeline/TimelineItem/TimelineItem.styles.ts +++ b/src/mantine-core/src/Timeline/TimelineItem/TimelineItem.styles.ts @@ -93,7 +93,7 @@ export default createStyles( itemTitle: { fontWeight: 500, lineHeight: 1, - marginBottom: theme.spacing.xs / 2, + marginBottom: `calc(${theme.spacing.xs}px / 2)`, textAlign: align, }, }; diff --git a/src/mantine-core/src/Tooltip/Tooltip.styles.ts b/src/mantine-core/src/Tooltip/Tooltip.styles.ts index 017db77c0e3..700f780b7e1 100644 --- a/src/mantine-core/src/Tooltip/Tooltip.styles.ts +++ b/src/mantine-core/src/Tooltip/Tooltip.styles.ts @@ -36,7 +36,7 @@ export default createStyles((theme, { color, radius, width, multiline }: Tooltip lineHeight: theme.lineHeight, fontSize: theme.fontSizes.sm, borderRadius: theme.fn.radius(radius), - padding: `${theme.spacing.xs / 2}px ${theme.spacing.xs}px`, + padding: `calc(${theme.spacing.xs}px / 2) ${theme.spacing.xs}px`, position: 'absolute', whiteSpace: multiline ? 'unset' : 'nowrap', pointerEvents: 'none', diff --git a/src/mantine-core/src/TransferList/RenderList/RenderList.styles.ts b/src/mantine-core/src/TransferList/RenderList/RenderList.styles.ts index 52ecd1c23d2..1f88e659766 100644 --- a/src/mantine-core/src/TransferList/RenderList/RenderList.styles.ts +++ b/src/mantine-core/src/TransferList/RenderList/RenderList.styles.ts @@ -19,16 +19,16 @@ export default createStyles((theme, { reversed, native, radius }: RenderListStyl display: 'block', width: `calc(100% - ${ITEM_PADDING * 2}px)`, padding: ITEM_PADDING, - marginLeft: theme.spacing.sm - ITEM_PADDING, - marginRight: theme.spacing.sm - ITEM_PADDING, + marginLeft: `calc(${theme.spacing.sm}px - ${ITEM_PADDING}px)`, + marginRight: `calc(${theme.spacing.sm}px - ${ITEM_PADDING}px)`, borderRadius: theme.fn.radius(radius), '&:first-of-type': { - marginTop: theme.spacing.sm - ITEM_PADDING, + marginTop: `calc(${theme.spacing.sm}px - ${ITEM_PADDING}px)`, }, '&:last-of-type': { - marginBottom: theme.spacing.sm - ITEM_PADDING, + marginBottom: `calc(${theme.spacing.sm}px - ${ITEM_PADDING}px)`, }, }, diff --git a/src/mantine-core/src/TypographyStylesProvider/TypographyStylesProvider.styles.ts b/src/mantine-core/src/TypographyStylesProvider/TypographyStylesProvider.styles.ts index 09c0ffab3e7..aa1a0770e19 100644 --- a/src/mantine-core/src/TypographyStylesProvider/TypographyStylesProvider.styles.ts +++ b/src/mantine-core/src/TypographyStylesProvider/TypographyStylesProvider.styles.ts @@ -9,7 +9,7 @@ export default createStyles((theme) => { fontWeight: values.fontWeight || theme.headings.fontWeight, marginTop: typeof values.lineHeight === 'number' - ? theme.spacing.xl * values.lineHeight + ? `calc(${theme.spacing.xl}px * ${values.lineHeight})` : theme.spacing.xl, marginBottom: theme.spacing.sm, ...values, @@ -99,7 +99,7 @@ export default createStyles((theme) => { '& code': { lineHeight: theme.lineHeight, - padding: `1px ${theme.spacing.xs / 1}px`, + padding: `1px calc(${theme.spacing.xs}px / 1)`, borderRadius: theme.radius.sm, color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black, backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[0], @@ -112,7 +112,7 @@ export default createStyles((theme) => { '& ul, & ol': { marginBottom: theme.spacing.md, - paddingLeft: theme.spacing.lg * 2, + paddingLeft: `calc(${theme.spacing.lg}px * 2)`, '& li': { marginTop: theme.spacing.xs, diff --git a/src/mantine-dates/src/components/Month/Month.styles.ts b/src/mantine-dates/src/components/Month/Month.styles.ts index ef84b7a0016..02712087606 100644 --- a/src/mantine-dates/src/components/Month/Month.styles.ts +++ b/src/mantine-dates/src/components/Month/Month.styles.ts @@ -26,7 +26,7 @@ export default createStyles((theme, { fullWidth }: MonthStylesParams) => ({ boxSizing: 'border-box', padding: 0, fontWeight: 'normal', - paddingBottom: theme.spacing.xs / 2, + paddingBottom: `calc(${theme.spacing.xs}px / 2)`, textAlign: 'center', cursor: 'default', userSelect: 'none', diff --git a/src/mantine-demos/src/components/Demo/CodeDemo/CodeDemo.styles.ts b/src/mantine-demos/src/components/Demo/CodeDemo/CodeDemo.styles.ts index 04447c09b1f..f2319f1906d 100644 --- a/src/mantine-demos/src/components/Demo/CodeDemo/CodeDemo.styles.ts +++ b/src/mantine-demos/src/components/Demo/CodeDemo/CodeDemo.styles.ts @@ -38,7 +38,7 @@ export default createStyles((theme: MantineTheme, { radius }: CodeDemoStylesPara }, controls: { - marginTop: theme.spacing.xs - 1, + marginTop: `calc(${theme.spacing.xs}px - 1px)`, alignItems: 'flex-end', }, diff --git a/src/mantine-demos/src/demos/core/Grid/Grid.demo.auto.tsx b/src/mantine-demos/src/demos/core/Grid/Grid.demo.auto.tsx new file mode 100644 index 00000000000..1c1241472d0 --- /dev/null +++ b/src/mantine-demos/src/demos/core/Grid/Grid.demo.auto.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Grid } from '@mantine/core'; +import { ColWrapper as Col } from './_col-wrapper'; + +const code = ` +import { Grid } from '@mantine/core'; + +function Demo() { + return ( + + span=auto + span=6 + span=auto + + ); +} +`; + +function Demo() { + return ( + + 1 + 2 + 3 + + ); +} + +export const auto: MantineDemo = { + type: 'demo', + code, + component: Demo, +}; diff --git a/src/mantine-demos/src/demos/core/Grid/Grid.demo.content.tsx b/src/mantine-demos/src/demos/core/Grid/Grid.demo.content.tsx new file mode 100644 index 00000000000..3c376083c74 --- /dev/null +++ b/src/mantine-demos/src/demos/core/Grid/Grid.demo.content.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Grid } from '@mantine/core'; +import { ColWrapper as Col } from './_col-wrapper'; + +const code = ` +import { Grid } from '@mantine/core'; + +function Demo() { + return ( + + fit content + 2 + + ); +} +`; + +function Demo() { + return ( + + fit content + 2 + + ); +} + +export const content: MantineDemo = { + type: 'demo', + code, + component: Demo, +}; diff --git a/src/mantine-demos/src/demos/core/Grid/index.ts b/src/mantine-demos/src/demos/core/Grid/index.ts index df68478af02..171338570e5 100644 --- a/src/mantine-demos/src/demos/core/Grid/index.ts +++ b/src/mantine-demos/src/demos/core/Grid/index.ts @@ -6,3 +6,5 @@ export { rows } from './Grid.demo.rows'; export { flexConfigurator } from './Grid.demo.flexConfigurator'; export { responsive } from './Grid.demo.responsive'; export { columns } from './Grid.demo.columns'; +export { auto } from './Grid.demo.auto'; +export { content } from './Grid.demo.content'; diff --git a/src/mantine-demos/src/demos/core/ScrollArea/ScrollArea.demo.stylesApi.tsx b/src/mantine-demos/src/demos/core/ScrollArea/ScrollArea.demo.stylesApi.tsx new file mode 100644 index 00000000000..99d66f7d038 --- /dev/null +++ b/src/mantine-demos/src/demos/core/ScrollArea/ScrollArea.demo.stylesApi.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Group, ScrollArea } from '@mantine/core'; +import { Content } from './_content'; + +const code = ` +import { ScrollArea } from '@mantine/core'; + +function Demo() { + return ( + + ({ + scrollbar: { + '&, &:hover': { + background: + theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], + }, + + '&[data-orientation="vertical"] .mantine-ScrollArea-thumb': { + backgroundColor: theme.colors.red[6], + }, + + '&[data-orientation="horizontal"] .mantine-ScrollArea-thumb': { + backgroundColor: theme.colors.blue[6], + }, + }, + + corner: { + opacity: 1, + background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], + }, + })} + > +
+ {/* ...content */} +
+
+
+ ); +} +`; + +function Demo() { + return ( + + ({ + scrollbar: { + '&, &:hover': { + background: + theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], + }, + + '&[data-orientation="vertical"] .mantine-ScrollArea-thumb': { + backgroundColor: theme.colors.red[6], + }, + + '&[data-orientation="horizontal"] .mantine-ScrollArea-thumb': { + backgroundColor: theme.colors.blue[6], + }, + }, + + corner: { + opacity: 1, + background: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], + }, + })} + > +
+ +
+
+
+ ); +} + +export const stylesApi: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-demos/src/demos/core/ScrollArea/index.ts b/src/mantine-demos/src/demos/core/ScrollArea/index.ts index 33acf40721b..4e0e92bb0a1 100644 --- a/src/mantine-demos/src/demos/core/ScrollArea/index.ts +++ b/src/mantine-demos/src/demos/core/ScrollArea/index.ts @@ -3,3 +3,4 @@ export { horizontal } from './ScrollArea.demo.horizontal'; export { scrollTo } from './ScrollArea.demo.scrollTo'; export { scrollPosition } from './ScrollArea.demo.scrollPosition'; export { maxHeight } from './ScrollArea.demo.maxHeight'; +export { stylesApi } from './ScrollArea.demo.stylesApi'; diff --git a/src/mantine-demos/src/demos/hooks/index.ts b/src/mantine-demos/src/demos/hooks/index.ts index b2f08745275..2b5ee82bfc8 100644 --- a/src/mantine-demos/src/demos/hooks/index.ts +++ b/src/mantine-demos/src/demos/hooks/index.ts @@ -47,3 +47,4 @@ export { useValidatedStateDemo } from './use-validated-state.demo.usage'; export { useViewportSizeDemo } from './use-viewport-size.demo'; export { useWindowScrollDemo } from './use-window-scroll.demo.usage'; export { useTextSelectionUsage } from './use-text-selection.demo.usage'; +export { usePreviousUsage } from './use-previous.demo.usage'; diff --git a/src/mantine-demos/src/demos/hooks/use-previous.demo.usage.tsx b/src/mantine-demos/src/demos/hooks/use-previous.demo.usage.tsx new file mode 100644 index 00000000000..d56557cdb8e --- /dev/null +++ b/src/mantine-demos/src/demos/hooks/use-previous.demo.usage.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { TextInput, Text } from '@mantine/core'; +import { usePrevious, useInputState } from '@mantine/hooks'; + +const code = ` +import { TextInput, Text } from '@mantine/core'; +import { usePrevious, useInputState } from '@mantine/hooks'; + +function Demo() { + const [value, setValue] = useInputState(''); + const previousValue = usePrevious(value); + + return ( +
+ + Current value: {value} + Previous value: {previousValue} +
+ ); +} +`; + +function Demo() { + const [value, setValue] = useInputState(''); + const previousValue = usePrevious(value); + + return ( +
+ + Current value: {value} + Previous value: {previousValue} +
+ ); +} + +export const usePreviousUsage: MantineDemo = { + type: 'demo', + component: Demo, + code, +}; diff --git a/src/mantine-form/src/FormProvider/FormProvider.tsx b/src/mantine-form/src/FormProvider/FormProvider.tsx new file mode 100644 index 00000000000..3655d58d17c --- /dev/null +++ b/src/mantine-form/src/FormProvider/FormProvider.tsx @@ -0,0 +1,33 @@ +import React, { createContext, useContext } from 'react'; +import { useForm } from '../use-form'; +import { UseFormReturnType, UseForm } from '../types'; + +export interface FormProviderProps
{ + form: Form; + children: React.ReactNode; +} + +export function createFormContext() { + type Form = UseFormReturnType; + + const FormContext = createContext(null); + + function FormProvider({ form, children }: FormProviderProps) { + return {children}; + } + + function useFormContext() { + const ctx = useContext(FormContext); + if (!ctx) { + throw new Error('useFormContext was called outside of FormProvider context'); + } + + return ctx; + } + + return [FormProvider, useFormContext, useForm] as [ + React.FC>, + () => Form, + UseForm + ]; +} diff --git a/src/mantine-form/src/index.ts b/src/mantine-form/src/index.ts index 1af9b6295ba..cc0fade0cf8 100644 --- a/src/mantine-form/src/index.ts +++ b/src/mantine-form/src/index.ts @@ -1,4 +1,5 @@ export { useForm } from './use-form'; +export { createFormContext } from './FormProvider/FormProvider'; export { zodResolver } from './resolvers/zod-resolver/zod-resolver'; export { yupResolver } from './resolvers/yup-resolver/yup-resolver'; diff --git a/src/mantine-form/src/resolvers/test-resolver.ts b/src/mantine-form/src/resolvers/test-resolver.ts deleted file mode 100644 index bfcf3f6aa48..00000000000 --- a/src/mantine-form/src/resolvers/test-resolver.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { renderHook, act } from '@testing-library/react'; -import { FormErrors, useForm } from '../index'; - -export const RESOLVER_ERROR_MESSAGES = { - name: 'test-name-error', - age: 'test-age-error', - fruitName: 'test-fruit-name-error', - fruitStock: 'test-fruit-stock-error', - nestedArray: 'test-nested-array', -}; - -const values = { - name: '1', - age: 5, - nested: { - object: '', - array: [{ item: 1 }, { item: 2 }, { item: 3 }], - }, - fruits: [ - { name: 'banana', stock: -3 }, - { name: '1', stock: 5 }, - ], -}; - -const validValues = { - name: 'test-name', - age: 50, - nested: { - object: 'test', - array: [{ item: 10 }, { item: 20 }, { item: 30 }], - }, - fruits: [ - { name: 'banana', stock: 10 }, - { name: 'orange', stock: 10 }, - ], -}; - -const initialValues = { - name: '1', - age: 5, - nested: { - object: '', - array: [{ item: 1 }, { item: 2 }, { item: 3 }], - }, - fruits: [ - { name: 'banana', stock: -3 }, - { name: '1', stock: 5 }, - ], -}; - -export function testResolver(schema: (values: Record) => FormErrors) { - // Resolver itself - it('validates given values', () => { - expect(schema(values)).toStrictEqual({ - name: RESOLVER_ERROR_MESSAGES.name, - age: RESOLVER_ERROR_MESSAGES.age, - 'fruits.0.stock': RESOLVER_ERROR_MESSAGES.fruitStock, - 'fruits.1.name': RESOLVER_ERROR_MESSAGES.fruitName, - 'nested.array.0.item': RESOLVER_ERROR_MESSAGES.nestedArray, - 'nested.array.1.item': RESOLVER_ERROR_MESSAGES.nestedArray, - }); - }); - - it('returns empty object if all fields are valid', () => { - expect(schema(validValues)).toStrictEqual({}); - }); - - // use-form integration - it('validates regular values', () => { - const hook = renderHook(() => useForm({ validate: schema, initialValues })); - - expect(hook.result.current.errors).toStrictEqual({}); - - act(() => { - const results = hook.result.current.validate(); - expect(results).toStrictEqual({ - hasErrors: true, - errors: { - name: RESOLVER_ERROR_MESSAGES.name, - age: RESOLVER_ERROR_MESSAGES.age, - 'fruits.0.stock': RESOLVER_ERROR_MESSAGES.fruitStock, - 'fruits.1.name': RESOLVER_ERROR_MESSAGES.fruitName, - 'nested.array.0.item': RESOLVER_ERROR_MESSAGES.nestedArray, - 'nested.array.1.item': RESOLVER_ERROR_MESSAGES.nestedArray, - }, - }); - }); - - expect(hook.result.current.errors).toStrictEqual({ - name: RESOLVER_ERROR_MESSAGES.name, - age: RESOLVER_ERROR_MESSAGES.age, - 'fruits.0.stock': RESOLVER_ERROR_MESSAGES.fruitStock, - 'fruits.1.name': RESOLVER_ERROR_MESSAGES.fruitName, - 'nested.array.0.item': RESOLVER_ERROR_MESSAGES.nestedArray, - 'nested.array.1.item': RESOLVER_ERROR_MESSAGES.nestedArray, - }); - }); - - it('validates single field with validateField handler', () => { - const hook = renderHook(() => useForm({ validate: schema, initialValues })); - - expect(hook.result.current.errors).toStrictEqual({}); - - act(() => { - const results = hook.result.current.validateField('name'); - expect(results).toStrictEqual({ hasError: true, error: RESOLVER_ERROR_MESSAGES.name }); - }); - - expect(hook.result.current.errors).toStrictEqual({ name: RESOLVER_ERROR_MESSAGES.name }); - }); -} diff --git a/src/mantine-form/src/resolvers/zod-resolver/zod-resolver.test.ts b/src/mantine-form/src/resolvers/zod-resolver/zod-resolver.test.ts deleted file mode 100644 index 7a7840cd67c..00000000000 --- a/src/mantine-form/src/resolvers/zod-resolver/zod-resolver.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { z } from 'zod'; -import { zodResolver } from './zod-resolver'; -import { testResolver, RESOLVER_ERROR_MESSAGES } from '../test-resolver'; - -describe('@mantine/form zod resolver', () => { - testResolver( - zodResolver( - z.object({ - name: z.string().min(2, { message: RESOLVER_ERROR_MESSAGES.name }), - age: z.number().min(10, { message: RESOLVER_ERROR_MESSAGES.age }), - nested: z.object({ - object: z.string(), - array: z.array( - z.object({ item: z.number().min(3, { message: RESOLVER_ERROR_MESSAGES.nestedArray }) }) - ), - }), - fruits: z.array( - z.object({ - name: z.string().min(2, { message: RESOLVER_ERROR_MESSAGES.fruitName }), - stock: z.number().min(0, { message: RESOLVER_ERROR_MESSAGES.fruitStock }), - }) - ), - }) - ) - ); -}); diff --git a/src/mantine-form/src/stories/Form.context.story.tsx b/src/mantine-form/src/stories/Form.context.story.tsx new file mode 100644 index 00000000000..d5c2c1ed015 --- /dev/null +++ b/src/mantine-form/src/stories/Form.context.story.tsx @@ -0,0 +1,35 @@ +import { TextInput } from '@mantine/core'; +import React from 'react'; +import { createFormContext } from '../index'; + +export default { title: 'Form' }; + +interface FormValues { + a: number; + b: string; +} + +const [FormProvider, useFormContext, useForm] = createFormContext(); + +function CustomField() { + const form = useFormContext(); + return ; +} + +export function Context() { + const form = useForm({ + initialValues: { + a: 0, + b: '', + }, + }); + + return ( +
+ + + {JSON.stringify(form.values)} + +
+ ); +} diff --git a/src/mantine-form/src/stories/Form.dirty.story.tsx b/src/mantine-form/src/stories/Form.dirty.story.tsx index f5b62d49f31..01971092828 100644 --- a/src/mantine-form/src/stories/Form.dirty.story.tsx +++ b/src/mantine-form/src/stories/Form.dirty.story.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ActionIcon, Code, Group, MantineProvider, TextInput, Text, Button } from '@mantine/core'; import { IconTrash } from '@tabler/icons'; -import { useForm } from '../index'; +import { useForm } from '../use-form'; export default { title: 'Form' }; diff --git a/src/mantine-form/src/stories/Form.rerendering2.story.tsx b/src/mantine-form/src/stories/Form.rerendering2.story.tsx new file mode 100644 index 00000000000..fca09486af3 --- /dev/null +++ b/src/mantine-form/src/stories/Form.rerendering2.story.tsx @@ -0,0 +1,51 @@ +/* eslint-disable no-spaced-func */ +import React, { useCallback, useState, useRef, memo } from 'react'; +import { TextInput, TextInputProps, MultiSelect, MultiSelectProps } from '@mantine/core'; + +export default { title: 'Form' }; + +const TestInput = memo((props: TextInputProps) => { + console.log(`Rerender ${props.name}`); + return ; +}); + +const TestMultiSelect = memo((props: MultiSelectProps) => { + console.log(`Rerender ${props.name}`); + return ; +}); + +function useTestForm() { + const [state, setState] = useState({ first: '', second: [] }); + const handleChange = useCallback((path: string, value: any) => { + setState((current) => ({ ...current, [path]: value })); + }, []); + + const callbacks = useRef void>>({}); + + const getInputProps = (path: string) => { + if (!(path in callbacks.current)) { + callbacks.current[path] = (event: any) => + handleChange(path, 'currentTarget' in event ? event.currentTarget.value : event); + } + + return { + value: state[path], + onChange: callbacks.current[path], + }; + }; + + return { getInputProps, values: state }; +} + +const data = ['1', '2', '3']; + +export function RerenderingTest() { + const form = useTestForm(); + + return ( +
+ + +
+ ); +} diff --git a/src/mantine-form/src/types.ts b/src/mantine-form/src/types.ts index 15049f073ad..253318f05aa 100644 --- a/src/mantine-form/src/types.ts +++ b/src/mantine-form/src/types.ts @@ -137,3 +137,7 @@ export interface UseFormReturnType { resetDirty: ResetDirty; isValid: IsValid; } + +export type UseForm> = ( + input?: UseFormInput +) => UseFormReturnType; diff --git a/src/mantine-hooks/src/index.ts b/src/mantine-hooks/src/index.ts index 17605db685e..40c6e7732fb 100644 --- a/src/mantine-hooks/src/index.ts +++ b/src/mantine-hooks/src/index.ts @@ -18,6 +18,7 @@ export { useInterval } from './use-interval/use-interval'; export { useIsomorphicEffect } from './use-isomorphic-effect/use-isomorphic-effect'; export { useListState } from './use-list-state/use-list-state'; export { useLocalStorage } from './use-local-storage/use-local-storage'; +export { useSessionStorage } from './use-session-storage/use-session-storage'; export { useMediaQuery } from './use-media-query/use-media-query'; export { useMergedRef, mergeRefs } from './use-merged-ref/use-merged-ref'; export { useMouse } from './use-mouse/use-mouse'; @@ -51,6 +52,7 @@ export { useFocusWithin } from './use-focus-within/use-focus-within'; export { useNetwork } from './use-network/use-network'; export { useTimeout } from './use-timeout/use-timeout'; export { useTextSelection } from './use-text-selection/use-text-selection'; +export { usePrevious } from './use-previous/use-previous'; export type { UseMovePosition } from './use-move/use-move'; export type { OS } from './use-os/use-os'; diff --git a/src/mantine-hooks/src/use-local-storage/create-storage.ts b/src/mantine-hooks/src/use-local-storage/create-storage.ts new file mode 100644 index 00000000000..6463f65280a --- /dev/null +++ b/src/mantine-hooks/src/use-local-storage/create-storage.ts @@ -0,0 +1,110 @@ +import { useState, useCallback, useEffect } from 'react'; +import { useWindowEvent } from '../use-window-event/use-window-event'; + +export type StorageType = 'localStorage' | 'sessionStorage'; + +export interface IStorageProperties { + /** Storage key */ + key: string; + + /** Default value that will be set if value is not found in storage */ + defaultValue?: T; + + /** If set to true, value will be update is useEffect after mount */ + getInitialValueInEffect?: boolean; + + /** Function to serialize value into string to be save in storage */ + serialize?(value: T): string; + + /** Function to deserialize string value from storage to value */ + deserialize?(value: string): T; +} + +function serializeJSON(value: T, hookName: string) { + try { + return JSON.stringify(value); + } catch (error) { + throw new Error(`@mantine/hooks ${hookName}: Failed to serialize the value`); + } +} + +function deserializeJSON(value: string) { + try { + return JSON.parse(value); + } catch { + return value; + } +} + +export function createStorage(type: StorageType, hookName: string) { + const eventName = type === 'localStorage' ? 'mantine-local-storage' : 'mantine-session-storage'; + + return function useStorage({ + key, + defaultValue = undefined, + getInitialValueInEffect = true, + deserialize = deserializeJSON, + serialize = (value: T) => serializeJSON(value, hookName), + }: IStorageProperties) { + const readStorageValue = useCallback( + (skipStorage?: boolean): T => { + if (typeof window === 'undefined' || !(type in window) || skipStorage) { + return (defaultValue ?? '') as T; + } + + const storageValue = window[type].getItem(key); + + return storageValue !== null ? deserialize(storageValue) : ((defaultValue ?? '') as T); + }, + [key, defaultValue] + ); + + const [value, setValue] = useState(readStorageValue(getInitialValueInEffect)); + + const setStorageValue = useCallback( + (val: T | ((prevState: T) => T)) => { + if (val instanceof Function) { + setValue((current) => { + const result = val(current); + window[type].setItem(key, serialize(result)); + window.dispatchEvent( + new CustomEvent(eventName, { detail: { key, value: val(current) } }) + ); + return result; + }); + } else { + window[type].setItem(key, serialize(val)); + window.dispatchEvent(new CustomEvent(eventName, { detail: { key, value: val } })); + setValue(val); + } + }, + [key] + ); + + useWindowEvent('storage', (event) => { + if (event.storageArea === window[type] && event.key === key) { + setValue(deserialize(event.newValue ?? undefined)); + } + }); + + useWindowEvent(eventName, (event) => { + if (event.detail.key === key) { + setValue(event.detail.value); + } + }); + + useEffect(() => { + if (defaultValue !== undefined && value === undefined) { + setStorageValue(defaultValue); + } + }, [defaultValue, value, setStorageValue]); + + useEffect(() => { + if (getInitialValueInEffect) { + setValue(readStorageValue()); + } + }, []); + + return [value === undefined ? defaultValue : value, setStorageValue] as const; + }; +} diff --git a/src/mantine-hooks/src/use-local-storage/use-local-storage.ts b/src/mantine-hooks/src/use-local-storage/use-local-storage.ts index 699c72ce27f..c3153f7a9a0 100644 --- a/src/mantine-hooks/src/use-local-storage/use-local-storage.ts +++ b/src/mantine-hooks/src/use-local-storage/use-local-storage.ts @@ -1,106 +1,5 @@ -import { useState, useCallback, useEffect } from 'react'; -import { useWindowEvent } from '../use-window-event/use-window-event'; +import { createStorage, IStorageProperties } from './create-storage'; -interface UseLocalStorage { - /** Local storage key */ - key: string; - - /** Default value that will be set if value is not found in local storage */ - defaultValue?: T; - - /** If set to true, value will be update is useEffect after mount */ - getInitialValueInEffect?: boolean; - - /** Function to serialize value into string to be save in local storage */ - serialize?(value: T): string; - - /** Function to deserialize string value from local storage to value */ - deserialize?(value: string): T; -} - -function serializeJSON(value: T) { - try { - return JSON.stringify(value); - } catch (error) { - throw new Error('@mantine/hooks use-local-storage: Failed to serialize the value'); - } -} - -function deserializeJSON(value: string) { - try { - return JSON.parse(value); - } catch { - return value; - } -} - -export function useLocalStorage({ - key, - defaultValue = undefined, - getInitialValueInEffect = true, - deserialize = deserializeJSON, - serialize = serializeJSON, -}: UseLocalStorage) { - const readLocalStorageValue = useCallback( - (skipStorage?: boolean): T => { - if (typeof window === 'undefined' || !('localStorage' in window) || skipStorage) { - return (defaultValue ?? '') as T; - } - - const storageValue = window.localStorage.getItem(key); - - return storageValue !== null ? deserialize(storageValue) : ((defaultValue ?? '') as T); - }, - [key, defaultValue] - ); - - const [value, setValue] = useState(readLocalStorageValue(getInitialValueInEffect)); - - const setLocalStorageValue = useCallback( - (val: T | ((prevState: T) => T)) => { - if (val instanceof Function) { - setValue((current) => { - const result = val(current); - window.localStorage.setItem(key, serialize(result)); - window.dispatchEvent( - new CustomEvent('mantine-local-storage', { detail: { key, value: val(current) } }) - ); - return result; - }); - } else { - window.localStorage.setItem(key, serialize(val)); - window.dispatchEvent( - new CustomEvent('mantine-local-storage', { detail: { key, value: val } }) - ); - setValue(val); - } - }, - [key] - ); - - useWindowEvent('storage', (event) => { - if (event.storageArea === window.localStorage && event.key === key) { - setValue(deserialize(event.newValue ?? undefined)); - } - }); - - useWindowEvent('mantine-local-storage', (event) => { - if (event.detail.key === key) { - setValue(event.detail.value); - } - }); - - useEffect(() => { - if (defaultValue !== undefined && value === undefined) { - setLocalStorageValue(defaultValue); - } - }, [defaultValue, value, setLocalStorageValue]); - - useEffect(() => { - if (getInitialValueInEffect) { - setValue(readLocalStorageValue()); - } - }, []); - - return [value === undefined ? defaultValue : value, setLocalStorageValue] as const; +export function useLocalStorage(props: IStorageProperties) { + return createStorage('localStorage', 'use-local-storage')(props); } diff --git a/src/mantine-hooks/src/use-network/use-network.ts b/src/mantine-hooks/src/use-network/use-network.ts index a1dbcd6391b..c6ba7ba0c1a 100644 --- a/src/mantine-hooks/src/use-network/use-network.ts +++ b/src/mantine-hooks/src/use-network/use-network.ts @@ -46,10 +46,12 @@ export function useNetwork() { useWindowEvent('offline', () => setStatus({ online: false, ...getConnection() })); useEffect(() => { - if (navigator.connection) { + const _navigator = navigator as any; + + if (_navigator.connection) { setStatus({ online: true, ...getConnection() }); - navigator.connection.addEventListener('change', handleConnectionChange); - return () => navigator.connection.removeEventListener('change', handleConnectionChange); + _navigator.connection.addEventListener('change', handleConnectionChange); + return () => _navigator.connection.removeEventListener('change', handleConnectionChange); } return undefined; diff --git a/src/mantine-hooks/src/use-previous/use-previous.test.ts b/src/mantine-hooks/src/use-previous/use-previous.test.ts new file mode 100644 index 00000000000..f1735df190c --- /dev/null +++ b/src/mantine-hooks/src/use-previous/use-previous.test.ts @@ -0,0 +1,19 @@ +import { renderHook } from '@testing-library/react'; +import { usePrevious } from './use-previous'; + +describe('@mantine/hooks/use-previous', () => { + it('returns undefined on intial render', () => { + const hook = renderHook(() => usePrevious(1)); + expect(hook.result.current).toBeUndefined(); + }); + + it('returns the previous value after update', () => { + const hook = renderHook(({ state }) => usePrevious(state), { initialProps: { state: 1 } }); + + hook.rerender({ state: 2 }); + expect(hook.result.current).toBe(1); + + hook.rerender({ state: 4 }); + expect(hook.result.current).toBe(2); + }); +}); diff --git a/src/mantine-hooks/src/use-previous/use-previous.ts b/src/mantine-hooks/src/use-previous/use-previous.ts new file mode 100644 index 00000000000..b6d9f759830 --- /dev/null +++ b/src/mantine-hooks/src/use-previous/use-previous.ts @@ -0,0 +1,11 @@ +import { useEffect, useRef } from 'react'; + +export function usePrevious(value: T): T | undefined { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} diff --git a/src/mantine-hooks/src/use-resize-observer/use-resize-observer.ts b/src/mantine-hooks/src/use-resize-observer/use-resize-observer.ts index ce8970a98b4..454c8ae7709 100644 --- a/src/mantine-hooks/src/use-resize-observer/use-resize-observer.ts +++ b/src/mantine-hooks/src/use-resize-observer/use-resize-observer.ts @@ -13,8 +13,6 @@ const defaultState: ObserverRect = { right: 0, }; -const browser = typeof window !== 'undefined'; - export function useResizeObserver() { const frameID = useRef(0); const ref = useRef(null); @@ -23,7 +21,7 @@ export function useResizeObserver() { const observer = useMemo( () => - browser + typeof window !== 'undefined' ? new ResizeObserver((entries: any) => { const entry = entries[0]; diff --git a/src/mantine-hooks/src/use-session-storage/use-session-storage.story.tsx b/src/mantine-hooks/src/use-session-storage/use-session-storage.story.tsx new file mode 100644 index 00000000000..2a28bf9e243 --- /dev/null +++ b/src/mantine-hooks/src/use-session-storage/use-session-storage.story.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { useSessionStorage } from './use-session-storage'; + +export default { + title: 'Hooks/use-session-storage', +}; + +export function Usage() { + const [value, setValue] = useSessionStorage({ + key: '@mantine/sessionStorage/val', + defaultValue: 'Value persists through reloads.', + }); + + return ( +
+ setValue(e.target.value)} /> +
+ ); +} + +export function SerializeJson() { + const [value, setValue] = useSessionStorage<{ mantine: string }>({ + key: '@mantine/sessionStorage/val', + defaultValue: { mantine: 'is awesome' }, + }); + + const [value2, setValue2] = useSessionStorage<{ mantine: string }>({ + key: '@mantine/sessionStorage/val', + defaultValue: { mantine: 'is awesome' }, + }); + + const [value3, setValue3] = useSessionStorage<{ mantine: string }>({ + key: '@mantine/sessionStorage/another-value', + defaultValue: { mantine: 'is awesome' }, + }); + + return ( +
+ setValue({ mantine: event.target.value })} + /> + setValue2({ mantine: event.target.value })} + /> + setValue3({ mantine: event.target.value })} + /> +
+ ); +} + +export function SerializeBoolean() { + const [value, setValue] = useSessionStorage({ + key: '@mantine/sessionStorage/val', + defaultValue: true, + }); + + return ( +
+ { + setValue(event.currentTarget.checked); + }} + /> +
+ ); +} + +export function MultipleHooks() { + const [value, setValue] = useSessionStorage({ + key: 'some-value', + defaultValue: '', + }); + + const [value2] = useSessionStorage({ + key: 'some-value', + defaultValue: '', + }); + + return ( +
+ setValue(event.currentTarget.value)} /> + +
+ ); +} diff --git a/src/mantine-hooks/src/use-session-storage/use-session-storage.ts b/src/mantine-hooks/src/use-session-storage/use-session-storage.ts new file mode 100644 index 00000000000..ea00f12dd08 --- /dev/null +++ b/src/mantine-hooks/src/use-session-storage/use-session-storage.ts @@ -0,0 +1,5 @@ +import { createStorage, IStorageProperties } from '../use-local-storage/create-storage'; + +export function useSessionStorage(props: IStorageProperties) { + return createStorage('sessionStorage', 'use-session-storage')(props); +} diff --git a/src/mantine-labs/src/TagInput/TagInput.styles.ts b/src/mantine-labs/src/TagInput/TagInput.styles.ts index 7fe9e7c9af9..7e63c41ee7f 100644 --- a/src/mantine-labs/src/TagInput/TagInput.styles.ts +++ b/src/mantine-labs/src/TagInput/TagInput.styles.ts @@ -23,14 +23,14 @@ export default createStyles((theme, { size, invalid }: TagInputStyles) => ({ display: 'flex', alignItems: 'center', flexWrap: 'wrap', - marginLeft: -theme.spacing.xs / 2, - paddingTop: theme.spacing.xs / 2 - 2, - paddingBottom: theme.spacing.xs / 2 - 2, + marginLeft: `calc(${-theme.spacing.xs}px / 2)`, + paddingTop: `calc(${theme.spacing.xs}px / 2 - 2px)`, + paddingBottom: `calc(${theme.spacing.xs}px / 2 - 2px)`, boxSizing: 'border-box', }, value: { - margin: `${theme.spacing.xs / 2 - 2}px ${theme.spacing.xs / 2}px`, + margin: `calc(${theme.spacing.xs}px / 2 - 2px) calc(${theme.spacing.xs}px / 2)`, }, tagInput: { @@ -40,7 +40,7 @@ export default createStyles((theme, { size, invalid }: TagInputStyles) => ({ outline: 0, fontSize: theme.fn.size({ size, sizes: theme.fontSizes }), padding: 0, - margin: theme.spacing.xs / 2, + margin: `calc(${theme.spacing.xs}px / 2)`, appearance: 'none', color: 'inherit', diff --git a/src/mantine-rte/src/components/RichTextEditor/RichTextEditor.styles.ts b/src/mantine-rte/src/components/RichTextEditor/RichTextEditor.styles.ts index 7c3c7048353..e08611f58f0 100644 --- a/src/mantine-rte/src/components/RichTextEditor/RichTextEditor.styles.ts +++ b/src/mantine-rte/src/components/RichTextEditor/RichTextEditor.styles.ts @@ -230,38 +230,38 @@ export default createStyles( '& ol, & ul': { marginTop: theme.spacing.sm, - paddingLeft: theme.spacing.md * 2, + paddingLeft: `calc(${theme.spacing.md}px * 2)`, listStylePosition: 'outside', }, '& h1': { fontSize: theme.headings.sizes.h1.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h1.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h1.lineHeight})`, }, '& h2': { fontSize: theme.headings.sizes.h2.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h2.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h2.lineHeight})`, }, '& h3': { fontSize: theme.headings.sizes.h3.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h3.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h3.lineHeight})`, }, '& h4': { fontSize: theme.headings.sizes.h4.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h4.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h4.lineHeight})`, }, '& h5': { fontSize: theme.headings.sizes.h5.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h5.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h5.lineHeight})`, }, '& h6': { fontSize: theme.headings.sizes.h6.fontSize, - marginBottom: theme.spacing.sm * (theme.headings.sizes.h6.lineHeight as number), + marginBottom: `calc(${theme.spacing.sm}px * ${theme.headings.sizes.h6.lineHeight})`, }, '& p': { @@ -296,7 +296,7 @@ export default createStyles( ), fontFamily: theme.fontFamilyMonospace, fontSize: theme.fontSizes.xs, - padding: `2px ${theme.spacing.xs / 2}px`, + padding: `2px calc(${theme.spacing.xs}px / 2)`, }, '& blockquote': { diff --git a/src/mantine-rte/src/components/Toolbar/Toolbar.styles.ts b/src/mantine-rte/src/components/Toolbar/Toolbar.styles.ts index 00e6c1d336d..6b1f5428863 100644 --- a/src/mantine-rte/src/components/Toolbar/Toolbar.styles.ts +++ b/src/mantine-rte/src/components/Toolbar/Toolbar.styles.ts @@ -10,7 +10,7 @@ export default createStyles((theme, { sticky, stickyOffset }: ToolbarStyles) => display: 'flex', alignItems: 'center', flexWrap: 'wrap', - margin: theme.spacing.md / 2, + margin: `calc(${theme.spacing.md}px / 2)`, }, toolbar: { @@ -29,7 +29,7 @@ export default createStyles((theme, { sticky, stickyOffset }: ToolbarStyles) => toolbarInner: { display: 'flex', flexWrap: 'wrap', - margin: -theme.spacing.md / 2, + margin: `calc(${-theme.spacing.md}px / 2)`, }, toolbarControl: { diff --git a/src/mantine-spotlight/src/ActionsList/ActionsList.styles.ts b/src/mantine-spotlight/src/ActionsList/ActionsList.styles.ts index bfada0a2480..0360dcfaff9 100644 --- a/src/mantine-spotlight/src/ActionsList/ActionsList.styles.ts +++ b/src/mantine-spotlight/src/ActionsList/ActionsList.styles.ts @@ -4,7 +4,7 @@ export default createStyles((theme) => ({ nothingFound: {}, actions: { - padding: theme.spacing.xs / 2, + padding: `calc(${theme.spacing.xs}px / 2)`, borderTop: `1px solid ${ theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2] }`, diff --git a/yarn.lock b/yarn.lock index 9447f244f94..a0785ab3d0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13701,10 +13701,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" + integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== uglify-js@^3.1.4: version "3.15.3"