Skip to content

Commit 0aa7725

Browse files
authored
Port components to ts 4 (#2723)
* loader-page * language-selector * link-with-chevron * more-fewer * multi-page-form * multiselect/book-tags/book-options * Tests * Create/use/test assertNotNull
1 parent 5ef668a commit 0aa7725

File tree

18 files changed

+284
-133
lines changed

18 files changed

+284
-133
lines changed

src/app/components/jsx-helpers/loader-page.js renamed to src/app/components/jsx-helpers/loader-page.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import React from 'react';
22
import {camelCaseKeys} from '~/helpers/page-data-utils';
33
import usePageData from '~/helpers/use-page-data';
4-
import {setPageTitleAndDescriptionFromBookData, useCanonicalLink} from '~/helpers/use-document-head';
4+
import {setPageTitleAndDescriptionFromBookData, useCanonicalLink, BookData} from '~/helpers/use-document-head';
55
import LoadingPlaceholder from '~/components/loading-placeholder/loading-placeholder';
66
import Error404 from '~/pages/404/404';
77

8+
type RawPageData = {
9+
error?: boolean;
10+
}
11+
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13+
type ChildType = (p: any) => React.JSX.Element;
14+
815
export function LoadedPage({
916
Child, data, props, doDocumentSetup, noCamelCase
17+
} : {
18+
Child: ChildType;
19+
data: any; // eslint-disable-line @typescript-eslint/no-explicit-any
20+
props: object;
21+
doDocumentSetup?: boolean;
22+
noCamelCase?: boolean;
1023
}) {
1124
const camelCaseData = React.useMemo(
1225
() => noCamelCase ? data : camelCaseKeys(data),
@@ -16,7 +29,7 @@ export function LoadedPage({
1629
useCanonicalLink(doDocumentSetup);
1730
React.useEffect(() => {
1831
if (doDocumentSetup) {
19-
setPageTitleAndDescriptionFromBookData(data);
32+
setPageTitleAndDescriptionFromBookData(data as BookData);
2033
}
2134
}, [data, doDocumentSetup]);
2235

@@ -32,8 +45,15 @@ export function LoadedPage({
3245
export default function LoaderPage({
3346
slug, Child, props={}, preserveWrapping=false, doDocumentSetup=false,
3447
noCamelCase=false
48+
}: {
49+
slug: string;
50+
Child: ChildType;
51+
props?: object;
52+
preserveWrapping?: boolean;
53+
doDocumentSetup?: boolean;
54+
noCamelCase?: boolean;
3555
}) {
36-
const data = usePageData(slug, preserveWrapping, noCamelCase);
56+
const data = usePageData<RawPageData>(slug, preserveWrapping, noCamelCase);
3757

3858
if (!data) {
3959
return <LoadingPlaceholder />;

src/app/components/language-selector/language-selector.d.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/app/components/language-selector/language-selector.js renamed to src/app/components/language-selector/language-selector.tsx

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import './language-selector.scss';
77

88
const polishSite = 'https://openstax.pl/podreczniki';
99

10-
export function LanguageLink({locale, slug}) {
10+
type PLocaleEntry = {
11+
locale: string;
12+
slug?: string;
13+
};
14+
15+
export type LocaleEntry = Required<PLocaleEntry>
16+
17+
export function LanguageLink({locale, slug}: PLocaleEntry) {
1118
const {setLanguage} = useLanguageContext();
12-
const onClick = React.useCallback((event) => {
19+
const onClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
1320
event.preventDefault();
1421
setLanguage(locale);
1522
}, [locale, setLanguage]);
@@ -19,29 +26,24 @@ export function LanguageLink({locale, slug}) {
1926
return (<a {...props}><LanguageText locale={locale} /></a>);
2027
}
2128

22-
// Provide a fallback so ancient browsers don't outright fail
23-
export function LanguageText({locale}) {
24-
if (Intl.DisplayNames) {
25-
return (<LanguageTextUsingIntl locale={locale} />);
26-
}
27-
return locale;
28-
}
29-
30-
function LanguageTextUsingIntl({locale}) {
29+
export function LanguageText({locale}: {locale: string}) {
3130
const {language} = useLanguageContext();
32-
const languageName = React.useMemo(
33-
() => new Intl.DisplayNames([language], {type: 'language'}),
34-
[language]
31+
const text = React.useMemo(
32+
() => (new Intl.DisplayNames([language], {type: 'language'})).of(locale),
33+
[language, locale]
3534
);
3635

37-
return (
38-
<React.Fragment>
39-
{languageName.of(locale)}
40-
</React.Fragment>
41-
);
36+
return text;
4237
}
4338

44-
function AnotherLanguage({locale, LinkPresentation, position, listLength}) {
39+
type LinkPresentationType = ({locale}: PLocaleEntry) => React.JSX.Element | null;
40+
41+
function AnotherLanguage({locale, LinkPresentation, position, listLength}: {
42+
locale: string;
43+
LinkPresentation: LinkPresentationType;
44+
position: number;
45+
listLength: number;
46+
}) {
4547
return (
4648
<React.Fragment>
4749
{listLength > 1 ? ', ' : ' '}
@@ -56,7 +58,7 @@ function AnotherLanguage({locale, LinkPresentation, position, listLength}) {
5658
);
5759
}
5860

59-
export function LanguageSelectorWrapper({children}) {
61+
export function LanguageSelectorWrapper({children}: React.PropsWithChildren<Record<never, never>>) {
6062
return (
6163
<div className="language-selector">
6264
<FontAwesomeIcon icon={faGlobe} />
@@ -66,7 +68,12 @@ export function LanguageSelectorWrapper({children}) {
6668
}
6769

6870
export default function LanguageSelector({
69-
LeadIn, otherLocales=[], LinkPresentation=LanguageLink, addPolish=false
71+
LeadIn, otherLocales, LinkPresentation=LanguageLink, addPolish=false
72+
}: {
73+
LeadIn: () => React.JSX.Element;
74+
otherLocales: string[];
75+
LinkPresentation: LinkPresentationType;
76+
addPolish?: boolean;
7077
}) {
7178
if (addPolish) {
7279
otherLocales.push('pl');

src/app/components/link-with-chevron/link-with-chevron.js renamed to src/app/components/link-with-chevron/link-with-chevron.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
44
import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight';
55
import './link-with-chevron.scss';
66

7-
export default function LinkWithChevron({children, className=undefined, ...props}) {
7+
export default function LinkWithChevron({children, className, ...props}:
8+
React.AnchorHTMLAttributes<HTMLAnchorElement>
9+
) {
810
return (
911
<a className={cn('link-with-chevron', className)} {...props}>
1012
{children}{' '}

src/app/components/more-fewer/more-fewer.js renamed to src/app/components/more-fewer/more-fewer.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,41 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
44
import {faCaretLeft} from '@fortawesome/free-solid-svg-icons/faCaretLeft';
55
import {faCaretRight} from '@fortawesome/free-solid-svg-icons/faCaretRight';
66
import $ from '~/helpers/$';
7+
import {assertNotNull} from '~/helpers/data';
78
import {treatSpaceOrEnterAsClick} from '~/helpers/events';
89

9-
function PseudoButton({onClick, children}) {
10+
function PseudoButton({onClick, children}: React.PropsWithChildren<{
11+
onClick?: React.MouseEventHandler<HTMLDivElement>;
12+
}>) {
1013
return (
11-
<div role="button" tabIndex="0" onClick={onClick} onKeyDown={treatSpaceOrEnterAsClick}>
14+
<div role="button" tabIndex={0} onClick={onClick} onKeyDown={treatSpaceOrEnterAsClick}>
1215
{children}
1316
</div>
1417
);
1518
}
1619

17-
function ButtonOrPresentation({condition, children, onClick}) {
20+
function ButtonOrPresentation({condition, children, onClick}: React.PropsWithChildren<{
21+
condition: boolean;
22+
onClick?: React.MouseEventHandler<HTMLDivElement>;
23+
}>) {
1824
return (
1925
condition ?
2026
<PseudoButton onClick={onClick} children={children} /> :
2127
<div role="presentation" children={children} />
2228
);
2329
}
2430

25-
export function Paginated({children, perPage=10}) {
31+
export function Paginated({children, perPage=10}: {
32+
perPage?: number;
33+
children: React.ReactNode[];
34+
}) {
2635
const [pageNumber, setPageNumber] = useState(1);
27-
const [pageChanged, setPageChanged] = useState();
36+
const [pageChanged, setPageChanged] = useState<number>();
2837
const lastPage = Math.ceil(children.length / perPage);
2938
const firstDisplayed = perPage * (pageNumber - 1);
3039
const displayedChildren = children
3140
.slice(perPage * (pageNumber - 1), firstDisplayed + perPage);
32-
const ref=useRef();
41+
const ref=useRef<HTMLDivElement>(null);
3342

3443
function nextPage() {
3544
setPageNumber(pageNumber + 1);
@@ -41,7 +50,7 @@ export function Paginated({children, perPage=10}) {
4150
}
4251
useEffect(() => {
4352
if (pageChanged) {
44-
$.scrollTo(ref.current, 70);
53+
$.scrollTo(assertNotNull(ref.current), 70);
4554
}
4655
}, [pageChanged]);
4756

@@ -62,7 +71,10 @@ export function Paginated({children, perPage=10}) {
6271
);
6372
}
6473

65-
export default function MoreFewer({children, pluralItemName}) {
74+
export default function MoreFewer({children, pluralItemName}: {
75+
pluralItemName: string;
76+
children: React.ReactNode[];
77+
}) {
6678
const [expanded, setExpanded] = useState(false);
6779
const displayedChildren = expanded ?
6880
<Paginated>{children}</Paginated> :

src/app/components/multi-page-form/buttons.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import usePaginatorContext from '~/components/paginator/paginator-context';
33
import usePagesContext from './pages-context';
44

55
type Args = {
6-
formRef: React.MutableRefObject<HTMLFormElement>;
6+
formRef: React.RefObject<HTMLFormElement>;
77
disabled: boolean;
88
onSubmit: (form: HTMLFormElement) => void;
99
};
1010

11-
export default function ButtonRow({formRef, onSubmit, disabled = false}: Args) {
11+
export default function ButtonRow({formRef, onSubmit, disabled}: Args) {
1212
return (
1313
<div className="button-row">
1414
<BackButton disabled={disabled} />
@@ -48,7 +48,7 @@ function SubmitButton({disabled, formRef, onSubmit}: Args) {
4848
const validateAndSubmit = useCallback<React.MouseEventHandler>(
4949
(event) => {
5050
if (validateCurrentPage()) {
51-
onSubmit(formRef.current);
51+
onSubmit(formRef.current as HTMLFormElement);
5252
}
5353
event.preventDefault();
5454
},

src/app/components/multi-page-form/multi-page-form.js renamed to src/app/components/multi-page-form/multi-page-form.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function pass() {
99
return true;
1010
}
1111

12-
function MarkupChildren({children}) {
12+
function MarkupChildren({children}: {children: React.ReactNode[]}) {
1313
const {isVisible} = usePaginatorContext();
1414
const {validatedPages, activeRef} = usePagesContext();
1515

@@ -44,12 +44,20 @@ function PageCount() {
4444
);
4545
}
4646

47+
type MultiPageFormProps = {
48+
children: React.ReactNode[];
49+
validatePage?: (p: unknown) => boolean;
50+
onPageChange?: (p: unknown) => boolean;
51+
onSubmit: () => void;
52+
submitting: boolean;
53+
} & React.FormHTMLAttributes<HTMLFormElement>;
54+
4755
function MultiPageFormInContext({
4856
children,
4957
validatePage=pass, onPageChange=pass, onSubmit,
5058
submitting, ...formParams
51-
}) {
52-
const formRef = React.useRef();
59+
}: MultiPageFormProps) {
60+
const formRef = React.useRef<HTMLFormElement>(null);
5361

5462
return (
5563
<div className="multi-page-form">
@@ -78,7 +86,7 @@ function MultiPageFormInContext({
7886
);
7987
}
8088

81-
export default function MultiPageForm(params) {
89+
export default function MultiPageForm(params: MultiPageFormProps) {
8290
return (
8391
<PaginatorContextProvider>
8492
<MultiPageFormInContext {...params} />

src/app/components/multi-page-form/pages-context.js renamed to src/app/components/multi-page-form/pages-context.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import {useState, useRef, useEffect, useCallback} from 'react';
22
import usePaginatorContext from '~/components/paginator/paginator-context';
33
import buildContext from '~/components/jsx-helpers/build-context';
44

5-
function useContextValue({pages, validatePage, onPageChange}) {
5+
function useContextValue({pages, validatePage, onPageChange}: {
6+
pages: number;
7+
validatePage: (p: unknown) => boolean;
8+
onPageChange: (p: unknown) => void;
9+
}) {
610
const [validatedPages, setValidatedPages] = useState({});
7-
const activeRef = useRef();
11+
const activeRef = useRef<HTMLDivElement>(null);
812
const {currentPage} = usePaginatorContext();
913
const validateCurrentPage = useCallback(
1014
() => {
11-
const invalid = activeRef.current.querySelector(':invalid');
15+
const invalid = activeRef.current?.querySelector(':invalid');
1216

1317
setValidatedPages({[currentPage]: true, ...validatedPages});
1418
return invalid === null && validatePage(currentPage);

src/app/components/multiselect/book-tags/book-options.js renamed to src/app/components/multiselect/book-tags/book-options.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import React from 'react';
22
import useMultiselectContext from '../multiselect-context';
33
import useSFBookContext from './sf-book-context';
4+
import type {SalesforceBook} from '~/helpers/books';
45
import useToggleContext from '~/components/toggle/toggle-context';
56
import {treatSpaceOrEnterAsClick} from '~/helpers/events';
67
import './book-options.scss';
78

89

9-
function BookOption({book}) {
10+
function BookOption({book}: {book: SalesforceBook}) {
1011
const {select, deselect, isSelected} = useMultiselectContext();
1112
const {toggle} = useToggleContext();
1213
const toggleBook = React.useCallback(
1314
() => isSelected(book) ? deselect(book) : select(book),
1415
[book, isSelected, deselect, select]
1516
);
1617
const onKeyDown = React.useCallback(
17-
(event) => {
18+
(event: React.KeyboardEvent) => {
1819
if (event.key === 'Escape') {
1920
event.preventDefault();
2021
event.stopPropagation();
@@ -28,7 +29,7 @@ function BookOption({book}) {
2829

2930
return (
3031
<span
31-
className="book-option" tabIndex="0"
32+
className="book-option" tabIndex={0}
3233
role="option" aria-selected={isSelected(book)}
3334
onClick={toggleBook}
3435
onKeyDown={onKeyDown}
@@ -38,7 +39,10 @@ function BookOption({book}) {
3839
);
3940
}
4041

41-
function SubjectListing({subject, books}) {
42+
function SubjectListing({subject, books}: {
43+
subject: string;
44+
books: SalesforceBook[];
45+
}) {
4246
const booksInSubject = books.filter((b) => b.subjects.includes(subject));
4347
const groupId = `group-${subject}`;
4448

src/app/helpers/data.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ export function assertDefined<T>(value: T | undefined): T {
77
return value;
88
}
99

10+
export function assertNotNull<T>(value: T | null): T {
11+
if (value === null) {
12+
throw new Error('Value is null');
13+
}
14+
return value;
15+
}
16+
1017
export function formatDateForBlog(date: string) {
1118
if (!date) {
1219
return null;

0 commit comments

Comments
 (0)