diff --git a/client/common/Button.tsx b/client/common/Button.tsx index d52abf3140..9b31c4acd1 100644 --- a/client/common/Button.tsx +++ b/client/common/Button.tsx @@ -92,6 +92,7 @@ export interface ButtonProps extends React.HTMLAttributes { * but React will automatically convert a boolean prop to the correct string value. */ focusable?: boolean; + label?: string; } interface StyledButtonProps extends ButtonProps { diff --git a/client/common/icons.jsx b/client/common/icons.tsx similarity index 81% rename from client/common/icons.jsx rename to client/common/icons.tsx index bd5a673e2a..80411bf937 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.tsx @@ -26,13 +26,25 @@ import Filter from '../images/filter.svg'; import Cross from '../images/cross.svg'; import Copy from '../images/copy.svg'; +export interface IconColors { + default?: string; + hover?: string; +} + +export interface IconProps extends React.SVGProps { + 'aria-label'?: string; + Icon?: IconColors; +} + // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html // could also give these a default size, color, etc. based on the theme // Need to add size to these - like small icon, medium icon, large icon. etc. -function withLabel(SvgComponent) { - const StyledIcon = styled(SvgComponent)` +function withLabel( + SvgComponent: React.ComponentType> +) { + const StyledIcon = styled(SvgComponent)` &&& { color: ${(props) => props.Icon?.default}; & g, @@ -53,27 +65,27 @@ function withLabel(SvgComponent) { } `; - const Icon = (props) => { - const { 'aria-label': ariaLabel } = props; + // Necessary because styled components inject a different type for the ref prop + type StyledIconProps = Omit< + React.ComponentProps, + 'ref' + > & { + ref?: React.Ref; + }; + + const Icon = (props: StyledIconProps) => { + const { 'aria-label': ariaLabel, ...rest } = props; if (ariaLabel) { return ( ); } - return ; - }; - - Icon.propTypes = { - 'aria-label': PropTypes.string - }; - - Icon.defaultProps = { - 'aria-label': null + return ; }; return Icon; diff --git a/client/common/useSyncFormTranslations.ts b/client/common/useSyncFormTranslations.ts index 4a90362750..c116db1e04 100644 --- a/client/common/useSyncFormTranslations.ts +++ b/client/common/useSyncFormTranslations.ts @@ -1,30 +1,32 @@ import { useEffect, MutableRefObject } from 'react'; +import type { FormApi } from 'final-form'; -export interface FormLike { - getState(): { values: Record }; - reset(): void; - change(field: string, value: unknown): void; -} +// Generic FormLike that mirrors FormApi for any form value type +export type FormLike> = Pick< + FormApi, + 'getState' | 'reset' | 'change' +>; /** * This hook ensures that form values are preserved when the language changes. * @param formRef * @param language */ -export const useSyncFormTranslations = ( - formRef: MutableRefObject, +export const useSyncFormTranslations = >( + formRef: MutableRefObject | null>, language: string ) => { useEffect(() => { - const form = formRef.current; + const form = formRef?.current; if (!form) return; const { values } = form.getState(); form.reset(); - Object.keys(values).forEach((field) => { - if (values[field]) { - form.change(field, values[field]); + (Object.keys(values) as (keyof FormValues)[]).forEach((field) => { + const value = values[field]; + if (value !== undefined && value !== null && value !== '') { + form.change(field, value); } }); }, [language]); diff --git a/client/modules/About/About.styles.js b/client/modules/About/About.styles.ts similarity index 100% rename from client/modules/About/About.styles.js rename to client/modules/About/About.styles.ts diff --git a/client/modules/About/pages/About.jsx b/client/modules/About/pages/About.tsx similarity index 81% rename from client/modules/About/pages/About.jsx rename to client/modules/About/pages/About.tsx index 840278cbb7..84935a0141 100644 --- a/client/modules/About/pages/About.jsx +++ b/client/modules/About/pages/About.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import type { TFunction } from 'react-i18next'; import { AboutPageContent, Intro, @@ -27,8 +27,34 @@ import packageData from '../../../../package.json'; import HeartIcon from '../../../images/heart.svg'; import AsteriskIcon from '../../../images/p5-asterisk.svg'; import LogoIcon from '../../../images/p5js-square-logo.svg'; +import { RootState } from '../../../reducers'; -const AboutSection = ({ section, t }) => ( +export interface AboutSectionInfoItem { + url: string; + title: string; + description: string; +} +export interface AboutSectionInfoSection { + header: string; + items: AboutSectionInfoItem[]; +} +export interface ContactSectionLink { + label: string; + href: string; +} + +export interface AboutSectionProps { + section: AboutSectionInfoSection; + t: TFunction<'translation'>; +} + +const AboutSection = ({ + section, + t +}: { + section: AboutSectionInfoSection; + t: TFunction<'translation'>; +}) => (

{t(section.header)}

@@ -47,11 +73,15 @@ const AboutSection = ({ section, t }) => (
); -const About = () => { +export const About = () => { const { t } = useTranslation(); - const p5version = useSelector((state) => { - const index = state.files.find((file) => file.name === 'index.html'); + const p5version = useSelector((state: RootState) => { + const index = state.files.find( + (file: { + name: string /** TODO: update once files types are defined in server */; + }) => file.name === 'index.html' + ); return index?.content.match(/\/p5@([\d.]+)\//)?.[1]; }); @@ -91,7 +121,11 @@ const About = () => { {AboutSectionInfo.map((section) => ( - + ))} @@ -152,19 +186,3 @@ const About = () => { ); }; - -AboutSection.propTypes = { - section: PropTypes.shape({ - header: PropTypes.string.isRequired, - items: PropTypes.arrayOf( - PropTypes.shape({ - url: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - description: PropTypes.string.isRequired - }) - ).isRequired - }).isRequired, - t: PropTypes.func.isRequired -}; - -export default About; diff --git a/client/modules/About/statics/aboutData.js b/client/modules/About/statics/aboutData.ts similarity index 91% rename from client/modules/About/statics/aboutData.js rename to client/modules/About/statics/aboutData.ts index a8623cfa7f..d22e6e75fe 100644 --- a/client/modules/About/statics/aboutData.js +++ b/client/modules/About/statics/aboutData.ts @@ -1,4 +1,9 @@ -export const ContactSectionLinks = [ +import type { + ContactSectionLink, + AboutSectionInfoSection +} from '../pages/About'; + +export const ContactSectionLinks: ContactSectionLink[] = [ { label: 'About.Github', href: 'https://github.com/processing/p5.js-web-editor' @@ -22,7 +27,7 @@ export const ContactSectionLinks = [ } ]; -export const AboutSectionInfo = [ +export const AboutSectionInfo: AboutSectionInfoSection[] = [ { header: 'About.NewP5', items: [ diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 53b0c82c63..242b5244c8 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -6,7 +6,7 @@ import { showReduxDevTools } from '../../store'; import DevTools from './components/DevTools'; import { setPreviousPath } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; -import CookieConsent from '../User/components/CookieConsent'; +import { CookieConsent } from '../User/components/CookieConsent'; function hideCookieConsent(pathname) { if (pathname.includes('/full/') || pathname.includes('/embed/')) { diff --git a/client/modules/IDE/components/Header/Toolbar.jsx b/client/modules/IDE/components/Header/Toolbar.jsx index fbf0b5d091..16ed74ff3e 100644 --- a/client/modules/IDE/components/Header/Toolbar.jsx +++ b/client/modules/IDE/components/Header/Toolbar.jsx @@ -20,7 +20,7 @@ import StopIcon from '../../../../images/stop.svg'; import PreferencesIcon from '../../../../images/preferences.svg'; import ProjectName from './ProjectName'; import VersionIndicator from '../VersionIndicator'; -import VisibilityDropdown from '../../../User/components/VisibilityDropdown'; +import { VisibilityDropdown } from '../../../User/components/VisibilityDropdown'; import { changeVisibility } from '../../actions/project'; const Toolbar = (props) => { diff --git a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap index 02e4776b1c..382e49aadd 100644 --- a/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap +++ b/client/modules/IDE/components/Header/__snapshots__/Nav.unit.test.jsx.snap @@ -351,7 +351,7 @@ exports[`Nav renders dashboard version for mobile 1`] = ` >