Skip to content

Commit

Permalink
[@mantine/core] Indicator: Add count support (#2237)
Browse files Browse the repository at this point in the history
* [@mantine/core] Indicator: count support

* [@mantine/core] Indicatoe: isShow back to disabled

* [@mantine/core] Indicator: typescript error fix

* [@mantine/core] MachineNumber: test pass

* [docs] Indicator docs optimization

* [@mantine/core] Indicator: Resolve conflicts

Co-authored-by: Aster <x.b.he@accenture.com>
  • Loading branch information
AmelloAster and Aster committed Sep 9, 2022
1 parent 680c0ca commit a8c45f8
Show file tree
Hide file tree
Showing 22 changed files with 944 additions and 85 deletions.
18 changes: 18 additions & 0 deletions docs/src/docs/core/Indicator.mdx
Expand Up @@ -27,6 +27,24 @@ element to keep `display: block`.

<Demo data={IndicatorDemos.inline} />

## OverflowCount

Set `overflowCount` to handle overflow cases:

<Demo data={IndicatorDemos.overflowCount} />

## Processing

Set `processing` to indicate that it is processing:

<Demo data={IndicatorDemos.processing} />

## ShowZero

Set `showZero` to display 0:

<Demo data={IndicatorDemos.showZero} />

## Offset

Set `offset` to change indicator position. It is useful when Indicator component is
Expand Down
6 changes: 3 additions & 3 deletions src/mantine-core/src/Indicator/Indicator.story.tsx
Expand Up @@ -12,7 +12,7 @@ const placements = ['start', 'center', 'end'] as const;
export const Positions = () => {
const items = positions.map((position) => {
const _items = placements.map((placement) => (
<Indicator position={`${position}-${placement}`}>
<Indicator dot position={`${position}-${placement}`}>
<Avatar radius={0} />
</Indicator>
));
Expand All @@ -25,15 +25,15 @@ export const Positions = () => {

export const Inline = () => (
<Box sx={{ padding: 40 }}>
<Indicator inline withBorder>
<Indicator dot inline withBorder>
<Avatar radius={0} />
</Indicator>
</Box>
);

export const WithRadius = () => (
<Box sx={{ padding: 40 }}>
<Indicator inline offset={12} size={20} position="bottom-end" withBorder color="red">
<Indicator inline dot offset={12} size={20} position="bottom-end" withBorder color="red">
<Avatar
radius={50000}
size="xl"
Expand Down
99 changes: 68 additions & 31 deletions src/mantine-core/src/Indicator/Indicator.styles.ts
@@ -1,4 +1,10 @@
import { createStyles, CSSObject, MantineColor, MantineNumberSize } from '@mantine/styles';
import {
createStyles,
CSSObject,
MantineColor,
MantineNumberSize,
keyframes,
} from '@mantine/styles';
import { IndicatorPosition } from './Indicator.types';

export interface IndicatorStylesParams {
Expand All @@ -13,6 +19,18 @@ export interface IndicatorStylesParams {
zIndex: React.CSSProperties['zIndex'];
}

const processingAnimation = (color: string) =>
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('-');
Expand Down Expand Up @@ -68,35 +86,54 @@ export default createStyles(
withLabel,
zIndex,
}: IndicatorStylesParams
) => ({
root: {
position: 'relative',
display: inline ? 'inline-block' : 'block',
},
) => {
const { background } = theme.fn.variant({
variant: 'filled',
primaryFallback: false,
color: color || theme.primaryColor,
});
return {
root: {
position: 'relative',
display: inline ? 'inline-block' : 'block',
},

indicator: {
...getPositionStyles(position, offset),
zIndex,
position: 'absolute',
[withLabel ? 'minWidth' : 'width']: size,
height: size,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: theme.fontSizes.xs,
paddingLeft: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0,
paddingRight: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0,
borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }),
backgroundColor: theme.fn.variant({
variant: 'filled',
primaryFallback: false,
color: color || theme.primaryColor,
}).background,
border: withBorder
? `2px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white}`
: undefined,
color: theme.white,
whiteSpace: 'nowrap',
},
})
indicator: {
...getPositionStyles(position, offset),
zIndex,
position: 'absolute',
[withLabel ? 'minWidth' : 'width']: size,
height: size,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: theme.fontSizes.xs,
paddingLeft: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0,
paddingRight: withLabel ? `calc(${theme.spacing.xs}px / 2)` : 0,
borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }),
backgroundColor: theme.fn.variant({
variant: 'filled',
primaryFallback: false,
color: color || theme.primaryColor,
}).background,
border: withBorder
? `2px solid ${theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white}`
: undefined,
color: theme.white,
whiteSpace: 'nowrap',
},

processing: {
animation: `${processingAnimation(background)} 1000ms linear infinite`,
},

common: {
...getPositionStyles(position, offset),
position: 'absolute',
[withLabel ? 'minWidth' : 'width']: size,
height: size,
borderRadius: theme.fn.size({ size: radius, sizes: theme.radius }),
},
};
}
);
39 changes: 37 additions & 2 deletions 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,
Expand All @@ -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<typeof useStyles>;

Expand All @@ -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;

Expand All @@ -47,6 +54,12 @@ export interface IndicatorProps
/** When component is disabled it renders children without indicator */
disabled?: boolean;

/** When showZero is true and label is zero renders children with indicator*/
showZero?: boolean;

/** Indicator processing animation */
processing?: boolean;

/** Indicator z-index */
zIndex?: React.CSSProperties['zIndex'];
}
Expand All @@ -57,7 +70,10 @@ const defaultProps: Partial<IndicatorProps> = {
inline: false,
withBorder: false,
disabled: false,
showZero: false,
processing: false,
size: 10,
overflowCount: 99,
radius: 1000,
zIndex: getDefaultZIndex('app'),
};
Expand All @@ -73,12 +89,16 @@ export const Indicator = forwardRef<HTMLDivElement, IndicatorProps>((props, ref)
withBorder,
className,
color,
dot,
styles,
label,
overflowCount,
showZero,
classNames,
disabled,
zIndex,
unstyled,
processing,
...others
} = useComponentDefaultProps('Indicator', defaultProps, props);

Expand All @@ -87,9 +107,24 @@ export const Indicator = forwardRef<HTMLDivElement, IndicatorProps>((props, ref)
{ name: 'Indicator', classNames, styles, unstyled }
);

const renderLabel = useMemo(() => {
if (isNumber(label)) {
return <Machine value={label} max={overflowCount} />;
}
return label;
}, [label, overflowCount]);

const isShowIndicator = useMemo(
() => !disabled && (dot || (!isUnDef(label) && !(label <= 0 && !showZero))),
[disabled, label, showZero]
);

return (
<Box ref={ref} className={cx(classes.root, className)} {...others}>
{!disabled && <div className={classes.indicator}>{label}</div>}
{isShowIndicator && (
<div className={cx(classes.indicator, classes.common)}>{renderLabel}</div>
)}
{processing && <div className={cx(classes.processing, classes.common)} />}
{children}
</Box>
);
Expand Down
9 changes: 9 additions & 0 deletions 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',
},
}));
63 changes: 63 additions & 0 deletions 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<HTMLDivElement, MachineNumberProps>(({ value = 0, max }, ref) => {
const [oldValue, setOldValue] = useState<number>();
const [newValue, setNewValue] = useState<number>();
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) ? (
<span ref={ref}>{value}</span>
) : (
<span ref={ref} className={classes.base}>
{numbers.map((number, i) => (
<MachineNumber
key={numbers.length - i - 1}
value={number}
oldOriginalNumber={oldValue}
newOriginalNumber={newValue}
/>
))}
{isDef(max) && value > max && <span>+</span>}
</span>
);
});

0 comments on commit a8c45f8

Please sign in to comment.