Update reaction picker visuals
This commit is contained in:
parent
1a3f87f7f6
commit
561bc0695f
10 changed files with 277 additions and 194 deletions
|
@ -3136,11 +3136,15 @@
|
|||
"message": "Please set up Signal on your phone and desktop to use the Sticker Pack Creator",
|
||||
"description": "The error message which appears when the user has not linked their account and attempts to use the Sticker Creator"
|
||||
},
|
||||
"Reactions--remove": {
|
||||
"message": "Remove reaction",
|
||||
"describe": "Shown when you want to remove a reaction you've made"
|
||||
},
|
||||
"Reactions--error": {
|
||||
"message": "Failed to send reaction. Please try again.",
|
||||
"description": "Shown when a reaction fails to send"
|
||||
},
|
||||
"ReactionsViewer--more": {
|
||||
"Reactions--more": {
|
||||
"message": "More",
|
||||
"description": "Use in the reaction picker as the alt text for the 'more' button"
|
||||
},
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" fill="#4a4a4a" r="16"/><g fill="#e9e9e9"><circle cx="8" cy="16" r="2"/><circle cx="16" cy="16" r="2"/><circle cx="24" cy="16" r="2"/></g></svg>
|
Before Width: | Height: | Size: 262 B |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><circle cx="16" cy="16" fill="#2e2e2e" r="16"/><g fill="#e9e9e9"><circle cx="8" cy="16" r="2"/><circle cx="16" cy="16" r="2"/><circle cx="24" cy="16" r="2"/></g></svg>
|
Before Width: | Height: | Size: 262 B |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16 32c8.8366 0 16-7.1634 16-16 0-8.83656-7.1634-16-16-16-8.83656 0-16 7.16344-16 16 0 8.8366 7.16344 16 16 16z" fill="#676767"/><g fill="#e9e9e9"><path d="m8 18c1.10457 0 2-.8954 2-2s-.89543-2-2-2-2 .8954-2 2 .89543 2 2 2z"/><path d="m16 18c1.1046 0 2-.8954 2-2s-.8954-2-2-2-2 .8954-2 2 .8954 2 2 2z"/><path d="m24 18c1.1046 0 2-.8954 2-2s-.8954-2-2-2-2 .8954-2 2 .8954 2 2 2z"/></g></svg>
|
Before Width: | Height: | Size: 494 B |
|
@ -1 +0,0 @@
|
|||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="m16 32c8.8366 0 16-7.1634 16-16 0-8.83656-7.1634-16-16-16-8.83656 0-16 7.16344-16 16 0 8.8366 7.16344 16 16 16z" fill="#4a4a4a"/><g fill="#e9e9e9"><path d="m8 18c1.10457 0 2-.8954 2-2s-.89543-2-2-2-2 .8954-2 2 .89543 2 2 2z"/><path d="m16 18c1.1046 0 2-.8954 2-2s-.8954-2-2-2-2 .8954-2 2 .8954 2 2 2z"/><path d="m24 18c1.1046 0 2-.8954 2-2s-.8954-2-2-2-2 .8954-2 2 .8954 2 2 2z"/></g></svg>
|
Before Width: | Height: | Size: 494 B |
|
@ -4684,142 +4684,6 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
// Module: Reaction Picker
|
||||
|
||||
@keyframes module-reaction-picker__background-fade {
|
||||
from {
|
||||
background: transparent;
|
||||
}
|
||||
to {
|
||||
// This color is the same in both light and dark themes
|
||||
background: rgba($color-black, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes module-reaction-picker__emoji-fade {
|
||||
from {
|
||||
transform: translate3d(0, 24px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.module-reaction-picker {
|
||||
width: 320px;
|
||||
height: 56px;
|
||||
border-radius: 30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
animation: {
|
||||
name: module-reaction-picker__background-fade;
|
||||
duration: 400ms;
|
||||
timing-function: $ease-out-expo;
|
||||
fill-mode: forwards;
|
||||
}
|
||||
|
||||
&__emoji-btn {
|
||||
@include button-reset;
|
||||
display: flex;
|
||||
min-width: 52px;
|
||||
min-height: 52px;
|
||||
border-radius: 52px;
|
||||
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
|
||||
@for $i from 0 through 6 {
|
||||
&:nth-of-type(#{$i + 1}) {
|
||||
left: 2px + ($i * 44px);
|
||||
|
||||
// Prevent animation jank
|
||||
opacity: 0;
|
||||
|
||||
animation: {
|
||||
name: module-reaction-picker__emoji-fade;
|
||||
duration: 400ms;
|
||||
timing-function: $ease-out-expo;
|
||||
delay: #{$i * 10ms};
|
||||
fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transition: background 400ms $ease-out-expo;
|
||||
&--selected {
|
||||
// This color is the same in both light and dark themes
|
||||
background: rgba($color-white, 0.3);
|
||||
}
|
||||
&--more {
|
||||
@include light-theme {
|
||||
background: url('../images/any-emoji-32-light.svg') no-repeat center;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: url('../images/any-emoji-32-dark.svg') no-repeat center;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
opacity: 0;
|
||||
transition: opacity 400ms $ease-out-expo;
|
||||
|
||||
@include light-theme {
|
||||
background: url('../images/any-emoji-32-light-hover.svg') no-repeat
|
||||
center;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: url('../images/any-emoji-32-dark-hover.svg') no-repeat
|
||||
center;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: $color-ultramarine;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: calc(50% - 2px);
|
||||
}
|
||||
}
|
||||
|
||||
$emoji-btn: &;
|
||||
|
||||
&__emoji {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
transform-origin: center;
|
||||
$scale: 32 / 48;
|
||||
transform: scale3d($scale, $scale, $scale);
|
||||
transition: transform 400ms $ease-out-expo;
|
||||
|
||||
#{$emoji-btn}:hover &,
|
||||
.keyboard-mode #{$emoji-btn}:focus & {
|
||||
transform: scale3d(1, 1, 1) translate3d(0, -24px, 0);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Module: Calling
|
||||
.module-calling {
|
||||
&__container {
|
||||
|
|
182
stylesheets/components/ReactionPicker.scss
Normal file
182
stylesheets/components/ReactionPicker.scss
Normal file
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-ReactionPicker {
|
||||
$button-size: 40px;
|
||||
$button-content-size: 28px;
|
||||
$max-expected-buttons: 7;
|
||||
|
||||
$emoji-size-from-component: 48px;
|
||||
$big-emoji-size: 42px;
|
||||
|
||||
@include rounded-corners;
|
||||
align-items: center;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
box-shadow: 0 1px 4px $color-black-alpha-05, 0 10px 16px $color-black-alpha-20;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
padding: 3px 7px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
z-index: 2;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: {
|
||||
name: module-ReactionPicker__appear;
|
||||
duration: 400ms;
|
||||
timing-function: $ease-out-expo;
|
||||
fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background: $color-white;
|
||||
border-color: $color-black-alpha-05;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-75;
|
||||
border-color: $color-gray-80;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include button-reset;
|
||||
align-items: center;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
// Prevent animation jank
|
||||
opacity: 0;
|
||||
|
||||
animation: {
|
||||
name: module-ReactionPicker__button-appear;
|
||||
duration: 400ms;
|
||||
timing-function: $ease-out-expo;
|
||||
fill-mode: forwards;
|
||||
// This delay is a fallback in case there are more than the expected number of
|
||||
// buttons.
|
||||
delay: #{$max-expected-buttons * 10ms};
|
||||
}
|
||||
}
|
||||
@for $i from 0 through $max-expected-buttons {
|
||||
&:nth-of-type(#{$i + 1}) {
|
||||
animation-delay: #{$i * 10ms};
|
||||
}
|
||||
}
|
||||
|
||||
&--emoji {
|
||||
$emoji-button-selector: &;
|
||||
|
||||
height: $button-size;
|
||||
width: $button-size;
|
||||
|
||||
.module-emoji {
|
||||
transform: scale($button-content-size / $emoji-size-from-component);
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 400ms $ease-out-expo;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focused-emoji {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.module-emoji {
|
||||
transform: scale($big-emoji-size / $emoji-size-from-component)
|
||||
translateY(-16px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include focused-emoji;
|
||||
}
|
||||
|
||||
@include keyboard-mode {
|
||||
&:focus {
|
||||
@include focused-emoji;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--more {
|
||||
// The margin makes the button take up the same space as the other buttons, while
|
||||
// not actually being as large.
|
||||
height: $button-content-size;
|
||||
margin: ($button-size - $button-content-size) / 2;
|
||||
width: $button-content-size;
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: background 200ms $ease-out-expo;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-02;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-60;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-45;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
border-radius: 100%;
|
||||
height: 3px;
|
||||
margin-right: 4px;
|
||||
width: 3px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-45;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--selected {
|
||||
@include light-theme {
|
||||
background: $color-black-alpha-20;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-white-alpha-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes module-ReactionPicker__appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes module-ReactionPicker__button-appear {
|
||||
from {
|
||||
transform: translate3d(0, 24px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -27,8 +27,8 @@
|
|||
|
||||
// New style: components
|
||||
@import './components/AddGroupMembersModal.scss';
|
||||
@import './components/App.scss';
|
||||
@import './components/AnnouncementsOnlyGroupBanner.scss';
|
||||
@import './components/App.scss';
|
||||
@import './components/Avatar.scss';
|
||||
@import './components/AvatarEditor.scss';
|
||||
@import './components/AvatarModalButtons.scss';
|
||||
|
@ -69,6 +69,7 @@
|
|||
@import './components/Modal.scss';
|
||||
@import './components/Preferences.scss';
|
||||
@import './components/ProfileEditor.scss';
|
||||
@import './components/ReactionPicker.scss';
|
||||
@import './components/SafetyNumberChangeDialog.scss';
|
||||
@import './components/SafetyNumberViewer.scss';
|
||||
@import './components/SearchInput.scss';
|
||||
|
|
|
@ -34,6 +34,34 @@ const DEFAULT_EMOJI_LIST = [
|
|||
'cry',
|
||||
];
|
||||
|
||||
const EmojiButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
{
|
||||
emoji: string;
|
||||
onSelect: () => unknown;
|
||||
selected: boolean;
|
||||
title?: string;
|
||||
}
|
||||
>(({ emoji, onSelect, selected, title }, ref) => (
|
||||
<button
|
||||
type="button"
|
||||
key={emoji}
|
||||
ref={ref}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-ReactionPicker__button',
|
||||
'module-ReactionPicker__button--emoji',
|
||||
selected && 'module-ReactionPicker__button--selected'
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
<Emoji size={48} emoji={emoji} title={title} />
|
||||
</button>
|
||||
));
|
||||
|
||||
export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{ i18n, selected, onClose, skinTone, onPick, renderEmojiPicker, style },
|
||||
|
@ -64,70 +92,69 @@ export const ReactionPicker = React.forwardRef<HTMLDivElement, Props>(
|
|||
[onPick]
|
||||
);
|
||||
|
||||
// Focus first button and restore focus on unmount
|
||||
const [focusRef] = useRestoreFocus();
|
||||
|
||||
if (pickingOther) {
|
||||
return renderEmojiPicker({ onPickEmoji, onClose, style, ref });
|
||||
}
|
||||
|
||||
const emojis = DEFAULT_EMOJI_LIST.map(shortName =>
|
||||
convertShortName(shortName, skinTone)
|
||||
);
|
||||
|
||||
// Focus first button and restore focus on unmount
|
||||
const [focusRef] = useRestoreFocus();
|
||||
|
||||
const otherSelected = selected && !emojis.includes(selected);
|
||||
|
||||
return pickingOther ? (
|
||||
renderEmojiPicker({ onPickEmoji, onClose, style, ref })
|
||||
) : (
|
||||
<div ref={ref} style={style} className="module-reaction-picker">
|
||||
let moreButton: React.ReactNode;
|
||||
if (otherSelected) {
|
||||
moreButton = (
|
||||
<EmojiButton
|
||||
emoji={selected}
|
||||
onSelect={() => {
|
||||
onPick(selected);
|
||||
}}
|
||||
selected
|
||||
title={i18n('Reactions--remove')}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
moreButton = (
|
||||
<button
|
||||
aria-label={i18n('ReactionsViewer--more')}
|
||||
className="module-ReactionPicker__button module-ReactionPicker__button--more"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
setPickingOther(true);
|
||||
}}
|
||||
tabIndex={0}
|
||||
title={i18n('ReactionsViewer--more')}
|
||||
type="button"
|
||||
>
|
||||
<div className="module-ReactionPicker__button--more__dot" />
|
||||
<div className="module-ReactionPicker__button--more__dot" />
|
||||
<div className="module-ReactionPicker__button--more__dot" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style} className="module-ReactionPicker">
|
||||
{emojis.map((emoji, index) => {
|
||||
const maybeFocusRef = index === 0 ? focusRef : undefined;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<EmojiButton
|
||||
emoji={emoji}
|
||||
key={emoji}
|
||||
ref={maybeFocusRef}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-reaction-picker__emoji-btn',
|
||||
emoji === selected
|
||||
? 'module-reaction-picker__emoji-btn--selected'
|
||||
: null
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onSelect={() => {
|
||||
onPick(emoji);
|
||||
}}
|
||||
title={emoji}
|
||||
>
|
||||
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||
<Emoji size={48} emoji={emoji} />
|
||||
</div>
|
||||
</button>
|
||||
ref={maybeFocusRef}
|
||||
selected={emoji === selected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-reaction-picker__emoji-btn',
|
||||
otherSelected
|
||||
? 'module-reaction-picker__emoji-btn--selected'
|
||||
: 'module-reaction-picker__emoji-btn--more'
|
||||
)}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (otherSelected && selected) {
|
||||
onPick(selected);
|
||||
} else {
|
||||
setPickingOther(true);
|
||||
}
|
||||
}}
|
||||
title={i18n('ReactionsViewer--more')}
|
||||
>
|
||||
{otherSelected ? (
|
||||
<div className="module-reaction-picker__emoji-btn__emoji">
|
||||
<Emoji size={48} emoji={selected} />
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
{moreButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ export type OwnProps = {
|
|||
skinTone?: SkinToneKey | number;
|
||||
size?: EmojiSizeType;
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type Props = OwnProps &
|
||||
|
@ -27,7 +28,15 @@ export type Props = OwnProps &
|
|||
export const Emoji = React.memo(
|
||||
React.forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{ style = {}, size = 28, shortName, skinTone, emoji, className }: Props,
|
||||
{
|
||||
className,
|
||||
emoji,
|
||||
shortName,
|
||||
size = 28,
|
||||
skinTone,
|
||||
style = {},
|
||||
title,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
let image = '';
|
||||
|
@ -50,8 +59,8 @@ export const Emoji = React.memo(
|
|||
<img
|
||||
className={`module-emoji__image--${size}px`}
|
||||
src={image}
|
||||
aria-label={emoji}
|
||||
title={emoji}
|
||||
aria-label={title ?? emoji}
|
||||
title={title ?? emoji}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue