Update reactions to new design (#2751)

* Replace search strings

* Add expander for reactions view

* lint

* Remove useless list

* Update tests

* lint

* Only shrink buttons on web version, mobile never shrinks

* Revert accidental change.

* Remove border from button

* Tidy up views.

* Finish matching designs.

* Fix height

* Remove a unneeded class

* Remove more cruft

* Remove unnessacery chunk

* Evenly space reactions

* lint

* Disable reaction buttons when busy

* Try to make menu a bit more responsive

* Update test

* further screen size tweaks
This commit is contained in:
Will Hunt 2024-11-15 16:02:06 +00:00 committed by GitHub
parent c1208ac7b6
commit eed1b98cf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 119 additions and 275 deletions

View File

@ -4,19 +4,19 @@
},
"action": {
"close": "Close",
"close_search": "Close search",
"copy_link": "Copy link",
"edit": "Edit",
"go": "Go",
"invite": "Invite",
"lower_hand": "Lower hand",
"no": "No",
"open_search": "Open search",
"pick_reaction": "Pick reaction",
"raise_hand": "Raise hand",
"raise_hand_or_send_reaction": "Raise hand or send reaction",
"register": "Register",
"remove": "Remove",
"show_less": "Show less",
"show_more": "Show more",
"sign_in": "Sign in",
"sign_out": "Sign out",
"submit": "Submit",
@ -62,7 +62,6 @@
"preferences": "Preferences",
"profile": "Profile",
"reaction": "Reaction",
"search": "Search",
"settings": "Settings",
"something_went_wrong": "Something went wrong",
"unencrypted": "Not encrypted",
@ -128,7 +127,6 @@
"rageshake_sending": "Sending…",
"rageshake_sending_logs": "Sending debug logs…",
"rageshake_sent": "Thanks!",
"reaction_search": "Search reactions…",
"recaptcha_caption": "This site is protected by ReCAPTCHA and the Google <2>Privacy Policy</2> and <6>Terms of Service</6> apply.<9></9>By clicking \"Register\", you agree to our <12>End User Licensing Agreement (EULA)</12>",
"recaptcha_dismissed": "Recaptcha dismissed",
"recaptcha_not_loaded": "Recaptcha not loaded",

View File

@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
.modal {
--inset-inline: 520px;
display: flex;
flex-direction: column;
}
@ -35,7 +36,7 @@ Please see LICENSE in the repository root for full details.
.drawer {
background: var(--cpd-color-bg-canvas-default);
inset-block-end: 0;
inset-inline: max(0px, calc((100% - 520px) / 2));
inset-inline: max(0px, calc((100% - var(--inset-inline)) / 2));
max-block-size: 90%;
border-start-start-radius: var(--border-radius);
border-start-end-radius: var(--border-radius);

View File

@ -42,8 +42,9 @@ Please see LICENSE in the repository root for full details.
}
.overlay.animate {
--overlay-top: 50%;
left: 50%;
top: 50%;
top: var(--overlay-top);
transform: translate(-50%, -50%);
}

View File

@ -3,78 +3,99 @@
}
.reactionPopupMenu {
--reaction-button-padding: 10px;
--reaction-button-fontsize: 20px;
--reaction-button-gap: var(--cpd-separator-spacing);
display: flex;
width: fit-content;
}
.reactionPopupMenuModal {
width: fit-content !important;
top: 82vh !important;
@media (max-width: 420px) {
.reactionPopupMenu {
--reaction-button-padding: 8px;
--reaction-button-fontsize: 16px;
--reaction-button-gap: 6px;
}
}
.reactionPopupMenuModal > div > div {
padding-inline: var(--cpd-space-6x) !important;
padding-block: var(--cpd-space-6x) var(--cpd-space-8x) !important;
div.reactionPopupMenuRoot.reactionPopupMenuModal {
--overlay-top: 82vh;
width: fit-content;
}
.reactionPopupMenu menu {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: var(--cpd-separator-spacing);
div.reactionPopupMenuRoot {
/* Center the drawer */
--inset-inline: 30em;
}
.reactionPopupMenuRoot > div {
width: fit-content;
max-width: 100vw;
}
div.reactionPopupMenuRoot.reactionPopupMenuModal > div > div {
padding-inline: var(--cpd-space-6x);
padding-block: var(--cpd-space-6x);
}
.reactionPopupMenu section {
height: fit-content;
margin-top: auto;
margin-bottom: auto;
flex: 1;
max-width: fit-content;
}
.reactionPopupMenuItem {
list-style: none;
.reactionPopupMenu section.reactionsMenuSection {
margin: auto 0;
flex: auto;
}
.reactionsMenu {
min-height: 3em;
margin: 0;
padding: 0;
flex-grow: 1;
gap: var(--reaction-button-gap);
/* Height of 3 rows plus padding. */
max-height: calc(
((var(--reaction-button-fontsize) + var(--cpd-separator-spacing)) * 2) * 3
);
max-width: calc(
((var(--reaction-button-fontsize) + var(--cpd-separator-spacing)) * 2) * 5
);
overflow-x: hidden;
overflow-y: auto;
list-style: none;
flex-wrap: wrap;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: start;
align-items: auto;
align-content: start;
width: fit-content;
}
.reactionsMenu > * {
flex: 0 0 auto;
}
.reactionButton {
padding: 1em;
font-size: 1.6em;
width: 1.4em;
height: 1.4em;
padding: var(--reaction-button-padding);
border-radius: var(--cpd-radius-pill-effect);
}
@media (max-width: 800px) {
.reactionButton {
padding: 1em;
font-size: 1em;
width: 1em;
height: 1em;
font-size: var(--reaction-button-fontsize);
min-block-size: unset;
}
border: none;
aspect-ratio: 1 / 1;
height: 100%;
}
.verticalSeperator {
background-color: var(--cpd-color-gray-800);
width: 1px;
width: 2px;
height: auto;
margin-left: var(--cpd-separator-spacing);
margin-right: var(--cpd-separator-spacing);
}
.searchForm {
display: flex;
flex-direction: row;
gap: var(--cpd-separator-spacing);
margin-bottom: var(--cpd-space-3x);
}
.searchForm > label {
flex: auto;
}
.alert {
margin-bottom: var(--cpd-space-3x);
animation: grow-in 200ms;

View File

@ -5,8 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { fireEvent, render } from "@testing-library/react";
import { act } from "react";
import { render } from "@testing-library/react";
import { expect, test } from "vitest";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import { TooltipProvider } from "@vector-im/compound-web";
@ -122,7 +121,7 @@ test("Can react with emoji", async () => {
]);
});
test("Can search for and send emoji", async () => {
test("Can fully expand emoji picker", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
@ -130,9 +129,7 @@ test("Can search for and send emoji", async () => {
<TestComponent rtcSession={rtcSession} room={room} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
// Search should autofocus.
await user.keyboard("crickets");
await user.click(getByLabelText("action.show_more"));
expect(container).toMatchSnapshot();
await user.click(getByText("🦗"));
@ -152,38 +149,6 @@ test("Can search for and send emoji", async () => {
]);
});
test("Can search for and send emoji with the keyboard", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, getByPlaceholderText, container } = render(
<TestComponent rtcSession={rtcSession} room={room} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
const searchField = getByPlaceholderText("reaction_search");
// Search should autofocus.
await user.keyboard("crickets");
expect(container).toMatchSnapshot();
act(() => {
fireEvent.keyDown(searchField, { key: "Enter" });
});
expect(room.testSentEvents).toEqual([
[
undefined,
ElementCallReactionEventType,
{
"m.relates_to": {
event_id: memberEventAlice,
rel_type: "m.reference",
},
name: "crickets",
emoji: "🦗",
},
],
]);
});
test("Can close search", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
@ -192,23 +157,7 @@ test("Can close search", async () => {
<TestComponent rtcSession={rtcSession} room={room} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
await user.click(getByLabelText("action.close_search"));
expect(container).toMatchSnapshot();
});
test("Can close search with the escape key", async () => {
const user = userEvent.setup();
const room = new MockRoom(memberUserIdAlice);
const rtcSession = new MockRTCSession(room, membership);
const { getByLabelText, container, getByPlaceholderText } = render(
<TestComponent rtcSession={rtcSession} room={room} />,
);
await user.click(getByLabelText("action.raise_hand_or_send_reaction"));
await user.click(getByLabelText("action.open_search"));
const searchField = getByPlaceholderText("reaction_search");
act(() => {
fireEvent.keyDown(searchField, { key: "Escape" });
});
await user.click(getByLabelText("action.show_more"));
await user.click(getByLabelText("action.show_less"));
expect(container).toMatchSnapshot();
});

View File

@ -5,24 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import { Button as CpdButton, Tooltip, Alert } from "@vector-im/compound-web";
import {
Button as CpdButton,
Tooltip,
Search,
Form,
Alert,
} from "@vector-im/compound-web";
import {
SearchIcon,
CloseIcon,
RaisedHandSolidIcon,
ReactionIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import {
ChangeEventHandler,
ComponentPropsWithoutRef,
FC,
KeyboardEventHandler,
ReactNode,
useCallback,
useEffect,
@ -84,43 +76,11 @@ export function ReactionPopupMenu({
canReact: boolean;
}): ReactNode {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
const [isSearching, setIsSearching] = useState(false);
const onSearch = useCallback<ChangeEventHandler<HTMLInputElement>>((ev) => {
ev.preventDefault();
setSearchText(ev.target.value.trim().toLocaleLowerCase());
}, []);
const [isFullyExpanded, setExpanded] = useState(false);
const filteredReactionSet = useMemo(
() =>
ReactionSet.filter(
(reaction) =>
!isSearching ||
(!!searchText &&
(reaction.name.startsWith(searchText) ||
reaction.alias?.some((a) => a.startsWith(searchText)))),
).slice(0, 6),
[searchText, isSearching],
);
const onSearchKeyDown = useCallback<KeyboardEventHandler<never>>(
(ev) => {
if (ev.key === "Enter") {
ev.preventDefault();
if (!canReact) {
return;
}
if (filteredReactionSet.length !== 1) {
return;
}
sendReaction(filteredReactionSet[0]);
setIsSearching(false);
} else if (ev.key === "Escape") {
ev.preventDefault();
setIsSearching(false);
}
},
[sendReaction, filteredReactionSet, canReact, setIsSearching],
() => (isFullyExpanded ? ReactionSet : ReactionSet.slice(0, 5)),
[isFullyExpanded],
);
const label = isHandRaised ? t("action.lower_hand") : t("action.raise_hand");
return (
@ -148,35 +108,15 @@ export function ReactionPopupMenu({
</Tooltip>
</section>
<div className={styles.verticalSeperator} />
<section>
{isSearching ? (
<>
<Form.Root className={styles.searchForm}>
<Search
required
value={searchText}
name="reactionSearch"
placeholder={t("reaction_search")}
onChange={onSearch}
onKeyDown={onSearchKeyDown}
// This is a reasonable use of autofocus, we are focusing when
// the search button is clicked (which matches the Element Web reaction picker)
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
<CpdButton
Icon={CloseIcon}
aria-label={t("action.close_search")}
size="sm"
kind="destructive"
onClick={() => setIsSearching(false)}
/>
</Form.Root>
</>
) : null}
<menu className={styles.reactionsMenu}>
<section className={styles.reactionsMenuSection}>
<menu
className={classNames(
isFullyExpanded && styles.reactionsMenuExpanded,
styles.reactionsMenu,
)}
>
{filteredReactionSet.map((reaction) => (
<li className={styles.reactionPopupMenuItem} key={reaction.name}>
<li key={reaction.name}>
<Tooltip label={reaction.name}>
<CpdButton
kind="secondary"
@ -191,21 +131,23 @@ export function ReactionPopupMenu({
))}
</menu>
</section>
{!isSearching ? (
<section style={{ marginLeft: "var(--cpd-separator-spacing)" }}>
<li key="search" className={styles.reactionPopupMenuItem}>
<Tooltip label={t("common.search")}>
<Tooltip
label={
isFullyExpanded ? t("action.show_less") : t("action.show_more")
}
>
<CpdButton
iconOnly
aria-label={t("action.open_search")}
Icon={SearchIcon}
aria-label={
isFullyExpanded ? t("action.show_less") : t("action.show_more")
}
Icon={isFullyExpanded ? ChevronUpIcon : ChevronDownIcon}
kind="tertiary"
onClick={() => setIsSearching(true)}
onClick={() => setExpanded(!isFullyExpanded)}
/>
</Tooltip>
</li>
</section>
) : null}
</div>
</>
);
@ -335,12 +277,13 @@ export function ReactionToggleButton({
title={t("action.pick_reaction")}
hideHeader
classNameModal={styles.reactionPopupMenuModal}
className={styles.reactionPopupMenuRoot}
onDismiss={() => setShowReactionsMenu(false)}
>
<ReactionPopupMenu
errorText={errorText}
isHandRaised={isHandRaised}
canReact={canReact}
canReact={!busy && canReact}
sendReaction={(reaction) => void sendRelation(reaction)}
toggleRaisedHand={toggleRaisedHand}
/>

View File

@ -10,7 +10,7 @@ exports[`Can close search 1`] = `
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":rec:"
aria-labelledby=":r9l:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
@ -36,16 +36,19 @@ exports[`Can close search 1`] = `
</div>
`;
exports[`Can close search with the escape key 1`] = `
<div>
exports[`Can fully expand emoji picker 1`] = `
<div
aria-hidden="true"
data-aria-hidden="true"
>
<button
aria-disabled="false"
aria-expanded="false"
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":rhh:"
aria-labelledby=":r6c:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
@ -76,7 +79,7 @@ exports[`Can lower hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r3i:"
aria-labelledby=":r36:"
class="_button_i91xf_17 raisedButton _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
@ -142,7 +145,7 @@ exports[`Can raise hand 1`] = `
aria-expanded="false"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r1p:"
aria-labelledby=":r1j:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="secondary"
data-size="lg"
@ -167,75 +170,3 @@ exports[`Can raise hand 1`] = `
</button>
</div>
`;
exports[`Can search for and send emoji 1`] = `
<div
aria-hidden="true"
data-aria-hidden="true"
>
<button
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":r74:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.536 15.536a1 1 0 0 0-1.415-1.415 2.987 2.987 0 0 1-2.12.879 2.988 2.988 0 0 1-2.122-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
/>
</svg>
</button>
</div>
`;
exports[`Can search for and send emoji with the keyboard 1`] = `
<div
aria-hidden="true"
data-aria-hidden="true"
>
<button
aria-disabled="false"
aria-expanded="true"
aria-haspopup="true"
aria-label="action.raise_hand_or_send_reaction"
aria-labelledby=":ra3:"
class="_button_i91xf_17 _has-icon_i91xf_66 _icon-only_i91xf_59"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.536 15.536a1 1 0 0 0-1.415-1.415 2.987 2.987 0 0 1-2.12.879 2.988 2.988 0 0 1-2.122-.879 1 1 0 1 0-1.414 1.415A4.987 4.987 0 0 0 12 17c1.38 0 2.632-.56 3.536-1.464ZM10 10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm5.5 1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
/>
<path
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10Zm-2 0a8 8 0 1 0-16 0 8 8 0 0 0 16 0Z"
/>
</svg>
</button>
</div>
`;