Update composition area buttons and menus with axo components

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-05-14 01:57:43 -05:00
committed by GitHub
parent ed76164f5f
commit 3b6871f0a6
16 changed files with 113 additions and 389 deletions
-84
View File
@@ -13,90 +13,6 @@
align-items: center;
background: none;
&__microphone {
height: 32px;
width: 32px;
border-radius: 4px;
text-align: center;
background: none;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border: none;
@include mixins.keyboard-mode {
&:focus {
outline: 2px solid variables.$color-ultramarine;
}
}
outline: none;
&:before {
content: '';
display: inline-block;
height: 20px;
width: 20px;
@include mixins.color-svg(
'../images/icons/v3/mic/mic.svg',
var(--color-label-primary)
);
}
}
&__recorder-button {
flex-grow: 0;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 32px;
opacity: 0.3;
text-align: center;
padding: 0;
&:focus,
&:hover {
opacity: 1;
}
outline: none;
.icon {
display: inline-block;
width: 24px;
height: 24px;
margin-bottom: -3px;
}
&--complete {
background: color.adjust(variables.$color-accent-green, $lightness: 20%);
border: 1px solid variables.$color-accent-green;
.icon {
@include mixins.color-svg(
'../images/icons/v3/check/check.svg',
variables.$color-accent-green
);
}
}
&--cancel {
background: color.adjust(variables.$color-accent-red, $lightness: 20%);
border: 1px solid variables.$color-accent-red;
.icon {
@include mixins.color-svg(
'../images/icons/v3/x/x.svg',
variables.$color-accent-red
);
}
}
}
&__time {
color: variables.$color-gray-60;
font-variant: tabular-nums;
@@ -99,26 +99,6 @@
}
}
&__send-button {
display: flex;
justify-content: center;
align-items: center;
background: none;
border: none;
width: 32px;
height: 32px;
&::after {
display: block;
content: '';
width: 20px;
height: 20px;
flex-shrink: 0;
@include mixins.color-svg(
'../images/icons/v3/send/send-fill.svg',
variables.$color-ultramarine
);
}
}
&__input {
flex-grow: 1;
position: relative;
@@ -44,44 +44,6 @@
justify-content: center;
}
&__send-button {
align-items: center;
border: none;
border-radius: 100%;
display: flex;
height: 32px;
justify-content: center;
width: 32px;
&::after {
content: '';
display: block;
flex-shrink: 0;
height: 20px;
width: 20px;
}
&--continue {
&::after {
height: 24px;
width: 24px;
@include mixins.color-svg(
'../images/icons/v3/arrow/arrow-right.svg',
variables.$color-white
);
}
}
&--forward {
&::after {
@include mixins.color-svg(
'../images/icons/v3/send/send-fill.svg',
variables.$color-white
);
}
}
}
// Disable vertical scrolling on the modal pages
// since the elements inside are scrollable themselves
.module-Modal__body {
@@ -1,76 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.MediaQualitySelector {
&__popper {
@include mixins.module-composition-popper;
& {
color: var(--color-label-primary);
padding-block: 12px;
padding-inline: 16px;
width: auto;
}
}
&__title {
@include mixins.font-body-1-bold;
margin-bottom: 12px;
}
&__option {
@include mixins.button-reset();
& {
align-items: center;
border-radius: 6px;
display: flex;
height: 42px;
margin-block: 2px;
margin-inline: 0;
min-width: 200px;
}
&--checkmark {
height: 12px;
margin-block: 0;
margin-inline: 6px;
width: 16px;
}
&--selected {
@include mixins.color-svg(
'../images/icons/v3/check/check-compact.svg',
currentColor
);
}
&--title {
@include mixins.font-body-2;
}
&--description {
@include mixins.font-subtitle;
}
&:hover {
@include mixins.light-theme() {
background-color: variables.$color-gray-05;
}
@include mixins.dark-theme() {
background-color: variables.$color-gray-65;
}
}
&:focus {
outline: none;
}
&:focus-visible {
box-shadow: 0 0 1px 1px variables.$color-ultramarine;
}
}
}
@@ -29,12 +29,4 @@
background: variables.$color-gray-75;
}
}
&__button {
font-size: 13px;
min-width: 76px;
line-height: 18px;
padding-block: 5px;
padding-inline: 16px;
}
}
@@ -35,7 +35,6 @@
flex-shrink: 0;
}
.FunButton__Icon--FunPicker,
.FunButton__Icon--EmojiPicker {
@include mixins.color-svg(
'../images/icons/v3/emoji/emoji.svg',
-1
View File
@@ -128,7 +128,6 @@ $is-storybook: false !default;
@use 'components/ListTile.scss';
@use 'components/LowDiskSpaceBackupImportModal.scss';
@use 'components/MediaEditor.scss';
@use 'components/MediaQualitySelector.scss';
@use 'components/MessageAudio.scss';
@use 'components/MessageBody.scss';
@use 'components/MessageTextRenderer.scss';
+13 -2
View File
@@ -1,7 +1,6 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, Ref, MouseEvent } from 'react';
import type { FC, Ref, MouseEvent, FocusEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { AxoSymbol } from './AxoSymbol.dom.tsx';
import { tw } from './tw.dom.tsx';
@@ -174,6 +173,14 @@ export namespace AxoIconButton {
* Called when the button is clicked. Not called when `pending` or `disabled`.
*/
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
/**
* Called when the mouse enters the button.
*/
onMouseEnter?: (event: MouseEvent<HTMLButtonElement>) => void;
/**
* Called when the mouse focuses the button.
*/
onFocus?: (event: FocusEvent<HTMLButtonElement>) => void;
}>;
/**
@@ -215,6 +222,8 @@ export namespace AxoIconButton {
pressed,
disabled,
onClick,
onMouseEnter,
onFocus,
...rest
} = props;
const intl = useAxoIntl();
@@ -249,6 +258,8 @@ export namespace AxoIconButton {
aria-pressed={pressed ?? undefined}
aria-disabled={(pending || disabled) ?? undefined}
onClick={handleClick}
onMouseEnter={onMouseEnter}
onFocus={onFocus}
className={tw(baseStyles, Variants.get(variant), Sizes.get(size))}
{...rest}
>
+10 -8
View File
@@ -870,11 +870,12 @@ export const CompositionArea = memo(function CompositionArea({
<div className="CompositionArea__placeholder" />
<div className="CompositionArea__button-cell">
<div className={actionSlotClassName}>
<button
type="button"
className="CompositionArea__send-button"
<AxoIconButton.Root
symbol="send-fill"
variant="primary"
size="md"
label={i18n('icu:sendMessageToContact')}
onClick={handleForceSend}
aria-label={i18n('icu:sendMessageToContact')}
/>
</div>
</div>
@@ -1294,11 +1295,12 @@ export const CompositionArea = memo(function CompositionArea({
{isViewOnceActive && (
<div className="CompositionArea__button-cell">
<div className={actionSlotClassName}>
<button
type="button"
className="CompositionArea__send-button"
<AxoIconButton.Root
size="md"
variant="primary"
symbol="send-fill"
label={i18n('icu:sendMessageToContact')}
onClick={handleForceSend}
aria-label={i18n('icu:sendMessageToContact')}
/>
</div>
</div>
+12 -8
View File
@@ -12,7 +12,6 @@ import {
} from 'react';
import { AttachmentList } from './conversation/AttachmentList.dom.tsx';
import type { AttachmentForUIType } from '../types/Attachment.std.ts';
import { Button } from './Button.dom.tsx';
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox.dom.tsx';
import type { Row } from './ConversationList.dom.tsx';
import { ConversationList, RowType } from './ConversationList.dom.tsx';
@@ -44,6 +43,7 @@ import { missingCaseError } from '../util/missingCaseError.std.ts';
import { Theme } from '../util/theme.std.ts';
import { Emoji } from '../axo/emoji.std.ts';
import { AxoConfirmDialog } from '../axo/AxoConfirmDialog.dom.tsx';
import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx';
export enum ForwardMessagesModalType {
Forward,
@@ -260,16 +260,20 @@ export function ForwardMessagesModal({
</div>
<div>
{isEditingMessage || !isLonelyDraftEditable ? (
<Button
aria-label={i18n('icu:ForwardMessageModal--continue')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--forward"
aria-disabled={!canForwardMessages}
<AxoIconButton.Root
size="md"
variant="primary"
symbol="send-fill"
label={i18n('icu:ForwardMessageModal--continue')}
disabled={!canForwardMessages}
onClick={forwardMessages}
/>
) : (
<Button
aria-label={i18n('icu:forwardMessage')}
className="module-ForwardMessageModal__send-button module-ForwardMessageModal__send-button--continue"
<AxoIconButton.Root
size="md"
variant="primary"
symbol="arrow-[end]"
label={i18n('icu:forwardMessage')}
disabled={!hasContactsSelected}
onClick={() => setIsEditingMessage(true)}
/>
-1
View File
@@ -1394,7 +1394,6 @@ export function MediaEditor({
i18n={i18n}
isHighQuality={localIsHighQuality}
onSelectQuality={handleSelectQuality}
theme={pickerTheme}
/>
</div>
)}
@@ -5,18 +5,20 @@ import type { JSX } from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { PropsType } from './MediaQualitySelector.dom.tsx';
import type { MediaQualitySelectorProps } from './MediaQualitySelector.dom.tsx';
import { MediaQualitySelector } from './MediaQualitySelector.dom.tsx';
export default {
title: 'Components/MediaQualitySelector',
argTypes: {},
args: {},
} satisfies Meta<PropsType>;
} satisfies Meta<MediaQualitySelectorProps>;
const { i18n } = window.SignalContext;
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
const createProps = (
overrideProps: Partial<MediaQualitySelectorProps> = {}
): MediaQualitySelectorProps => ({
conversationId: 'abc123',
i18n,
isHighQuality: overrideProps.isHighQuality ?? false,
+48 -117
View File
@@ -1,60 +1,45 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { KeyboardEvent, JSX } from 'react';
import { useCallback, useRef, useState } from 'react';
import classNames from 'classnames';
import { Popover } from 'radix-ui';
import type { JSX } from 'react';
import { useMemo, useCallback } from 'react';
import type { LocalizerType } from '../types/Util.std.ts';
import { ThemeType } from '../types/Util.std.ts';
import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx';
import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.tsx';
import { tw } from '../axo/tw.dom.tsx';
export type PropsType = {
export type MediaQualitySelectorProps = Readonly<{
conversationId: string;
i18n: LocalizerType;
isHighQuality: boolean;
onSelectQuality: (conversationId: string, isHQ: boolean) => unknown;
theme?: ThemeType;
};
onSelectQuality: (conversationId: string, isHighQuality: boolean) => unknown;
}>;
enum MediaQuality {
Standard = 'standard',
High = 'high',
}
export function MediaQualitySelector({
conversationId,
i18n,
isHighQuality,
onSelectQuality,
theme,
}: PropsType): JSX.Element {
const [open, setOpen] = useState(false);
const standardRef = useRef<HTMLButtonElement>(null);
const highRef = useRef<HTMLButtonElement>(null);
}: MediaQualitySelectorProps): JSX.Element {
const value = useMemo(() => {
return isHighQuality ? MediaQuality.High : MediaQuality.Standard;
}, [isHighQuality]);
const handleOpenAutoFocus = useCallback(
(e: Event) => {
e.preventDefault();
if (isHighQuality) {
highRef.current?.focus();
} else {
standardRef.current?.focus();
}
const handleValueChange = useCallback(
(selected: string) => {
onSelectQuality(conversationId, selected === MediaQuality.High);
},
[isHighQuality]
[conversationId, onSelectQuality]
);
const handleContentKeyDown = useCallback((ev: KeyboardEvent) => {
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
if (document.activeElement === standardRef.current) {
highRef.current?.focus();
} else {
standardRef.current?.focus();
}
ev.stopPropagation();
ev.preventDefault();
}
}, []);
return (
<Popover.Root open={open} onOpenChange={setOpen}>
<Popover.Trigger asChild>
<AxoDropdownMenu.Root>
<AxoDropdownMenu.Trigger>
<AxoIconButton.Root
variant="borderless-secondary"
size="md"
@@ -63,86 +48,32 @@ export function MediaQualitySelector({
label={i18n('icu:MediaQualitySelector--button')}
tooltip={false}
/>
</Popover.Trigger>
{open && (
<Popover.Portal>
<div
className={classNames({
'light-theme': theme === ThemeType.light,
'dark-theme': theme === ThemeType.dark,
})}
</AxoDropdownMenu.Trigger>
<AxoDropdownMenu.Content>
<AxoDropdownMenu.RadioGroup
value={value}
onValueChange={handleValueChange}
>
<AxoDropdownMenu.Label>
{i18n('icu:MediaQualitySelector--title')}
</AxoDropdownMenu.Label>
<AxoDropdownMenu.RadioItem
value={MediaQuality.Standard}
symbol="hd-slash"
>
<Popover.Content
className="MediaQualitySelector__popper"
side="top"
align="start"
sideOffset={4}
onOpenAutoFocus={handleOpenAutoFocus}
onKeyDown={handleContentKeyDown}
>
<div className="MediaQualitySelector__title">
{i18n('icu:MediaQualitySelector--title')}
</div>
<button
ref={standardRef}
aria-label={i18n(
'icu:MediaQualitySelector--standard-quality-title'
)}
className="MediaQualitySelector__option"
type="button"
onClick={() => {
onSelectQuality(conversationId, false);
setOpen(false);
}}
>
<div
className={classNames({
'MediaQualitySelector__option--checkmark': true,
'MediaQualitySelector__option--selected': !isHighQuality,
})}
/>
<div>
<div className="MediaQualitySelector__option--title">
{i18n('icu:MediaQualitySelector--standard-quality-title')}
</div>
<div className="MediaQualitySelector__option--description">
{i18n(
'icu:MediaQualitySelector--standard-quality-description'
)}
</div>
</div>
</button>
<button
ref={highRef}
aria-label={i18n(
'icu:MediaQualitySelector--high-quality-title'
)}
className="MediaQualitySelector__option"
type="button"
onClick={() => {
onSelectQuality(conversationId, true);
setOpen(false);
}}
>
<div
className={classNames({
'MediaQualitySelector__option--checkmark': true,
'MediaQualitySelector__option--selected': isHighQuality,
})}
/>
<div>
<div className="MediaQualitySelector__option--title">
{i18n('icu:MediaQualitySelector--high-quality-title')}
</div>
<div className="MediaQualitySelector__option--description">
{i18n('icu:MediaQualitySelector--high-quality-description')}
</div>
</div>
</button>
</Popover.Content>
</div>
</Popover.Portal>
)}
</Popover.Root>
{i18n('icu:MediaQualitySelector--standard-quality-title')}
<div className={tw('type-body-small text-label-secondary')}>
{i18n('icu:MediaQualitySelector--standard-quality-description')}
</div>
</AxoDropdownMenu.RadioItem>
<AxoDropdownMenu.RadioItem value={MediaQuality.High} symbol="hd">
{i18n('icu:MediaQualitySelector--high-quality-title')}
<div className={tw('type-body-small text-label-secondary')}>
{i18n('icu:MediaQualitySelector--high-quality-description')}
</div>
</AxoDropdownMenu.RadioItem>
</AxoDropdownMenu.RadioGroup>
</AxoDropdownMenu.Content>
</AxoDropdownMenu.Root>
);
}
+7 -12
View File
@@ -3,7 +3,7 @@
import type { ReactNode, JSX } from 'react';
import type { LocalizerType } from '../types/I18N.std.ts';
import { Button, ButtonSize, ButtonVariant } from './Button.dom.tsx';
import { AxoButton } from '../axo/AxoButton.dom.tsx';
type Props = {
i18n: LocalizerType;
@@ -21,21 +21,16 @@ export function RecordingComposer({
return (
<div className="RecordingComposer">
<div className="RecordingComposer__content">{children}</div>
<Button
className="RecordingComposer__button"
<AxoButton.Root
variant="borderless-secondary"
size="md"
onClick={onCancel}
size={ButtonSize.Medium}
variant={ButtonVariant.Secondary}
>
{i18n('icu:RecordingComposer__cancel')}
</Button>
<Button
className="RecordingComposer__button"
onClick={onSend}
size={ButtonSize.Medium}
>
</AxoButton.Root>
<AxoButton.Root variant="primary" size="md" onClick={onSend}>
{i18n('icu:RecordingComposer__send')}
</Button>
</AxoButton.Root>
</div>
);
}
@@ -11,6 +11,7 @@ import {
useStartRecordingShortcut,
useKeyboardShortcuts,
} from '../../hooks/useKeyboardShortcuts.dom.tsx';
import { AxoIconButton } from '../../axo/AxoIconButton.dom.tsx';
export type PropsType = {
conversationId: string;
@@ -50,14 +51,15 @@ export function AudioCapture({
return (
<div className="AudioCapture">
<button
aria-label={i18n('icu:voiceRecording--start')}
className="AudioCapture__microphone"
<AxoIconButton.Root
symbol="mic"
variant="borderless-secondary"
size="md"
label={i18n('icu:voiceRecording--start')}
onClick={handleClick}
onMouseEnter={handleWarmup}
onFocus={handleWarmup}
title={i18n('icu:voiceRecording--start')}
type="button"
tooltip={false}
/>
</div>
);
+11 -5
View File
@@ -1,11 +1,12 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { type JSX } from 'react';
import { VisuallyHidden } from 'react-aria';
import { Pressable, VisuallyHidden } from 'react-aria';
import { Button } from 'react-aria-components';
import type { LocalizerType } from '../../types/I18N.std.ts';
import { FunStaticEmoji } from './FunEmoji.dom.tsx';
import { Emoji } from '../../axo/emoji.std.ts';
import { AxoIconButton } from '../../axo/AxoIconButton.dom.tsx';
/**
* Fun Picker Button
@@ -18,10 +19,15 @@ export type FunPickerButtonProps = Readonly<{
export function FunPickerButton(props: FunPickerButtonProps): JSX.Element {
const { i18n } = props;
return (
<Button className="FunButton">
<span className="FunButton__Icon FunButton__Icon--FunPicker" />
<VisuallyHidden>{i18n('icu:FunButton__Label--FunPicker')}</VisuallyHidden>
</Button>
<Pressable>
<AxoIconButton.Root
symbol="emoji"
variant="borderless-secondary"
label={i18n('icu:FunButton__Label--FunPicker')}
size="md"
tooltip={false}
/>
</Pressable>
);
}