Skip to content

Commit

Permalink
feat(ColorsGrid): add ColorsGrid component
Browse files Browse the repository at this point in the history
  • Loading branch information
aulian0v committed Jan 10, 2024
1 parent ed68043 commit db26335
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 0 deletions.
86 changes: 86 additions & 0 deletions src/components/ColorsGrid/ColorsGrid.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@use '../variables';

$block: '.#{variables.$ns-new}colors-grid';

#{$block} {
--_--color-size: 28px;
--_--color-icon-size: 12px;
--_--color-gap: 1px;

width: min-content;
height: min-content;
display: grid;
grid-template-columns: repeat(var(--_--cols-num), 1fr);
gap: var(--_--color-gap);
border-radius: 8px;
box-shadow: 0px 2px 52px 0px var(--g-color-sfx-shadow);
padding: 12px;

&__item {
display: flex;
justify-content: center;
align-items: center;
width: var(--_--color-size);
height: var(--_--color-size);
border-radius: 1px;
box-sizing: border-box;

&_active {
outline: 1px solid var(--g-color-line-focus);
outline-offset: 1px;
border-radius: 4px;
z-index: 1;
}

&_void {
border: 1px solid var(--g-color-line-generic-solid);
position: relative;
overflow: hidden;

&::after {
content: '';
position: absolute;
display: inline-block;
width: 150%;
height: 2px;
background-color: var(--g-color-line-danger);
transform: rotate(-45deg);
}
}

&[role='button'] {
&:hover,
&:focus-visible {
outline: 1px solid var(--g-color-line-focus);
outline-offset: 0px;
z-index: 1;
}
}
}

&__check {
width: var(--_--color-icon-size);
height: var(--_--color-icon-size);
z-index: 1;
}

&_size {
&_s {
--_--color-size: 24px;
--_--color-icon-size: 12px;
--_--color-gap: 1px;
}

&_m {
--_--color-size: 28px;
--_--color-icon-size: 14px;
--_--color-gap: 1px;
}

&_l {
--_--color-size: 36px;
--_--color-icon-size: 16px;
--_--color-gap: 2px;
}
}
}
53 changes: 53 additions & 0 deletions src/components/ColorsGrid/ColorsGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';

import type {DOMProps, QAProps} from '../types';
import {blockNew} from '../utils/cn';

import {ColorsGridItem} from './ColorsGridItem';
import type {Color} from './utils';

import './ColorsGrid.scss';

const b = blockNew('colors-grid');

export type ColorsGridSize = 's' | 'm' | 'l';

export interface ColorsGridProps extends DOMProps, QAProps {
colors: Color[];
value?: Color;
rowSize?: number;
size?: ColorsGridSize;
onUpdate?(value: Color): void;
}

export function ColorsGrid(props: ColorsGridProps) {
const {colors, rowSize = 6, size = 'm', value, onUpdate, style, className, qa} = props;

const handleSelectColor = (color: Color) => {
if (!onUpdate) {
return undefined;
}

return (event: React.UIEvent<HTMLElement>) => {
event.preventDefault();
onUpdate(color);
};
};

return (
<div
data-qa={qa}
className={b({size}, className)}
style={{...style, '--_--cols-num': rowSize} as React.CSSProperties}
>
{colors.map((color, index) => (
<ColorsGridItem
key={index}
color={color}
selected={color === value}
onClick={handleSelectColor(color)}
/>
))}
</div>
);
}
44 changes: 44 additions & 0 deletions src/components/ColorsGrid/ColorsGridItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';

import {Check} from '@gravity-ui/icons';

import {useActionHandlers} from '../../hooks/useActionHandlers';
import {Icon} from '../Icon';
import {useTheme} from '../theme';
import {blockNew} from '../utils/cn';

import {Color, getContrastColor} from './utils';

const b = blockNew('colors-grid');

export interface ColorsGridItemProps {
color: Color;
selected: boolean;
onClick?(event: React.UIEvent): void;
}

export function ColorsGridItem({color, selected, onClick}: ColorsGridItemProps) {
const theme = useTheme();

const isClickable = Boolean(onClick);
const isVoid = !color;
const style = {
backgroundColor: color || undefined,
color: selected ? getContrastColor(color, theme) : undefined,
};

const {onKeyDown} = useActionHandlers(onClick);

return (
<div
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
style={style}
className={b('item', {void: isVoid, active: selected})}
onClick={onClick}
onKeyDown={onKeyDown}
>
{!isVoid && selected && <Icon className={b('check')} size={40} data={Check} />}
</div>
);
}
47 changes: 47 additions & 0 deletions src/components/ColorsGrid/__stories__/ColorsGrid.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';

import type {StoryObj} from '@storybook/react';

import {Showcase} from '../../../demo/Showcase';
import {ColorsGrid} from '../ColorsGrid';
import type {Color} from '../utils';

import {ColorsGridShowcase} from './ColorsGridShowcase';

export default {
title: 'Components/Inputs/ColorsGrid',
component: ColorsGrid,
};

type Story = StoryObj<typeof ColorsGrid>;

const colors: Color[] = [
null,
'rgba(255, 255, 0, 0.45)',
'#FFB9DD',
'#FF91A1',
'#8AD554',
'#70C1AF',
'#DAE0E7',
'#FF7E00',
'#ED65A9',
'#BA74B3',
'#E8B0A4',
'#52A6C5',
];

export const Default: Story = {
args: {colors},
render: (args) => <ColorsGridShowcase {...args} />,
};

export const Size: Story = {
args: {colors},
render: (args) => (
<Showcase>
<ColorsGrid {...args} size="s" />
<ColorsGrid {...args} size="m" />
<ColorsGrid {...args} size="l" />
</Showcase>
),
};
10 changes: 10 additions & 0 deletions src/components/ColorsGrid/__stories__/ColorsGridShowcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

import {ColorsGrid, ColorsGridProps} from '../ColorsGrid';
import type {Color} from '../utils';

export const ColorsGridShowcase = (props: ColorsGridProps) => {
const [value, onUpdate] = React.useState<Color>();

return <ColorsGrid {...props} value={value} onUpdate={onUpdate} />;
};
70 changes: 70 additions & 0 deletions src/components/ColorsGrid/__tests__/ColorsGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';

import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import {ColorsGrid, ColorsGridSize} from '../ColorsGrid';
import type {Color} from '../utils';

const qaId = 'colors-grid-component';

const colors: Color[] = ['#ff0000'];
const sizes: ColorsGridSize[] = ['s', 'm', 'l'];

describe('ColorsGrid', () => {
test('render by default', () => {
render(<ColorsGrid qa={qaId} colors={colors} />);
const grid = screen.getByTestId(qaId);

expect(grid).toBeVisible();
});

test.each(sizes)('render with given "%s" size', (size) => {
render(<ColorsGrid qa={qaId} colors={colors} size={size} />);
const grid = screen.getByTestId(qaId);

expect(grid).toHaveClass(`g-colors-grid_size_${size}`);
});

test('render with empty colors list', () => {
render(<ColorsGrid qa={qaId} colors={[]} />);
const grid = screen.getByTestId(qaId);

expect(grid).toBeEmptyDOMElement();
});

test('render colors list', () => {
render(<ColorsGrid qa={qaId} colors={colors} />);
const {children} = screen.getByTestId(qaId);

expect(children.length).toBe(colors.length);
});

test('render with null color', async () => {
render(<ColorsGrid qa={qaId} colors={[null]} />);
const {firstChild} = screen.getByTestId(qaId);

expect(firstChild).toHaveClass('g-colors-grid__item_void');
});

test('render with active color', () => {
const color = colors[0];

render(<ColorsGrid qa={qaId} colors={colors} value={color} />);
const {firstChild} = screen.getByTestId(qaId);

expect(firstChild).toHaveClass('g-colors-grid__item_active');
});

test('call onChange when color changed', async () => {
const color = colors[0];
const onChangeFn = jest.fn();
const user = userEvent.setup();

render(<ColorsGrid qa={qaId} colors={colors} onUpdate={onChangeFn} />);

await user.click(screen.getByRole('button'));

expect(onChangeFn).toBeCalledWith(color);
});
});
2 changes: 2 additions & 0 deletions src/components/ColorsGrid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {ColorsGrid} from './ColorsGrid';
export type {ColorsGridProps} from './ColorsGrid';
47 changes: 47 additions & 0 deletions src/components/ColorsGrid/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export type Color = string | null;

const parseRgba = (color: string) => {
return color
.slice(color.indexOf('(') + 1, -1)
.split(', ')
.map(Number);
};

const getComputedColor = (color: string) => {
const element = document.createElement('div');

element.style.color = color;
document.body.appendChild(element);

const computedColor = window.getComputedStyle(element).color;

element.remove();

return computedColor;
};

export const getContrastColor = (color: string | null, theme: string) => {
const defaultColor = theme.startsWith('light')
? 'var(--g-color-text-dark-primary)'
: 'var(--g-color-text-light-primary)';

if (!color) {
return defaultColor;
}

const rgba = getComputedColor(color);

if (!rgba) {
return defaultColor;
}

const [r, g, b, a = 1] = parseRgba(rgba);

if (a < 0.5) {
return defaultColor;
}

return (r * 0.299 + g * 0.587 + b * 0.114) * a > 186
? 'var(--g-color-text-dark-primary)'
: 'var(--g-color-text-light-primary)';
};

0 comments on commit db26335

Please sign in to comment.