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

feat(Reactions): added new Reactions component #197

Merged
merged 28 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
157ec91
feat: added a basic Reactions component (not working yet)
Ruminat Mar 5, 2024
a658369
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat May 14, 2024
a663963
fix: fixed story filename
Ruminat May 14, 2024
327ca7b
fix: fixed reactions' popups
Ruminat May 30, 2024
2ca5a5e
feat: added tests to Reactions
Ruminat May 31, 2024
bae5629
feat: added README
Ruminat May 31, 2024
7693d1a
fix: fixed reaction's view
Ruminat May 31, 2024
7f5902e
fix: $buttons -> $reactions
Ruminat May 31, 2024
540f8c0
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat May 31, 2024
d9c1f8e
fix: fixed qa attribute
Ruminat May 31, 2024
8675b22
fix: minor PR fixes
Ruminat Jun 7, 2024
2f411e2
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat Jun 7, 2024
8d96867
fix: eslint
Ruminat Jun 7, 2024
17d7cfe
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat Jul 11, 2024
855b5d4
fix: simplified `ReactionProps`'s `tooltip` + removed `content` and `…
Ruminat Jul 11, 2024
7852b9e
fix: removed useStableCallback completely
Ruminat Jul 19, 2024
e087e03
fix: api refactoring
Ruminat Jul 19, 2024
1916890
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat Jul 19, 2024
b30d0f9
fix: fixed palette (absent options + test (aria-label))
Ruminat Jul 23, 2024
7e5b50b
feat: added `tooltipBehavior` property to Reactions component
Ruminat Jul 23, 2024
c7cf6c4
chore: added myself as owner of `Notifications` and `Reactions` compo…
Ruminat Jul 24, 2024
db1f96d
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat Jul 24, 2024
942bacc
fix: minor PR fixes
Ruminat Aug 2, 2024
4cdeec6
fix: removed tooltipBehavior + changed tooltip to renderTooltip
Ruminat Aug 12, 2024
e3bd90f
Merge remote-tracking branch 'origin/main' into feature/reactions-com…
Ruminat Aug 12, 2024
29f706c
fix: added a span to add-reaction-button
Ruminat Aug 12, 2024
5e80065
fix: used colorText
Ruminat Aug 14, 2024
2439ec1
fix: used flat-secondary for reaction-button
Ruminat Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/components/Reactions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
## Reactions

Component for user reactions (e.g. 👍, 😊, 😎 etc) as new GitHub comments for example.

### Usage example

```typescript
import React from 'react';

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

const user = {
spongeBob: {name: 'Sponge Bob'},
patrick: {name: 'Patrick'},
};

const currentUser = user.spongeBob;

const option = {
'thumbs-up': {content: '👍', value: 'thumbs-up'},
cool: {content: '😎', value: 'cool'},
} satisfies Record<string, PaletteOption>;

const options = Object.values(option);

const YourComponent = () => {
// You can set up a mapping: reaction.value -> users reacted
const [usersReacted, setUsersReacted] = React.useState({
[option.cool.value]: [user.spongeBob],
});

// And then convert that mapping into an array of ReactionProps
const reactions = React.useMemo(
() =>
Object.entries(usersReacted).map(
([value, users]): ReactionProps => ({
...option[value as keyof typeof option],
counter: users.length,
selected: users.some(({name}) => name === currentUser.name),
}),
),
[usersReacted],
);

// You can then handle clicking on a reaction with chaning the inital mapping,
// and the array of ReactionProps will change accordingly
const onClickReaction = React.useCallback(
(value: string) => {
if (!usersReacted[value]) {
// If the reaction is not present yet
setUsersReacted((current) => ({...current, [value]: [currentUser]}));
} else if (!usersReacted[value].some(({name}) => name === currentUser.name)) {
// If the reaction is present, but current user hasn't selected it yet
setUsersReacted((current) => ({
...current,
[value]: [...usersReacted[value], currentUser],
}));
} else if (usersReacted[value].length > 1) {
// If the user used that reaction, and he's not the only one who used it
setUsersReacted((current) => ({
...current,
[value]: usersReacted[value].filter(({name}) => name !== currentUser.name),
}));
} else {
// If the user used that reaction, and he's the only one who used it
setUsersReacted((current) => {
const newValue = {...current};
delete newValue[value];
return newValue;
});
}
},
[usersReacted],
);

return (
<Reactions palette={{options}} reactions={reactions} onClickReaction={onClickReaction} />
);
};
```

For more code examples go to [Reactions.stories.tsx](https://github.com/gravity-ui/components/blob/main/src/components/Reactions/__stories__/Reactions.stories.tsx).

### Props

**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`:

| 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. |
112 changes: 112 additions & 0 deletions src/components/Reactions/Reaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';

import {Button, ButtonSize, PaletteOption, PopoverProps, Popup} from '@gravity-ui/uikit';

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

import {useReactionsContext} from './context';
import {useReactionsPopup} from './hooks';

export interface ReactionProps extends PaletteOption {
/**
* Should be true when the user used this reaction.
*/
selected?: boolean;
/**
* Display a number after the icon.
* Represents the number of users who used this reaction.
*/
counter?: React.ReactNode;
/**
* If present, when a user hovers over the reaction, a popover appears with `tooltip.content`.
* Can be used to display users who used this reaction.
*/
tooltip?: ReactionTooltipProps;
}

export interface ReactionTooltipProps
extends Pick<PopoverProps, 'strategy' | 'placement' | 'modifiers'> {
/**
* Tooltip's content.
*/
content: React.ReactNode;
/**
* Tooltip content's HTML class attribute.
*/
className?: string;
/**
* 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.
*/
canClosePopup?: () => boolean;
}

interface ReactionInnerProps {
reaction: ReactionProps;
size: ButtonSize;
onClick?: (value: string) => void;
}

const popupDefaultPlacement: PopoverProps['placement'] = [
'bottom-start',
'bottom',
'bottom-end',
'top-start',
'top',
'top-end',
];

const b = block('reactions');

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

const onClickCallback = useStableCallback(() => onClick?.(value));
Ruminat marked this conversation as resolved.
Show resolved Hide resolved

const buttonRef = React.useRef<HTMLButtonElement>(null);
const {onMouseEnter, onMouseLeave} = useReactionsPopup(props.reaction, buttonRef);
const {openedTooltip: currentHoveredReaction} = useReactionsContext();

const button = (
<Button
className={b('reaction-button', {size})}
ref={buttonRef}
disabled={disabled}
size={size}
selected={selected}
view="outlined"
extraProps={{value}}
onClick={onClickCallback}
>
<Button.Icon>{content}</Button.Icon>
{counter === undefined || counter === null ? null : (
<span className={b('reaction-button-text', {size})}>{counter}</span>
)}
</Button>
);

return tooltip ? (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{button}

{currentHoveredReaction && currentHoveredReaction.reaction.value === value ? (
<Popup
anchorRef={currentHoveredReaction.ref}
contentClassName={b('popup', tooltip.className)}
placement={tooltip.placement ?? popupDefaultPlacement}
strategy={tooltip.strategy}
modifiers={tooltip.modifiers}
open={currentHoveredReaction.open}
hasArrow
>
{tooltip.content}
</Popup>
) : null}
</div>
) : (
button
);
}
49 changes: 49 additions & 0 deletions src/components/Reactions/Reactions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@use '../variables';

$block: '.#{variables.$ns}reactions';

#{$block} {
&__reaction-button_add-button {
color: var(--g-color-text-secondary);
Ruminat marked this conversation as resolved.
Show resolved Hide resolved
}

&__popup {
padding: 8px;
}

&__add-reaction-popover {
max-width: unset;
}

&__reaction-button_size_xs {
font-size: 12px;
}
&__reaction-button_size_s {
font-size: 16px;
}
&__reaction-button_size_m {
font-size: 16px;
}
&__reaction-button_size_l {
font-size: 16px;
}
&__reaction-button_size_xl {
Ruminat marked this conversation as resolved.
Show resolved Hide resolved
font-size: 20px;
}

&__reaction-button-text_size_xs {
font-size: 10px;
}
&__reaction-button-text_size_s {
font-size: 12px;
Ruminat marked this conversation as resolved.
Show resolved Hide resolved
}
&__reaction-button-text_size_m {
font-size: 13px;
}
&__reaction-button-text_size_l {
font-size: 14px;
}
&__reaction-button-text_size_xl {
font-size: 16px;
}
}
Loading
Loading