diff --git a/packages/website/package.json b/packages/website/package.json index 2058c4598359..1a69a53ea58b 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -6,6 +6,7 @@ "build": "docusaurus build", "clear": "docusaurus clear", "format": "prettier --write \"./**/*.{md,mdx,ts,js,tsx,jsx}\" --ignore-path ../../.prettierignore", + "lint": "eslint . --ext .js,.ts --ignore-path ../../.eslintignore", "serve": "docusaurus serve", "start": "docusaurus start", "swizzle": "docusaurus swizzle" diff --git a/packages/website/src/components/OptionsSelector.tsx b/packages/website/src/components/OptionsSelector.tsx index 71f83a95902a..023efce6315d 100644 --- a/packages/website/src/components/OptionsSelector.tsx +++ b/packages/website/src/components/OptionsSelector.tsx @@ -43,19 +43,32 @@ function OptionsSelector({ const [copyLink, setCopyLink] = useState(false); const [copyMarkdown, setCopyMarkdown] = useState(false); - const updateTS = useCallback((version: string) => { - setState({ ts: version }); - }, []); + const updateTS = useCallback( + (version: string) => { + setState({ ts: version }); + }, + [setState], + ); - const updateRules = useCallback((rules: RulesRecord) => { - setState({ rules: rules }); - setEslintModal(false); - }, []); + const updateRules = useCallback( + (rules?: RulesRecord) => { + if (rules) { + setState({ rules: rules }); + } + setEslintModal(false); + }, + [setState], + ); - const updateTsConfig = useCallback((config: CompilerFlags) => { - setState({ tsConfig: config }); - setTypeScriptModal(false); - }, []); + const updateTsConfig = useCallback( + (config?: CompilerFlags) => { + if (config) { + setState({ tsConfig: config }); + } + setTypeScriptModal(false); + }, + [setState], + ); const copyLinkToClipboard = useCallback(async () => { await navigator.clipboard.writeText(document.location.toString()); diff --git a/packages/website/src/components/config/ConfigEslint.tsx b/packages/website/src/components/config/ConfigEslint.tsx index b14baa69983d..cb64ae0a46ad 100644 --- a/packages/website/src/components/config/ConfigEslint.tsx +++ b/packages/website/src/components/config/ConfigEslint.tsx @@ -2,11 +2,12 @@ import React, { useCallback, useEffect, useState } from 'react'; import type { RulesRecord, RuleEntry } from '@typescript-eslint/website-eslint'; import ConfigEditor, { ConfigOptionsType } from './ConfigEditor'; -import { RuleDetails } from '../types'; +import type { RuleDetails } from '../types'; +import { shallowEqual } from '../lib/shallowEqual'; export interface ModalEslintProps { readonly isOpen: boolean; - readonly onClose: (rules: RulesRecord) => void; + readonly onClose: (value?: RulesRecord) => void; readonly ruleOptions: RuleDetails[]; readonly rules: RulesRecord; } @@ -55,19 +56,22 @@ function ConfigEslint(props: ModalEslintProps): JSX.Element { const onClose = useCallback( (newConfig: Record) => { - props.onClose( - Object.fromEntries( - Object.entries(newConfig) - .map<[string, unknown]>(([name, value]) => - Array.isArray(value) && value.length === 1 - ? [name, value[0]] - : [name, value], - ) - .filter(checkOptions), - ), + const cfg = Object.fromEntries( + Object.entries(newConfig) + .map<[string, unknown]>(([name, value]) => + Array.isArray(value) && value.length === 1 + ? [name, value[0]] + : [name, value], + ) + .filter(checkOptions), ); + if (!shallowEqual(cfg, props.rules)) { + props.onClose(cfg); + } else { + props.onClose(); + } }, - [props.onClose], + [props.onClose, props.rules], ); return ( diff --git a/packages/website/src/components/config/ConfigTypeScript.tsx b/packages/website/src/components/config/ConfigTypeScript.tsx index a86f66aff961..27cc2cbdd97b 100644 --- a/packages/website/src/components/config/ConfigTypeScript.tsx +++ b/packages/website/src/components/config/ConfigTypeScript.tsx @@ -1,13 +1,14 @@ import React, { useCallback } from 'react'; import tsConfigOptions from '../tsConfigOptions.json'; -import type { CompilerFlags } from '../types'; import ConfigEditor from './ConfigEditor'; +import type { CompilerFlags } from '../types'; +import { shallowEqual } from '../lib/shallowEqual'; interface ModalTypeScriptProps { - isOpen: boolean; - onClose: (config: CompilerFlags) => void; - config?: CompilerFlags; + readonly isOpen: boolean; + readonly onClose: (config?: CompilerFlags) => void; + readonly config?: CompilerFlags; } function checkOptions(item: [string, unknown]): item is [string, boolean] { @@ -17,11 +18,16 @@ function checkOptions(item: [string, unknown]): item is [string, boolean] { function ConfigTypeScript(props: ModalTypeScriptProps): JSX.Element { const onClose = useCallback( (newConfig: Record) => { - props.onClose( - Object.fromEntries(Object.entries(newConfig).filter(checkOptions)), + const cfg = Object.fromEntries( + Object.entries(newConfig).filter(checkOptions), ); + if (!shallowEqual(cfg, props.config)) { + props.onClose(cfg); + } else { + props.onClose(); + } }, - [props.onClose], + [props.onClose, props.config], ); return ( diff --git a/packages/website/src/components/hooks/useHashState.ts b/packages/website/src/components/hooks/useHashState.ts index d6a8e8cc8e91..b1263ce04ccb 100644 --- a/packages/website/src/components/hooks/useHashState.ts +++ b/packages/website/src/components/hooks/useHashState.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { CompilerFlags, ConfigModel, RulesRecord } from '../types'; import * as lz from 'lzstring.ts'; +import { shallowEqual } from '../lib/shallowEqual'; function writeQueryParam(value: string): string { return lz.LZString.compressToEncodedURIComponent(value); @@ -76,26 +77,6 @@ const writeStateToUrl = (newState: ConfigModel): string => { return ''; }; -function shallowEqual( - object1: Record | ConfigModel | undefined, - object2: Record | ConfigModel | undefined, -): boolean { - if (object1 === object2) { - return true; - } - const keys1 = Object.keys(object1 ?? {}); - const keys2 = Object.keys(object2 ?? {}); - if (keys1.length !== keys2.length) { - return false; - } - for (const key of keys1) { - if (object1![key] !== object2![key]) { - return false; - } - } - return true; -} - function useHashState( initialState: ConfigModel, ): [ConfigModel, (cfg: Partial) => void] { diff --git a/packages/website/src/components/lib/shallowEqual.ts b/packages/website/src/components/lib/shallowEqual.ts new file mode 100644 index 000000000000..2f34444c5874 --- /dev/null +++ b/packages/website/src/components/lib/shallowEqual.ts @@ -0,0 +1,21 @@ +import type { ConfigModel } from '@site/src/components/types'; + +export function shallowEqual( + object1: Record | ConfigModel | undefined, + object2: Record | ConfigModel | undefined, +): boolean { + if (object1 === object2) { + return true; + } + const keys1 = Object.keys(object1 ?? {}); + const keys2 = Object.keys(object2 ?? {}); + if (keys1.length !== keys2.length) { + return false; + } + for (const key of keys1) { + if (object1![key] !== object2![key]) { + return false; + } + } + return true; +} diff --git a/packages/website/src/components/modals/Modal.module.css b/packages/website/src/components/modals/Modal.module.css index 45c0099482d2..694b1b113fc5 100644 --- a/packages/website/src/components/modals/Modal.module.css +++ b/packages/website/src/components/modals/Modal.module.css @@ -38,7 +38,8 @@ } .modalClose { - cursor: pointer; + transition: color var(--ifm-transition-fast) + var(--ifm-transition-timing-default); } .modalClose:hover, diff --git a/packages/website/src/components/modals/Modal.tsx b/packages/website/src/components/modals/Modal.tsx index ff17a0d89f6b..fe380b1ccb6d 100644 --- a/packages/website/src/components/modals/Modal.tsx +++ b/packages/website/src/components/modals/Modal.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */ -import React from 'react'; +import React, { MouseEvent, useCallback, useEffect } from 'react'; import clsx from 'clsx'; import styles from './Modal.module.css'; import CloseIcon from '../icons/CloseIcon'; @@ -12,24 +12,48 @@ interface ModalProps { } function Modal(props: ModalProps): JSX.Element { + useEffect(() => { + const closeOnEscapeKeyDown = (e: KeyboardEvent): void => { + if (e.key === 'Escape' || e.keyCode === 27) { + props.onClose(); + } + }; + + document.body.addEventListener('keydown', closeOnEscapeKeyDown); + return (): void => { + document.body.removeEventListener('keydown', closeOnEscapeKeyDown); + }; + }, []); + + const onClick = useCallback( + (e: MouseEvent) => { + if (e.currentTarget === e.target) { + props.onClose(); + } + }, + [props.onClose], + ); + return (
{ - e.stopPropagation(); - }} >

{props.header}

- + className={clsx(styles.modalClose, 'clean-btn')} + type="button" + > + +
{React.Children.map(props.children, child => child)}