216 lines
6.8 KiB
TypeScript
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>
|
|
);
|
|
}
|