Skip to content

Commit

Permalink
fix: fixed comments after review
Browse files Browse the repository at this point in the history
  • Loading branch information
German Shteinardt committed May 9, 2024
1 parent 45aba44 commit 8df1b53
Show file tree
Hide file tree
Showing 25 changed files with 285 additions and 219 deletions.
2 changes: 1 addition & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export * from './useActionHandlers';
export * from './useAsyncActionHandler';
export * from './useBodyScrollLock';
export * from './useControlledState';
export * from './useGeneratorColor';
export * from './useColorGenerator';
export * from './useFileInput';
export * from './useFocusWithin';
export * from './useForkRef';
Expand Down
26 changes: 26 additions & 0 deletions src/hooks/useColorGenerator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--GITHUB_BLOCK-->

# useColorGenerator

<!--/GITHUB_BLOCK-->

```tsx
import {useColorGenerator} from '@gravity-ui/uikit';
```

The `useColorGenerator` a hook that generates a unique (but consistent) background color based on some unique attribute (e.g., name, id, email). The background color remains unchanged with each update.

## Properties

| Name | Description | Type | Default | | |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | :---------: | --- | --- |
| mode | Value to control color saturation | `saturated` `unsaturated` `bright` | `saturated` |
| token | Unique attribute of the entity (e.g., name, id, email) | `string` | | | |
| colorKeys | If an array of colors is passed, an index is generated from the token passed, and the value from the color array at that index is returned | `string[]` | | | |

## Result

`useColorGenerator` returns an object with exactly two values:

1. color - unique color from a token.
2. textColor - text color (dark or light), ensurring higher contrast on generated color.
12 changes: 12 additions & 0 deletions src/hooks/useColorGenerator/__stories__/ColorGenerator.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@use '../../../components/variables.scss';

$block: '.#{variables.$ns}color-generator';

#{$block} {
&__color-items {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-block-start: 20px;
}
}
36 changes: 36 additions & 0 deletions src/hooks/useColorGenerator/__stories__/ColoredAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';

import {Avatar} from '../../../components/Avatar';
import type {AvatarProps} from '../../../components/Avatar';
import type {UseColorGeneratorProps} from '../types';
import {useColorGenerator} from '../useColorGenerator';

type ColoredAvatarProps = AvatarProps & {
withText: boolean;
mode: UseColorGeneratorProps['mode'];
token: UseColorGeneratorProps['token'];
};

export const ColoredAvatar = ({
mode,
theme,
token,
withText,
...avatarProps
}: ColoredAvatarProps) => {
const {color, textColor} = useColorGenerator({
token,
mode,
});

return (
<Avatar
{...avatarProps}
theme={theme}
text={withText ? token : undefined}
color={withText ? textColor : undefined}
backgroundColor={color}
size="l"
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';

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

import {Button} from '../../../components/Button';
import {block} from '../../../components/utils/cn';
import {randomString} from '../__tests__/utils/randomString';

import {ColoredAvatar} from './ColoredAvatar';

import './ColorGenerator.scss';

const b = block('color-generator');

const meta: Meta = {
title: 'Hooks/useColorGenerator',
argTypes: {
mode: {
options: ['unsaturated', 'saturated', 'bright'],
control: {type: 'radio'},
},
withText: {
control: 'boolean',
},
},
};

export default meta;

type Story = StoryObj<typeof ColoredAvatar>;

const initValues = () => {
const result = Array.from({length: 10}, () => randomString(16));

return result;
};

const Template = (args: React.ComponentProps<typeof ColoredAvatar>) => {
const {mode, withText} = args;
const [tokens, setTokens] = React.useState<string[]>(initValues);

const onClick = React.useCallback(() => {
const newToken = randomString(16);
setTokens((prev) => [newToken, ...prev]);
}, []);

return (
<React.Fragment>
<Button title="generate color" onClick={onClick}>
Generate color
</Button>
<div className={b('color-items')}>
{tokens.map((token) => (
<ColoredAvatar key={token} token={token} mode={mode} withText={withText} />
))}
</div>
</React.Fragment>
);
};

export const Default: Story = {
render: (args) => {
return <Template {...args} />;
},
args: {
mode: 'unsaturated',
withText: false,
},
};
24 changes: 24 additions & 0 deletions src/hooks/useColorGenerator/__tests__/getHue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {getHue} from '../utils'; // Подставьте правильное имя файла

describe('getHue', () => {
test('returns values within the range [0, 360)', () => {
const MIN_HASH = -Math.pow(2, 31);
const MAX_HASH = Math.pow(2, 31);

for (let i = 0; i < 1000; i++) {
const hash = Math.random() * (MAX_HASH - MIN_HASH) + MIN_HASH;
const hue = getHue(hash);

expect(hue).toBeGreaterThanOrEqual(0);
expect(hue).toBeLessThan(360);
}

const maxHue = getHue(MAX_HASH);
const minHue = getHue(MIN_HASH);

expect(maxHue).toBeGreaterThanOrEqual(0);
expect(maxHue).toBeLessThan(360);
expect(minHue).toBeGreaterThanOrEqual(0);
expect(minHue).toBeLessThan(360);
});
});
23 changes: 23 additions & 0 deletions src/hooks/useColorGenerator/__tests__/randomIndex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {randomIndex} from '../utils';

import {randomString} from './utils/randomString';

describe('randomIndex', () => {
test('returns numbers within the range [0, max)', () => {
const MAX_VALUE = 500;

for (let i = 0; i <= 1000; i++) {
const token = randomString(16);
const index = randomIndex(token, MAX_VALUE);

expect(index).toBeGreaterThanOrEqual(0);
expect(index).toBeLessThan(MAX_VALUE);
}

const zeroIndex = randomIndex('test', 0);
expect(zeroIndex).toBe(0);

const oneIndex = randomIndex('test', 1);
expect(oneIndex).toBe(0);
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
/* eslint-disable no-bitwise */
import type {ColorProps, ThemeColorSettings} from '../types';

import {BLACK_COLOR, WHITE_COLOR, colorOptions} from './constants';
import {getHue} from './getHue';
import {hashFnv32a} from './hashFnv32a';
import {normalizeHash} from './normalizeHash';
import {randomIndex} from './randomIndex';
import type {ColorProps, ThemeColorSettings} from './types';
import {getHue, hashFnv32a, normalizeHash, randomIndex} from './utils';

class Color {
export class ColorGenerator {
private _colorKeys?: string[];
private _saturation?: number;
private _lightness?: number;
Expand Down Expand Up @@ -40,14 +36,12 @@ class Color {
return this.hslColor();
}

get oppositeColor() {
get textColor() {
if (!this._hue || !this._saturation || !this._lightness) {
return WHITE_COLOR;
}

const luminance = this.getLuminance(this._hue, this._saturation, this._lightness);

return luminance > 0.7 ? BLACK_COLOR : WHITE_COLOR;
return this.getTextColor(this._hue, this._saturation, this._lightness);
}

private getColorKeysIndex() {
Expand All @@ -58,6 +52,7 @@ class Color {
return randomIndex(this._token, this._colorKeys.length);
}

// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB
private hslToRgb = (h: number, s: number, l: number) => {
s /= 100;
l /= 100;
Expand All @@ -76,15 +71,35 @@ class Color {
return [r, g, b] as [number, number, number];
};

private rgbToLuminance(r: number, g: number, b: number) {
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}

private getLuminance(h: number, s: number, l: number) {
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
private getTextColor(
h: number,
s: number,
l: number,
lightColor = WHITE_COLOR,
darkColor = BLACK_COLOR,
) {
const rgb = this.hslToRgb(h, s, l);
const luminance = this.rgbToLuminance(...rgb);
const N = rgb.length;
const normalizedValues = Array(N);

for (let i = 0; i < N; i++) {
let c = rgb[i];
c /= 255.0;

return luminance;
if (c <= 0.04045) {
c /= 12.92;
} else {
c = Math.pow((c + 0.055) / 1.055, 2.4);
}

normalizedValues[i] = c;
}

const [r, g, b] = normalizedValues;
const L = 0.2126 * r + 0.7152 * g + 0.0722 * b;

return L > 0.179 ? darkColor : lightColor;
}

private hslColor() {
Expand Down Expand Up @@ -113,7 +128,3 @@ class Color {
return hash;
}
}

export const colorGenerator = (args: ColorProps) => {
return new Color(args);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ColorOptions, ThemeColorSettings} from '../types';
import type {ColorOptions, ThemeColorSettings} from './types';

const bright: ColorOptions = {
lightness: [45, 55],
Expand Down
2 changes: 2 additions & 0 deletions src/hooks/useColorGenerator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {useColorGenerator} from './useColorGenerator';
export type {UseColorGeneratorProps} from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type ColorProps = {
theme: string;
};

export type UseGeneratorColorProps = {
export type UseColorGeneratorProps = {
colorKeys?: string[];
mode?: 'saturated' | 'unsaturated' | 'bright';
token: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable valid-jsdoc */
import {useThemeType} from '../../components/theme/useThemeType';

import type {UseGeneratorColorProps} from './types';
import {colorGenerator} from './utils/color';
import {ColorGenerator} from './color';
import type {UseColorGeneratorProps} from './types';

/**
* It is used to create a unique color from a token (string) and to obtain an inverted color (black or white),
Expand All @@ -14,25 +14,25 @@ import {colorGenerator} from './utils/color';
import {Avatar} from '@gravity-ui/uikit';
const Component = ({ token, text, ...avatarProps }) => {
const {color, oppositeColor} = useGeneratorColor({
const {color, textColor} = useColorGenerator({
token,
});
return (
<Avatar
{...avatarProps}
text={text}
color={text ? oppositeColor : undefined}
color={text ? textColor : undefined}
backgroundColor={color}
/>
);
};
```
*/
export function useGeneratorColor(props: UseGeneratorColorProps) {
export function useColorGenerator(props: UseColorGeneratorProps) {
const theme = useThemeType();

const options = colorGenerator({
const options = new ColorGenerator({
...props,
theme,
});
Expand Down
Loading

0 comments on commit 8df1b53

Please sign in to comment.