diff --git a/packages/core/src/context/hotkeys/hotkeysProvider.tsx b/packages/core/src/context/hotkeys/hotkeysProvider.tsx index 330d9e2f66..65110eed41 100644 --- a/packages/core/src/context/hotkeys/hotkeysProvider.tsx +++ b/packages/core/src/context/hotkeys/hotkeysProvider.tsx @@ -109,7 +109,8 @@ export interface HotkeysProviderProps { */ export const HotkeysProvider = ({ children, dialogProps, renderDialog, value }: HotkeysProviderProps) => { const hasExistingContext = value != null; - const [state, dispatch] = value ?? React.useReducer(hotkeysReducer, { ...initialHotkeysState, hasProvider: true }); + const fallbackReducer = React.useReducer(hotkeysReducer, { ...initialHotkeysState, hasProvider: true }); + const [state, dispatch] = value ?? fallbackReducer; const handleDialogClose = React.useCallback(() => dispatch({ type: "CLOSE_DIALOG" }), []); const dialog = renderDialog?.(state, { handleDialogClose }) ?? ( diff --git a/packages/core/src/hooks/hotkeys/useHotkeys.ts b/packages/core/src/hooks/hotkeys/useHotkeys.ts index bda89a469d..b386c56efa 100644 --- a/packages/core/src/hooks/hotkeys/useHotkeys.ts +++ b/packages/core/src/hooks/hotkeys/useHotkeys.ts @@ -77,9 +77,11 @@ export function useHotkeys(keys: readonly HotkeyConfig[], options: UseHotkeysOpt // register keys with global context const [state, dispatch] = React.useContext(HotkeysContext); - if (!state.hasProvider) { - React.useEffect(() => console.warn(HOTKEYS_PROVIDER_NOT_FOUND), []); - } + React.useEffect(() => { + if (!state.hasProvider) { + console.warn(HOTKEYS_PROVIDER_NOT_FOUND); + } + }, []); // we can still bind the hotkeys if there is no HotkeysProvider, they just won't show up in the dialog React.useEffect(() => { diff --git a/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx index 1acf7d199c..6887f994ce 100644 --- a/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx +++ b/packages/docs-app/src/examples/core-examples/useHotkeysExample.tsx @@ -32,13 +32,13 @@ export const UseHotkeysExample: React.FC = props => { } }, [pianoRef]); - // create a dictionary of key states and updater functions - const keys = Array.apply(null, Array(24)) - .map(() => React.useState(() => false), []) - .map(([pressed, setPressed]) => ({ - pressed, - setPressed, - })); + const [keyPressed, setKeyPressed] = React.useState(new Array(25).fill(false)); + + const setKeyState = React.useCallback((targetIndex: number, newValue: boolean) => { + setKeyPressed(previouslySelected => + previouslySelected.map((value, index) => (index === targetIndex ? newValue : value)), + ); + }, []); const hotkeys = React.useMemo( () => [ @@ -52,169 +52,169 @@ export const UseHotkeysExample: React.FC = props => { combo: "Q", group: "useHotkeys Example", label: "Play a C5", - onKeyDown: () => keys[0].setPressed(true), - onKeyUp: () => keys[0].setPressed(false), + onKeyDown: () => setKeyState(0, true), + onKeyUp: () => setKeyState(0, false), }, { combo: "2", group: "useHotkeys Example", label: "Play a C#5", - onKeyDown: () => keys[1].setPressed(true), - onKeyUp: () => keys[1].setPressed(false), + onKeyDown: () => setKeyState(1, true), + onKeyUp: () => setKeyState(1, false), }, { combo: "W", group: "useHotkeys Example", label: "Play a D5", - onKeyDown: () => keys[2].setPressed(true), - onKeyUp: () => keys[2].setPressed(false), + onKeyDown: () => setKeyState(2, true), + onKeyUp: () => setKeyState(2, false), }, { combo: "3", group: "useHotkeys Example", label: "Play a D#5", - onKeyDown: () => keys[3].setPressed(true), - onKeyUp: () => keys[3].setPressed(false), + onKeyDown: () => setKeyState(3, true), + onKeyUp: () => setKeyState(3, false), }, { combo: "E", group: "useHotkeys Example", label: "Play a E5", - onKeyDown: () => keys[4].setPressed(true), - onKeyUp: () => keys[4].setPressed(false), + onKeyDown: () => setKeyState(4, true), + onKeyUp: () => setKeyState(4, false), }, { combo: "R", group: "useHotkeys Example", label: "Play a F5", - onKeyDown: () => keys[5].setPressed(true), - onKeyUp: () => keys[5].setPressed(false), + onKeyDown: () => setKeyState(5, true), + onKeyUp: () => setKeyState(5, false), }, { combo: "5", group: "useHotkeys Example", label: "Play a F#5", - onKeyDown: () => keys[6].setPressed(true), - onKeyUp: () => keys[6].setPressed(false), + onKeyDown: () => setKeyState(6, true), + onKeyUp: () => setKeyState(6, false), }, { combo: "T", group: "useHotkeys Example", label: "Play a G5", - onKeyDown: () => keys[7].setPressed(true), - onKeyUp: () => keys[7].setPressed(false), + onKeyDown: () => setKeyState(7, true), + onKeyUp: () => setKeyState(7, false), }, { combo: "6", group: "useHotkeys Example", label: "Play a G#5", - onKeyDown: () => keys[8].setPressed(true), - onKeyUp: () => keys[8].setPressed(false), + onKeyDown: () => setKeyState(8, true), + onKeyUp: () => setKeyState(8, false), }, { combo: "Y", group: "useHotkeys Example", label: "Play a A5", - onKeyDown: () => keys[9].setPressed(true), - onKeyUp: () => keys[9].setPressed(false), + onKeyDown: () => setKeyState(9, true), + onKeyUp: () => setKeyState(9, false), }, { combo: "7", group: "useHotkeys Example", label: "Play a A#5", - onKeyDown: () => keys[10].setPressed(true), - onKeyUp: () => keys[10].setPressed(false), + onKeyDown: () => setKeyState(10, true), + onKeyUp: () => setKeyState(10, false), }, { combo: "U", group: "useHotkeys Example", label: "Play a B5", - onKeyDown: () => keys[11].setPressed(true), - onKeyUp: () => keys[11].setPressed(false), + onKeyDown: () => setKeyState(11, true), + onKeyUp: () => setKeyState(11, false), }, { combo: "Z", group: "useHotkeys Example", label: "Play a C4", - onKeyDown: () => keys[12].setPressed(true), - onKeyUp: () => keys[12].setPressed(false), + onKeyDown: () => setKeyState(12, true), + onKeyUp: () => setKeyState(12, false), }, { combo: "S", group: "useHotkeys Example", label: "Play a C#4", - onKeyDown: () => keys[13].setPressed(true), - onKeyUp: () => keys[13].setPressed(false), + onKeyDown: () => setKeyState(13, true), + onKeyUp: () => setKeyState(13, false), }, { combo: "X", group: "useHotkeys Example", label: "Play a D4", - onKeyDown: () => keys[14].setPressed(true), - onKeyUp: () => keys[14].setPressed(false), + onKeyDown: () => setKeyState(14, true), + onKeyUp: () => setKeyState(14, false), }, { combo: "D", group: "useHotkeys Example", label: "Play a D#4", - onKeyDown: () => keys[15].setPressed(true), - onKeyUp: () => keys[15].setPressed(false), + onKeyDown: () => setKeyState(15, true), + onKeyUp: () => setKeyState(15, false), }, { combo: "C", group: "useHotkeys Example", label: "Play a E4", - onKeyDown: () => keys[16].setPressed(true), - onKeyUp: () => keys[16].setPressed(false), + onKeyDown: () => setKeyState(16, true), + onKeyUp: () => setKeyState(16, false), }, { combo: "V", group: "useHotkeys Example", label: "Play a F4", - onKeyDown: () => keys[17].setPressed(true), - onKeyUp: () => keys[17].setPressed(false), + onKeyDown: () => setKeyState(17, true), + onKeyUp: () => setKeyState(17, false), }, { combo: "G", group: "useHotkeys Example", label: "Play a F#4", - onKeyDown: () => keys[18].setPressed(true), - onKeyUp: () => keys[18].setPressed(false), + onKeyDown: () => setKeyState(18, true), + onKeyUp: () => setKeyState(18, false), }, { combo: "B", group: "useHotkeys Example", label: "Play a G4", - onKeyDown: () => keys[19].setPressed(true), - onKeyUp: () => keys[19].setPressed(false), + onKeyDown: () => setKeyState(19, true), + onKeyUp: () => setKeyState(19, false), }, { combo: "H", group: "useHotkeys Example", label: "Play a G#4", - onKeyDown: () => keys[20].setPressed(true), - onKeyUp: () => keys[20].setPressed(false), + onKeyDown: () => setKeyState(20, true), + onKeyUp: () => setKeyState(20, false), }, { combo: "N", group: "useHotkeys Example", label: "Play a A4", - onKeyDown: () => keys[21].setPressed(true), - onKeyUp: () => keys[21].setPressed(false), + onKeyDown: () => setKeyState(21, true), + onKeyUp: () => setKeyState(21, false), }, { combo: "J", group: "useHotkeys Example", label: "Play a A#4", - onKeyDown: () => keys[22].setPressed(true), - onKeyUp: () => keys[22].setPressed(false), + onKeyDown: () => setKeyState(22, true), + onKeyUp: () => setKeyState(22, false), }, { combo: "M", group: "useHotkeys Example", label: "Play a B4", - onKeyDown: () => keys[23].setPressed(true), - onKeyUp: () => keys[23].setPressed(false), + onKeyDown: () => setKeyState(23, true), + onKeyUp: () => setKeyState(23, false), }, ], [], @@ -232,32 +232,32 @@ export const UseHotkeysExample: React.FC = props => { onKeyUp={handleKeyUp} >
- - - - - - - - - - - - + + + + + + + + + + + +
- - - - - - - - - - - - + + + + + + + + + + + +
diff --git a/packages/docs-theme/src/tags/css.tsx b/packages/docs-theme/src/tags/css.tsx index 9f23834abe..2edbf41237 100644 --- a/packages/docs-theme/src/tags/css.tsx +++ b/packages/docs-theme/src/tags/css.tsx @@ -31,6 +31,16 @@ export const CssExample: React.FC = ({ value }) => { const { getDocsData } = React.useContext(DocumentationContext); const [activeModifiers, setActiveModifiers] = React.useState>(new Set()); + const getModifiers = React.useCallback( + (prefix: "." | ":") => { + return Array.from(activeModifiers.keys()) + .filter(mod => mod.charAt(0) === prefix) + .map(mod => mod.slice(1)) + .join(" "); + }, + [activeModifiers], + ); + const getModifierToggleHandler = (modifier: string) => { return () => { const newModifiers = new Set(activeModifiers); @@ -60,15 +70,6 @@ export const CssExample: React.FC = ({ value }) => { )); - const getModifiers = React.useCallback( - (prefix: "." | ":") => { - return Array.from(activeModifiers.keys()) - .filter(mod => mod.charAt(0) === prefix) - .map(mod => mod.slice(1)) - .join(" "); - }, - [activeModifiers], - ); const classModifiers = getModifiers("."); const attrModifiers = getModifiers(":"); const exampleHtml = markup diff --git a/packages/eslint-config/eslint-plugin-rules.json b/packages/eslint-config/eslint-plugin-rules.json index e9ef2144e1..cae6ce8b4f 100644 --- a/packages/eslint-config/eslint-plugin-rules.json +++ b/packages/eslint-config/eslint-plugin-rules.json @@ -57,5 +57,7 @@ "react/no-direct-mutation-state": "error", "react/no-find-dom-node": "error", "react/no-string-refs": "error", - "react/self-closing-comp": "error" + "react/self-closing-comp": "error", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn" } diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index bee301b999..d4dcc643e0 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -25,7 +25,7 @@ const tsEslintRules = require("./typescript-eslint-rules.json"); * For TS files, configure typescript-eslint, including type-aware lint rules which use the TS program. */ module.exports = { - plugins: ["@blueprintjs", "header", "import", "jsdoc", "react"], + plugins: ["@blueprintjs", "header", "import", "jsdoc", "react", "react-hooks"], extends: ["plugin:@blueprintjs/recommended", "plugin:import/typescript"], parserOptions: { ecmaVersion: 2022 }, settings: { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 1bd7b7c492..487de5f8f0 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -13,7 +13,8 @@ "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "~2.26.0", "eslint-plugin-jsdoc": "^46.2.4", - "eslint-plugin-react": "^7.32.2" + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0" }, "repository": { "type": "git", diff --git a/yarn.lock b/yarn.lock index ff357c3373..c98fe3f023 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4697,6 +4697,11 @@ eslint-plugin-prettier@^4.2.1: dependencies: prettier-linter-helpers "^1.0.0" +eslint-plugin-react-hooks@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + eslint-plugin-react@^7.32.2: version "7.32.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10"