signal-desktop/ts/components/SafetyTipsModal.tsx
2024-03-12 09:29:31 -07:00

216 lines
6.8 KiB
TypeScript

// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { UIEvent } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import uuid from 'uuid';
import type { LocalizerType } from '../types/I18N';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';
import { useReducedMotion } from '../hooks/useReducedMotion';
export type SafetyTipsModalProps = Readonly<{
i18n: LocalizerType;
onClose(): void;
}>;
export function SafetyTipsModal({
i18n,
onClose,
}: SafetyTipsModalProps): JSX.Element {
const pages = useMemo(() => {
return [
{
key: 'crypto',
title: i18n('icu:SafetyTipsModal__TipTitle--Crypto'),
description: i18n('icu:SafetyTipsModal__TipDescription--Crypto'),
imageUrl: 'images/safety-tips/safety-tip-crypto.png',
},
{
key: 'vague',
title: i18n('icu:SafetyTipsModal__TipTitle--Vague'),
description: i18n('icu:SafetyTipsModal__TipDescription--Vague'),
imageUrl: 'images/safety-tips/safety-tip-vague.png',
},
{
key: 'links',
title: i18n('icu:SafetyTipsModal__TipTitle--Links'),
description: i18n('icu:SafetyTipsModal__TipDescription--Links'),
imageUrl: 'images/safety-tips/safety-tip-links.png',
},
{
key: 'business',
title: i18n('icu:SafetyTipsModal__TipTitle--Business'),
description: i18n('icu:SafetyTipsModal__TipDescription--Business'),
imageUrl: 'images/safety-tips/safety-tip-business.png',
},
];
}, [i18n]);
const [modalId] = useState(() => uuid());
const [cardWrapperId] = useState(() => uuid());
function getCardIdForPage(pageIndex: number) {
return `${cardWrapperId}_${pages[pageIndex].key}`;
}
const maxPageIndex = pages.length - 1;
const [pageIndex, setPageIndexInner] = useState(0);
const reducedMotion = useReducedMotion();
const scrollEndTimer = useRef<NodeJS.Timeout | null>(null);
const [hasPageIndexChanged, setHasPageIndexChanged] = useState(false);
function setPageIndex(nextPageIndex: number) {
setPageIndexInner(nextPageIndex);
setHasPageIndexChanged(true);
}
function clearScrollEndTimer() {
if (scrollEndTimer.current != null) {
clearTimeout(scrollEndTimer.current);
scrollEndTimer.current = null;
}
}
useEffect(() => {
return () => {
clearScrollEndTimer();
};
}, []);
function scrollToPageIndex(nextPageIndex: number) {
clearScrollEndTimer();
setPageIndex(nextPageIndex);
document.getElementById(getCardIdForPage(nextPageIndex))?.scrollIntoView({
inline: 'center',
behavior: reducedMotion ? 'instant' : 'smooth',
});
}
function handleScroll(event: UIEvent) {
clearScrollEndTimer();
const { scrollWidth, scrollLeft, clientWidth } = event.currentTarget;
const maxScrollLeft = scrollWidth - clientWidth;
const absScrollLeft = Math.abs(scrollLeft);
const percentScrolled = absScrollLeft / maxScrollLeft;
const scrolledPageIndex = Math.round(percentScrolled * maxPageIndex);
scrollEndTimer.current = setTimeout(() => {
setPageIndex(scrolledPageIndex);
}, 100);
}
return (
<Modal
i18n={i18n}
modalName="SafetyTipsModal"
moduleClassName="SafetyTipsModal"
noMouseClose
hasXButton
padded={false}
title={i18n('icu:SafetyTipsModal__Title')}
onClose={onClose}
aria-describedby={`${modalId}-description`}
modalFooter={
<>
<Button
className="SafetyTipsModal__Button SafetyTipsModal__Button--Previous"
variant={ButtonVariant.SecondaryAffirmative}
aria-disabled={pageIndex === 0}
aria-controls={cardWrapperId}
onClick={() => {
if (pageIndex === 0) {
return;
}
scrollToPageIndex(pageIndex - 1);
}}
>
{i18n('icu:SafetyTipsModal__Button--Previous')}
</Button>
{pageIndex < maxPageIndex ? (
<Button
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
variant={ButtonVariant.Primary}
aria-controls={cardWrapperId}
onClick={() => {
if (pageIndex === maxPageIndex) {
return;
}
scrollToPageIndex(pageIndex + 1);
}}
>
{i18n('icu:SafetyTipsModal__Button--Next')}
</Button>
) : (
<Button
className="SafetyTipsModal__Button SafetyTipsModal__Button--Next"
variant={ButtonVariant.Primary}
onClick={onClose}
>
{i18n('icu:SafetyTipsModal__Button--Done')}
</Button>
)}
</>
}
>
<p className="SafetyTipsModal__Description" id={`${modalId}-description`}>
{i18n('icu:SafetyTipsModal__Description')}
</p>
<div>
<div
className="SafetyTipsModal__CardWrapper"
id={cardWrapperId}
aria-live={hasPageIndexChanged ? 'assertive' : undefined}
aria-atomic
onScroll={handleScroll}
>
{pages.map((page, index) => {
const isCurrentPage = pageIndex === index;
return (
<div
id={getCardIdForPage(index)}
key={page.key}
className="SafetyTipsModal__Card"
aria-hidden={!isCurrentPage}
>
<img
role="presentation"
alt=""
className="SafetyTipsModal__CardImage"
src={page.imageUrl}
width={664}
height={314}
/>
<h2 className="SafetyTipsModal__CardTitle">{page.title}</h2>
<p className="SafetyTipsModal__CardDescription">
{page.description}
</p>
</div>
);
})}
</div>
<div className="SafetyTipsModal__Dots">
{pages.map((page, index) => {
const isCurrentPage = pageIndex === index;
return (
<button
key={page.key}
className="SafetyTipsModal__DotsButton"
type="button"
aria-controls={cardWrapperId}
aria-current={isCurrentPage ? 'step' : undefined}
onClick={() => {
scrollToPageIndex(index);
}}
>
<div className="SafetyTipsModal__DotsButtonLabel">
{i18n('icu:SafetyTipsModal__DotLabel', {
page: index + 1,
})}
</div>
</button>
);
})}
</div>
</div>
</Modal>
);
}