generated from gravity-ui/package-example
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Reactions): added new Reactions component (#197)
- Loading branch information
Showing
15 changed files
with
881 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.