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): add addButtonPlacement property #215

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 21 additions & 23 deletions src/components/Reactions/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
## Reactions

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

### Usage example

```typescript
import React from 'react';

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

const user = {
spongeBob: {name: 'Sponge Bob'},
Expand All @@ -20,18 +19,18 @@ const currentUser = user.spongeBob;
const option = {
'thumbs-up': {content: '👍', value: 'thumbs-up'},
cool: {content: '😎', value: 'cool'},
} satisfies Record<string, PaletteOption>;
} satisfies Record<string, ReactionProps>;

const options = Object.values(option);
const options: ReactionProps[] = Object.values(option);

const YourComponent = () => {
export 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(
const reactionsState = React.useMemo(
() =>
Object.entries(usersReacted).map(
([value, users]): ReactionState => ({
Expand All @@ -44,7 +43,7 @@ const YourComponent = () => {
);

// You can then handle clicking on a reaction with changing the inital mapping,
// and the array of ReactionState will change accordingly
// and the reactionsState array will change accordingly
const onToggle = React.useCallback(
(value: string) => {
if (!usersReacted[value]) {
Expand Down Expand Up @@ -74,9 +73,7 @@ const YourComponent = () => {
[usersReacted],
);

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

Expand All @@ -86,18 +83,19 @@ For more code examples go to [Reactions.stories.tsx](https://github.com/gravity-

**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 |
| Property | Type | Required | Default | Description |
| :------------------- | :------------------------------------------ | :------: | :------ | :--------------------------------------------------------------------------------------------- |
| `addButtonPlacement` | `'start' or 'end'` | | `'end'` | Position of the "Add reaction" button. |
| `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):

Expand Down
54 changes: 32 additions & 22 deletions src/components/Reactions/Reactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export interface ReactionsProps extends Pick<PaletteProps, 'size'>, QAProps, DOM
* Reactions' readonly state (when a user is unable to react for some reason).
*/
readOnly?: boolean;
/**
* Position of the "Add reaction" button.
*
* @default 'end'
*/
addButtonPlacement?: 'start' | 'end';
/**
* If present, when a user hovers over the reaction, a popover appears with renderTooltip(state) content.
* Can be used to display users who used this reaction.
Expand Down Expand Up @@ -74,6 +80,7 @@ export function Reactions({
paletteProps,
readOnly,
qa,
addButtonPlacement = 'end',
renderTooltip,
onToggle,
}: ReactionsProps) {
Expand Down Expand Up @@ -122,6 +129,28 @@ export function Reactions({
[paletteProps, reactions, paletteValue, size, onUpdatePalette],
);

const addReactionButton = readOnly ? null : (
<Popover
content={paletteContent}
tooltipContentClassName={b('add-reaction-popover')}
openOnHover={false}
hasArrow={false}
focusTrap
autoFocus
>
<Button
className={b('reaction-button')}
size={size}
extraProps={{'aria-label': i18n('add-reaction')}}
view="flat-secondary"
>
<Button.Icon>
<Icon data={FaceSmile} size={buttonSizeToIconSize[size]} />
</Button.Icon>
</Button>
</Popover>
);

return (
<ReactionsContextProvider
value={{
Expand All @@ -130,6 +159,8 @@ export function Reactions({
}}
>
<Flex className={b(null, className)} style={style} gap={1} wrap={true} qa={qa}>
{addButtonPlacement === 'start' ? addReactionButton : null}

{/* Reactions' list */}
{reactionsState.map((reaction) => {
const content = paletteOptionsMap[reaction.value]?.content ?? '?';
Expand All @@ -146,28 +177,7 @@ export function Reactions({
);
})}

{/* Add reaction button */}
{readOnly ? null : (
<Popover
content={paletteContent}
tooltipContentClassName={b('add-reaction-popover')}
openOnHover={false}
hasArrow={false}
focusTrap
autoFocus
>
<Button
className={b('reaction-button')}
size={size}
extraProps={{'aria-label': i18n('add-reaction')}}
view="flat-secondary"
>
<Button.Icon>
<Icon data={FaceSmile} size={buttonSizeToIconSize[size]} />
</Button.Icon>
</Button>
</Popover>
)}
{addButtonPlacement === 'end' ? addReactionButton : null}
</Flex>
</ReactionsContextProvider>
);
Expand Down
15 changes: 15 additions & 0 deletions src/components/Reactions/__stories__/Reactions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,18 @@ export const Size: StoryFn = () => {
</Flex>
);
};

export const AddButtonPlacement: StoryFn = () => {
return (
<Flex direction="column" gap={4}>
<Flex direction="column" gap={2}>
<Text variant="subheader-1">Start</Text>
<Reactions {...useMockReactions()} addButtonPlacement="start" />
</Flex>
<Flex direction="column" gap={2}>
<Text variant="subheader-1">End</Text>
<Reactions {...useMockReactions()} addButtonPlacement="end" />
</Flex>
</Flex>
);
};
Loading