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 all 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
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
/src/components/FilePreview @KirillDyachkovskiy
/src/components/FormRow @ogonkov
/src/components/HelpPopover @Raubzeug
/src/components/Notifications @Ruminat
/src/components/OnboardingMenu @nikita-jpg
/src/components/PlaceholderContainer @Marginy605
/src/components/PromoSheet @Avol-V
/src/components/Reactions @Ruminat
/src/components/SharePopover @niktverd
/src/components/StoreBadge @NikitaCG
/src/components/Stories @darkgenius
Expand Down
108 changes: 108 additions & 0 deletions src/components/Reactions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
## 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 {ReactionState, 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 ReactionState
const reactions = React.useMemo(
() =>
Object.entries(usersReacted).map(
([value, users]): ReactionState => ({
value,
counter: users.length,
selected: users.some(({name}) => name === currentUser.name),
}),
),
[usersReacted],
);

// You can then handle clicking on a reaction with changing the inital mapping,
// and the array of ReactionState will change accordingly
const onToggle = 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} onToggle={onToggle} />
);
};
```

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 |
| :--------------- | :------------------------------------------ | :------: | :------ | :--------------------------------------------------------------------------------------------- |
| `className` | `string` | | | HTML `class` attribute |
| `onToggle` | `(value: string) => void` | | | Fires when a user clicks on a Reaction (in a Palette or in the Reactions' list) |
| `paletteProps` | `ReactionsPaletteProps` | `true` | | Notifications' palette props — it's a `Palette` component with available reactions to the user |
| `qa` | `string` | | | `qa` attribute for testing |
| `reactions` | `PaletteOption[]` | `true` | | List of all available reactions |
| `reactionsState` | `ReactionState[]` | `true` | | List of reactions that were used |
| `readOnly` | `boolean` | | `false` | readOnly state (usage example: only signed in users can react) |
| `renderTooltip` | `(state: ReactionState) => React.ReactNode` | | | Reaction's tooltip with the list of reacted users for example |
| `size` | `ButtonSize` | | `m` | Buttons's size |
| `style` | `React.CSSProperties` | | | HTML `style` attribute |

**ReactionState** (single reaction props):

| Property | Type | Required | Default | Description |
| :--------- | :---------------- | :------: | :------ | :-------------------------------- |
| `counter` | `React.ReactNode` | | | How many users used this reaction |
| `selected` | `boolean` | | | Is reaction selected by the user |
| `value` | `string` | | | Reaction's unique value (ID) |
94 changes: 94 additions & 0 deletions src/components/Reactions/Reaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';

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

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

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

export type ReactionProps = Pick<PaletteOption, 'value' | 'content' | 'title'>;

export interface ReactionState {
/**
* Reaction's unique value (ID).
*/
value: string;
/**
* 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;
}

interface ReactionInnerProps extends Pick<PaletteOption, 'content'> {
reaction: ReactionState;
size: ButtonSize;
tooltip?: React.ReactNode;
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, selected, counter} = props.reaction;
const {size, content, tooltip, onClick} = props;

const onClickCallback = React.useCallback(() => onClick?.(value), [onClick, value]);

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

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

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

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

#{$block} {
&__popup {
padding: 8px;
}

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

&__reaction-button-content_size_xs {
font-size: 12px;
}
&__reaction-button-content_size_xs#{&}__reaction-button-content_text {
font-size: var(--g-text-caption-1-font-size);
}

&__reaction-button-content_size_s {
font-size: 16px;
}
&__reaction-button-content_size_s#{&}__reaction-button-content_text {
font-size: var(--g-text-caption-2-font-size);
}

&__reaction-button-content_size_m {
font-size: 16px;
}
&__reaction-button-content_size_m#{&}__reaction-button-content_text {
font-size: var(--g-text-body-1-font-size);
}

&__reaction-button-content_size_l {
font-size: 16px;
}
&__reaction-button-content_size_l#{&}__reaction-button-content_text {
font-size: var(--g-text-subheader-1-font-size);
}

&__reaction-button-content_size_xl {
font-size: 20px;
}
&__reaction-button-content_size_xl#{&}__reaction-button-content_text {
font-size: var(--g-text-subheader-2-font-size);
}
}
Loading
Loading