Update Backup Media download progress indicator for narrow left panes

Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
trevor-signal 2025-04-01 17:20:46 -04:00 committed by GitHub
commit 40e91e96fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 117 additions and 73 deletions

View file

@ -4,34 +4,17 @@
@use '../mixins'; @use '../mixins';
@use '../variables'; @use '../variables';
.BackupMediaDownloadProgress { .BackupMediaDownloadProgress__content {
border-radius: 12px;
display: flex; display: flex;
gap: 10px; flex-direction: column;
padding: 11px; justify-content: center;
padding-inline-end: 16px; min-height: 36px;
margin-inline: 10px; margin-inline-end: 20px;
margin-block-end: 6px; gap: 2px;
margin-block-start: 2px;
user-select: none;
position: relative;
&__title {
@include mixins.font-body-2-bold;
}
@include mixins.light-theme {
background-color: variables.$color-white;
border: 1px solid variables.$color-gray-20;
}
@include mixins.dark-theme {
background: variables.$color-gray-75;
border: 1px solid variables.$color-gray-60;
}
} }
.BackupMediaDownloadProgress__icon { .BackupMediaDownloadProgress__title {
margin-top: 6px; @include mixins.font-body-2-bold;
} }
.BackupMediaDownloadProgress__icon--complete { .BackupMediaDownloadProgress__icon--complete {
@ -89,10 +72,12 @@ button.BackupMediaDownloadProgress__button {
} }
button.BackupMediaDownloadProgress__button-more { button.BackupMediaDownloadProgress__button-more {
position: absolute;
inset-inline-end: 14px;
inset-block-start: 10px;
@include mixins.button-reset; @include mixins.button-reset;
& {
position: absolute;
inset-block-start: 8px;
inset-inline-end: 14px;
}
&::after { &::after {
content: ''; content: '';
display: block; display: block;
@ -113,10 +98,12 @@ button.BackupMediaDownloadProgress__button-more {
} }
} }
button.BackupMediaDownloadProgress__button-close { button.BackupMediaDownloadProgress__button-close {
position: absolute;
inset-inline-end: 14px;
inset-block-start: 10px;
@include mixins.button-reset; @include mixins.button-reset;
& {
position: absolute;
inset-block-start: 12px;
inset-inline-end: 12px;
}
&::after { &::after {
content: ''; content: '';
display: block; display: block;
@ -137,14 +124,6 @@ button.BackupMediaDownloadProgress__button-close {
} }
} }
.BackupMediaDownloadProgress__content {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
min-height: 36px;
}
.BackupMediaDownloadProgress__description { .BackupMediaDownloadProgress__description {
@include mixins.font-subtitle; @include mixins.font-subtitle;

View file

@ -30,6 +30,7 @@
padding-inline: 16px 14px; padding-inline: 16px 14px;
user-select: none; user-select: none;
width: 100%; width: 100%;
position: relative;
font-size: 13px; font-size: 13px;
line-height: 18px; line-height: 18px;

View file

@ -7,11 +7,40 @@ import { action } from '@storybook/addon-actions';
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress'; import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
import { KIBIBYTE } from '../types/AttachmentSize'; import { KIBIBYTE } from '../types/AttachmentSize';
import { WidthBreakpoint } from './_util';
const { i18n } = window.SignalContext; const { i18n } = window.SignalContext;
type PropsType = ComponentProps<typeof BackupMediaDownloadProgress>; type PropsType = ComponentProps<typeof BackupMediaDownloadProgress>;
function Template(args: PropsType): JSX.Element {
return (
<>
<div style={{ width: 350 }}>
<p>Wide</p>
<BackupMediaDownloadProgress
{...args}
widthBreakpoint={WidthBreakpoint.Wide}
/>
</div>
<div style={{ width: 280 }}>
<p>Medium</p>
<BackupMediaDownloadProgress
{...args}
widthBreakpoint={WidthBreakpoint.Medium}
/>
</div>
<div style={{ width: 130 }}>
<p>Narrow</p>
<BackupMediaDownloadProgress
{...args}
widthBreakpoint={WidthBreakpoint.Narrow}
/>
</div>
</>
);
}
export default { export default {
title: 'Components/BackupMediaDownloadProgress', title: 'Components/BackupMediaDownloadProgress',
args: { args: {
@ -27,34 +56,27 @@ export default {
} satisfies Meta<PropsType>; } satisfies Meta<PropsType>;
export function InProgress(args: PropsType): JSX.Element { export function InProgress(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} />; return <Template {...args} />;
} }
export function Increasing(args: PropsType): JSX.Element { export function Increasing(args: PropsType): JSX.Element {
return ( return <Template {...args} {...useIncreasingFractionComplete()} />;
<BackupMediaDownloadProgress
{...args}
{...useIncreasingFractionComplete()}
/>
);
} }
export function Paused(args: PropsType): JSX.Element { export function Paused(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isPaused />; return <Template {...args} isPaused />;
} }
export function Idle(args: PropsType): JSX.Element { export function Idle(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isIdle />; return <Template {...args} isIdle />;
} }
export function PausedAndIdle(args: PropsType): JSX.Element { export function PausedAndIdle(args: PropsType): JSX.Element {
return <BackupMediaDownloadProgress {...args} isPaused isIdle />; return <Template {...args} isPaused isIdle />;
} }
export function Complete(args: PropsType): JSX.Element { export function Complete(args: PropsType): JSX.Element {
return ( return <Template {...args} downloadedBytes={args.totalBytes} />;
<BackupMediaDownloadProgress {...args} downloadedBytes={args.totalBytes} />
);
} }
function useIncreasingFractionComplete() { function useIncreasingFractionComplete() {

View file

@ -9,6 +9,8 @@ import { roundFractionForProgressBar } from '../util/numbers';
import { ProgressCircle } from './ProgressCircle'; import { ProgressCircle } from './ProgressCircle';
import { ContextMenu } from './ContextMenu'; import { ContextMenu } from './ContextMenu';
import { BackupMediaDownloadCancelConfirmationDialog } from './BackupMediaDownloadCancelConfirmationDialog'; import { BackupMediaDownloadCancelConfirmationDialog } from './BackupMediaDownloadCancelConfirmationDialog';
import { LeftPaneDialog } from './LeftPaneDialog';
import { WidthBreakpoint } from './_util';
export type PropsType = Readonly<{ export type PropsType = Readonly<{
i18n: LocalizerType; i18n: LocalizerType;
@ -16,6 +18,7 @@ export type PropsType = Readonly<{
totalBytes: number; totalBytes: number;
isIdle: boolean; isIdle: boolean;
isPaused: boolean; isPaused: boolean;
widthBreakpoint: WidthBreakpoint;
handleCancel: VoidFunction; handleCancel: VoidFunction;
handleClose: VoidFunction; handleClose: VoidFunction;
handleResume: VoidFunction; handleResume: VoidFunction;
@ -32,6 +35,7 @@ export function BackupMediaDownloadProgress({
handleClose, handleClose,
handleResume, handleResume,
handlePause, handlePause,
widthBreakpoint,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const [isShowingCancelConfirmation, setIsShowingCancelConfirmation] = const [isShowingCancelConfirmation, setIsShowingCancelConfirmation] =
useState(false); useState(false);
@ -62,7 +66,10 @@ export function BackupMediaDownloadProgress({
let actionButton: JSX.Element | undefined; let actionButton: JSX.Element | undefined;
if (fractionComplete === 1) { if (fractionComplete === 1) {
icon = ( icon = (
<div className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--complete" /> <div
className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--complete"
aria-label={i18n('icu:BackupMediaDownloadProgress__title-complete')}
/>
); );
content = ( content = (
<> <>
@ -77,7 +84,10 @@ export function BackupMediaDownloadProgress({
actionButton = closeButton; actionButton = closeButton;
} else if (isIdle && !isPaused) { } else if (isIdle && !isPaused) {
icon = ( icon = (
<div className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--idle" /> <div
className="BackupMediaDownloadProgress__icon BackupMediaDownloadProgress__icon--idle"
aria-label={i18n('icu:BackupMediaDownloadProgress__description-idle')}
/>
); );
content = ( content = (
<> <>
@ -94,28 +104,34 @@ export function BackupMediaDownloadProgress({
); );
actionButton = closeButton; actionButton = closeButton;
} else { } else {
icon = (
<div className="BackupMediaDownloadProgress__icon">
<ProgressCircle fractionComplete={fractionComplete} />
</div>
);
if (isPaused) { if (isPaused) {
content = ( content = (
<> <>
<div className="BackupMediaDownloadProgress__title"> <div className="BackupMediaDownloadProgress__title">
{i18n('icu:BackupMediaDownloadProgress__title-paused')} {i18n('icu:BackupMediaDownloadProgress__title-paused')}
</div> </div>
<button {widthBreakpoint !== WidthBreakpoint.Narrow ? (
type="button" <button
onClick={handleResume} type="button"
className="BackupMediaDownloadProgress__button" onClick={handleResume}
aria-label={i18n('icu:BackupMediaDownloadProgress__button-resume')} className="BackupMediaDownloadProgress__button"
> aria-label={i18n(
{i18n('icu:BackupMediaDownloadProgress__button-resume')} 'icu:BackupMediaDownloadProgress__button-resume'
</button> )}
>
{i18n('icu:BackupMediaDownloadProgress__button-resume')}
</button>
) : null}
</> </>
); );
icon = (
<div className="BackupMediaDownloadProgress__icon">
<ProgressCircle
fractionComplete={fractionComplete}
ariaLabel={i18n('icu:BackupMediaDownloadProgress__title-paused')}
/>
</div>
);
} else { } else {
content = ( content = (
<> <>
@ -131,6 +147,16 @@ export function BackupMediaDownloadProgress({
</div> </div>
</> </>
); );
icon = (
<div className="BackupMediaDownloadProgress__icon">
<ProgressCircle
fractionComplete={fractionComplete}
ariaLabel={i18n(
'icu:BackupMediaDownloadProgress__title-in-progress'
)}
/>
</div>
);
} }
actionButton = ( actionButton = (
@ -173,10 +199,13 @@ export function BackupMediaDownloadProgress({
} }
return ( return (
<div className="BackupMediaDownloadProgress"> <LeftPaneDialog
{icon} type="info"
containerWidthBreakpoint={widthBreakpoint}
icon={icon}
>
<div className="BackupMediaDownloadProgress__content">{content}</div> <div className="BackupMediaDownloadProgress__content">{content}</div>
{actionButton} {widthBreakpoint !== WidthBreakpoint.Narrow ? actionButton : null}
{isShowingCancelConfirmation ? ( {isShowingCancelConfirmation ? (
<BackupMediaDownloadCancelConfirmationDialog <BackupMediaDownloadCancelConfirmationDialog
i18n={i18n} i18n={i18n}
@ -184,6 +213,6 @@ export function BackupMediaDownloadProgress({
handleConfirmCancel={handleConfirmedCancel} handleConfirmCancel={handleConfirmedCancel}
/> />
) : null} ) : null}
</div> </LeftPaneDialog>
); );
} }

View file

@ -801,6 +801,7 @@ export function LeftPane({
{showBackupMediaDownloadProgress ? ( {showBackupMediaDownloadProgress ? (
<BackupMediaDownloadProgress <BackupMediaDownloadProgress
i18n={i18n} i18n={i18n}
widthBreakpoint={widthBreakpoint}
{...backupMediaDownloadProgress} {...backupMediaDownloadProgress}
handleClose={dismissBackupMediaDownloadBanner} handleClose={dismissBackupMediaDownloadBanner}
handlePause={pauseBackupMediaDownload} handlePause={pauseBackupMediaDownload}

View file

@ -10,7 +10,12 @@ type Props = React.ComponentProps<typeof ProgressCircle>;
export default { export default {
title: 'Components/ProgressCircle', title: 'Components/ProgressCircle',
component: ProgressCircle, component: ProgressCircle,
args: { fractionComplete: 0, width: undefined, strokeWidth: undefined }, args: {
fractionComplete: 0,
width: undefined,
strokeWidth: undefined,
ariaLabel: undefined,
},
} satisfies ComponentMeta<Props>; } satisfies ComponentMeta<Props>;
export function Zero(args: Props): JSX.Element { export function Zero(args: Props): JSX.Element {

View file

@ -7,10 +7,12 @@ export function ProgressCircle({
fractionComplete, fractionComplete,
width = 24, width = 24,
strokeWidth = 3, strokeWidth = 3,
ariaLabel,
}: { }: {
fractionComplete: number; fractionComplete: number;
width?: number; width?: number;
strokeWidth?: number; strokeWidth?: number;
ariaLabel?: string;
}): JSX.Element { }): JSX.Element {
const radius = width / 2 - strokeWidth / 2; const radius = width / 2 - strokeWidth / 2;
const circumference = radius * 2 * Math.PI; const circumference = radius * 2 * Math.PI;
@ -21,6 +23,11 @@ export function ProgressCircle({
className="ProgressCircle" className="ProgressCircle"
width={widthInPixels} width={widthInPixels}
height={widthInPixels} height={widthInPixels}
role="progressbar"
aria-label={ariaLabel}
aria-valuenow={Math.trunc(fractionComplete * 100)}
aria-valuemin={0}
aria-valuemax={100}
> >
<circle <circle
className="ProgressCircle__background" className="ProgressCircle__background"