Update reaction picker visuals

This commit is contained in:
Evan Hahn 2021-09-07 16:30:58 -05:00 committed by GitHub
parent 1a3f87f7f6
commit 561bc0695f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 277 additions and 194 deletions

View File

@ -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"
},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View 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;
}
}

View File

@ -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';

View File

@ -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>
);
}

View File

@ -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>
);