Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@mantine/core] Indicator: count support #2237

Merged
merged 8 commits into from Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
);
});