-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ColorsGrid): add ColorsGrid component
- Loading branch information
Showing
8 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
47
src/components/ColorsGrid/__stories__/ColorsGrid.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
src/components/ColorsGrid/__stories__/ColorsGridShowcase.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export {ColorsGrid} from './ColorsGrid'; | ||
export type {ColorsGridProps} from './ColorsGrid'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)'; | ||
}; |