Skip to content

Commit

Permalink
fix: api refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
Ruminat committed Jul 19, 2024
1 parent 7852b9e commit e087e03
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 93 deletions.
58 changes: 26 additions & 32 deletions src/components/Reactions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Component for user reactions (e.g. 👍, 😊, 😎 etc) as new GitHub comments
import React from 'react';

import {PaletteOption} from '@gravity-ui/uikit';
import {ReactionProps, Reactions} from '@gravity-ui/components';
import {ReactionStateProps, Reactions} from '@gravity-ui/components';

const user = {
spongeBob: {name: 'Sponge Bob'},
Expand All @@ -30,11 +30,11 @@ const YourComponent = () => {
[option.cool.value]: [user.spongeBob],
});

// And then convert that mapping into an array of ReactionProps
// And then convert that mapping into an array of ReactionStateProps
const reactions = React.useMemo(
() =>
Object.entries(usersReacted).map(
([value, users]): ReactionProps => ({
([value, users]): ReactionStateProps => ({
value,
counter: users.length,
selected: users.some(({name}) => name === currentUser.name),
Expand All @@ -44,8 +44,8 @@ const YourComponent = () => {
);

// You can then handle clicking on a reaction with changing the inital mapping,
// and the array of ReactionProps will change accordingly
const onClickReaction = React.useCallback(
// and the array of ReactionStateProps will change accordingly
const onToggle = React.useCallback(
(value: string) => {
if (!usersReacted[value]) {
// If the reaction is not present yet
Expand Down Expand Up @@ -75,7 +75,7 @@ const YourComponent = () => {
);

return (
<Reactions palette={{options}} reactions={reactions} onClickReaction={onClickReaction} />
<Reactions palette={{options}} reactions={reactions} onToggle={onToggle} />
);
};
```
Expand All @@ -86,29 +86,23 @@ For more code examples go to [Reactions.stories.tsx](https://github.com/gravity-

**ReactionsProps** (main component props — Reactions' list):

| Property | Type | Required | Default | Description |
| :---------------- | :------------------------ | :------: | :------ | :--------------------------------------------------------------------------------------------- |
| `reactions` | `ReactionProps[]` | `true` | | List of Reactions to display |
| `palette` | `ReactionsPaletteProps` | `true` | | Notifications' palette props — it's a `Palette` component with available reactions to the user |
| `onClickReaction` | `(value: string) => void` | | | Fires when a user clicks on a Reaction (in a Palette or in the Reactions' list) |
| `size` | `ButtonSize` | | `m` | Buttons's size |
| `disabled` | `boolean` | | `false` | If the buttons' are disabled |
| `qa` | `string` | | | `qa` attribute for testing |
| `className` | `string` | | | HTML class attribute |
| `style` | `React.CSSProperties` | | | HTML style attribute |

**ReactionProps** (single reaction props) extends `Palette`'s `PaletteOption` `disabled` and `value` props:

| Property | Type | Required | Default | Description |
| :--------- | :--------------------- | :------: | :------ | :------------------------------------------------------------ |
| `selected` | `boolean` | | | Is reaction selected by the user |
| `counter` | `React.ReactNode` | | | How many users used this reaction |
| `tooltip` | `ReactionTooltipProps` | | | Reaction's tooltip with the list of reacted users for example |

**ReactionTooltipProps** — notification's type extends `Pick<PopoverProps, 'strategy' | 'placement' | 'modifiers'>`:

| Property | Type | Required | Default | Description |
| :-------------- | :---------------- | :------: | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `React.ReactNode` | `true` | | Tooltip's content |
| `className` | `string` | | | Tooltip content's HTML class attribute |
| `canClosePopup` | `() => boolean` | | | Fires when the `onMouseLeave` callback is called. Usage example: you have some popup inside a tooltip, you hover on it, you don't want the tooltip to be closed because of that. |
| Property | Type | Required | Default | Description |
| :--------------- | :------------------------ | :------: | :------ | :--------------------------------------------------------------------------------------------- |
| `reactions` | `PaletteOption[]` | `true` | | List of all available reactions |
| `reactionsState` | `ReactionStateProps[]` | `true` | | List of reactions that were used |
| `paletteProps` | `ReactionsPaletteProps` | `true` | | Notifications' palette props — it's a `Palette` component with available reactions to the user |
| `onToggle` | `(value: string) => void` | | | Fires when a user clicks on a Reaction (in a Palette or in the Reactions' list) |
| `size` | `ButtonSize` | | `m` | Buttons's size |
| `readOnly` | `boolean` | | `false` | readOnly state (usage example: only signed in users can react) |
| `qa` | `string` | | | `qa` attribute for testing |
| `className` | `string` | | | HTML class attribute |
| `style` | `React.CSSProperties` | | | HTML style attribute |

**ReactionStateProps** (single reaction props):

| Property | Type | Required | Default | Description |
| :--------- | :---------------- | :------: | :------ | :------------------------------------------------------------ |
| `value` | `string` | | | Reaction's unique value (ID) |
| `selected` | `boolean` | | | Is reaction selected by the user |
| `counter` | `React.ReactNode` | | | How many users used this reaction |
| `tooltip` | `React.ReactNode` | | | Reaction's tooltip with the list of reacted users for example |
13 changes: 9 additions & 4 deletions src/components/Reactions/Reaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {block} from '../utils/cn';
import {useReactionsContext} from './context';
import {useReactionsPopup} from './hooks';

export interface ReactionProps extends Pick<PaletteOption, 'disabled' | 'value'> {
export type ReactionProps = Pick<PaletteOption, 'value' | 'content' | 'title'>;

export interface ReactionStateProps {
/**
* Reaction's unique value (ID).
*/
value: string;
/**
* Should be true when the user used this reaction.
*/
Expand All @@ -25,7 +31,7 @@ export interface ReactionProps extends Pick<PaletteOption, 'disabled' | 'value'>
}

interface ReactionInnerProps extends Pick<PaletteOption, 'content'> {
reaction: ReactionProps;
reaction: ReactionStateProps;
size: ButtonSize;
onClick?: (value: string) => void;
}
Expand All @@ -42,7 +48,7 @@ const popupDefaultPlacement: PopoverProps['placement'] = [
const b = block('reactions');

export function Reaction(props: ReactionInnerProps) {
const {value, disabled, selected, counter, tooltip} = props.reaction;
const {value, selected, counter, tooltip} = props.reaction;
const {size, content, onClick} = props;

const onClickCallback = React.useCallback(() => onClick?.(value), [onClick, value]);
Expand All @@ -55,7 +61,6 @@ export function Reaction(props: ReactionInnerProps) {
<Button
className={b('reaction-button', {size})}
ref={buttonRef}
disabled={disabled}
size={size}
selected={selected}
view="outlined"
Expand Down
70 changes: 38 additions & 32 deletions src/components/Reactions/Reactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,39 @@ import xor from 'lodash/xor';

import {block} from '../utils/cn';

import {Reaction, ReactionProps} from './Reaction';
import {Reaction, ReactionProps, ReactionStateProps} from './Reaction';
import {ReactionsContextProvider, ReactionsContextTooltipProps} from './context';

import './Reactions.scss';

const b = block('reactions');

export type ReactionsPaletteProps = Omit<
export type ReactionsPaletteProps = Pick<
PaletteProps,
'value' | 'defaultValue' | 'onUpdate' | 'size' | 'disabled' | 'multiple'
'columns' | 'rowClassName' | 'optionClassName'
>;

export interface ReactionsProps extends Pick<PaletteProps, 'size' | 'disabled'>, QAProps, DOMProps {
export interface ReactionsProps extends Pick<PaletteProps, 'size'>, QAProps, DOMProps {
/**
* Users' reactions.
* All available reactions.
*/
reactions: ReactionProps[];
/**
* Users' reactions.
*/
reactionsState: ReactionStateProps[];
/**
* Reactions' palette props.
*/
palette: ReactionsPaletteProps;
paletteProps?: ReactionsPaletteProps;
/**
* Reactions' readonly state (when a user is unable to react for some reason).
*/
readOnly?: boolean;
/**
* Callback for clicking on a reaction in the Palette or directly in the reactions' list.
*/
onClickReaction?: (value: string) => void;
onToggle?: (value: string) => void;
}

const buttonSizeToIconSize = {
Expand All @@ -53,59 +61,57 @@ const buttonSizeToIconSize = {

export function Reactions({
reactions,
reactionsState,
className,
style,
size = 'm',
disabled,
palette,
paletteProps,
readOnly,
qa,
onClickReaction,
onToggle,
}: ReactionsProps) {
const [currentHoveredReaction, setCurrentHoveredReaction] = React.useState<
ReactionsContextTooltipProps | undefined
>(undefined);

const paletteOptionsMap = React.useMemo(
() =>
palette.options
? palette.options.reduce<Record<PaletteOption['value'], PaletteOption>>(
(acc, current) => {
// eslint-disable-next-line no-param-reassign
acc[current.value] = current;
return acc;
},
{},
)
: {},
[palette.options],
reactions.reduce<Record<PaletteOption['value'], PaletteOption>>((acc, current) => {
// eslint-disable-next-line no-param-reassign
acc[current.value] = current;
return acc;
}, {}),
[reactions],
);

const paletteValue = React.useMemo(
() => reactions.filter((reaction) => reaction.selected).map((reaction) => reaction.value),
[reactions],
() =>
reactionsState
.filter((reaction) => reaction.selected)
.map((reaction) => reaction.value),
[reactionsState],
);

const onUpdatePalette = React.useCallback(
(updated: string[]) => {
const diffValues = xor(paletteValue, updated);
for (const diffValue of diffValues) {
onClickReaction?.(diffValue);
onToggle?.(diffValue);
}
},
[onClickReaction, paletteValue],
[onToggle, paletteValue],
);

const paletteContent = React.useMemo(
() => (
<Palette
{...palette}
{...paletteProps}
value={paletteValue}
disabled={disabled}
size={size}
onUpdate={onUpdatePalette}
/>
),
[paletteValue, disabled, size, palette, onUpdatePalette],
[paletteValue, size, paletteProps, onUpdatePalette],
);

return (
Expand All @@ -117,22 +123,22 @@ export function Reactions({
>
<Flex className={b(null, className)} style={style} gap={1} wrap={true} qa={qa}>
{/* Reactions' list */}
{reactions.map((reaction) => {
{reactionsState.map((reaction) => {
const content = paletteOptionsMap[reaction.value]?.content ?? '?';

return (
<Reaction
key={reaction.value}
content={content}
reaction={disabled ? {...reaction, disabled} : reaction}
reaction={reaction}
size={size}
onClick={onClickReaction}
onClick={readOnly ? undefined : onToggle}
/>
);
})}

{/* Add reaction button */}
{disabled ? null : (
{readOnly ? null : (
<Popover
content={paletteContent}
tooltipContentClassName={b('add-reaction-popover')}
Expand Down
21 changes: 19 additions & 2 deletions src/components/Reactions/__stories__/Reactions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,25 @@ export const Default: StoryFn = () => {
return <Reactions {...useMockReactions()} />;
};

export const Disabled: StoryFn = () => {
return <Reactions {...useMockReactions()} disabled={true} />;
export const Readonly: StoryFn = () => {
const {reactions, reactionsState, onToggle} = useMockReactions();

return (
<Reactions
reactions={reactions}
reactionsState={reactionsState.map((reaction) => ({
...reaction,
tooltip: (
<Flex direction="column" gap={2}>
<Text variant="subheader-1">You must be singed in to react</Text>
{reaction.tooltip}
</Flex>
),
}))}
onToggle={onToggle}
readOnly={true}
/>
);
};

export const Size: StoryFn = () => {
Expand Down
11 changes: 0 additions & 11 deletions src/components/Reactions/__tests__/Reactions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ describe('Reactions', () => {
},
);

test('all buttons are disabled when disabled=true prop is given', () => {
render(<TestReactions qa={qaId} disabled={true} />);

const $component = screen.getByTestId(qaId);
const $reactions = within($component).getAllByRole('button');

$reactions.forEach(($reaction: HTMLElement) => {
expect($reaction).toBeDisabled();
});
});

test('show given reaction', () => {
render(<TestReactions qa={qaId} />);

Expand Down
16 changes: 8 additions & 8 deletions src/components/Reactions/__tests__/mock/mockHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import {Flex, Text, User} from '@gravity-ui/uikit';

import {ReactionProps} from '../../Reaction';
import {ReactionStateProps} from '../../Reaction';
import {Reactions, ReactionsProps} from '../../Reactions';

import {
Expand Down Expand Up @@ -41,7 +41,7 @@ const renderUsersReacted = (users: ReactionsMockUser[]) => {
);
};

const getTooltip = (users: ReactionsMockUser[]): ReactionProps['tooltip'] =>
const getTooltip = (users: ReactionsMockUser[]): ReactionStateProps['tooltip'] =>
renderUsersReacted(users);

export function useMockReactions(): ReactionsProps {
Expand All @@ -54,10 +54,10 @@ export function useMockReactions(): ReactionsProps {
[option.sad.value]: [user.squidward],
});

const reactions = React.useMemo(
const reactionsState = React.useMemo(
() =>
Object.entries(usersReacted).map(
([value, users]): ReactionProps => ({
([value, users]): ReactionStateProps => ({
value,
counter: users.length,
tooltip: getTooltip(users),
Expand All @@ -67,7 +67,7 @@ export function useMockReactions(): ReactionsProps {
[usersReacted],
);

const onClickReaction = React.useCallback(
const onToggle = React.useCallback(
(value: string) => {
if (!usersReacted[value]) {
setUsersReacted((current) => ({...current, [value]: [currentUser]}));
Expand All @@ -93,9 +93,9 @@ export function useMockReactions(): ReactionsProps {
);

return {
palette: {options},
reactions,
onClickReaction,
reactions: options,
reactionsState,
onToggle,
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/Reactions/context.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';

import type {ReactionProps} from './Reaction';
import type {ReactionStateProps} from './Reaction';

export interface ReactionsContextTooltipProps {
reaction: ReactionProps;
reaction: ReactionStateProps;
ref: React.RefObject<HTMLButtonElement>;
open: boolean;
}
Expand Down
Loading

0 comments on commit e087e03

Please sign in to comment.