diff --git a/.changeset/funny-carrots-leave.md b/.changeset/funny-carrots-leave.md new file mode 100644 index 0000000000..f3bc7a7689 --- /dev/null +++ b/.changeset/funny-carrots-leave.md @@ -0,0 +1,11 @@ +--- +"@khanacademy/math-input": major +"@khanacademy/perseus": major +--- + +We've removed the deprecated `useV2Keypad` prop from the MobileKeypad component. + +The V2 keypad is now the default, and the old keypad has been removed. + +Additionally, the mobile keypad no longer accepts the `keypadActive` or +`setKeypadActive` props. It now gets those values itself from the `KeypadContext`. diff --git a/packages/math-input/src/components/__tests__/integration.test.tsx b/packages/math-input/src/components/__tests__/integration.test.tsx index a5f7914201..acd52c68fc 100644 --- a/packages/math-input/src/components/__tests__/integration.test.tsx +++ b/packages/math-input/src/components/__tests__/integration.test.tsx @@ -12,8 +12,8 @@ import React, {useState} from "react"; import {KeypadType} from "../../enums"; import MathInput from "../input/math-input"; +import {MobileKeypad} from "../keypad"; import {KeypadContext, StatefulKeypadContextProvider} from "../keypad-context"; -import KeypadSwitch from "../keypad-switch"; import type {KeypadConfiguration} from "../../types"; @@ -60,11 +60,10 @@ function KeypadWithContext() { {({setKeypadElement}) => { return ( - {}} onAnalyticsEvent={async () => {}} - useV2Keypad /> ); }} diff --git a/packages/math-input/src/components/keypad-legacy/__tests__/gesture-state-machine.test.ts b/packages/math-input/src/components/keypad-legacy/__tests__/gesture-state-machine.test.ts deleted file mode 100644 index 00dd24e3ff..0000000000 --- a/packages/math-input/src/components/keypad-legacy/__tests__/gesture-state-machine.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -import GestureStateMachine from "../gesture-state-machine"; - -import type {Handlers} from "../gesture-state-machine"; - -const swipeThresholdPx = 5; -const longPressWaitTimeMs = 5; -const holdIntervalMs = 5; - -// Generates a set of handlers, to be passed to a GestureStateMachine instance, -// that track any callbacks, along with their arguments, by pushing to the -// provided buffer on call. -const eventTrackers = (buffer) => { - const handlers = {}; - const callbackNames = [ - "onBlur", - "onFocus", - "onTrigger", - "onTouchEnd", - "onLongPress", - "onSwipeChange", - "onSwipeEnd", - ]; - callbackNames.forEach((callbackName) => { - handlers[callbackName] = function () { - // eslint-disable-next-line prefer-rest-params - buffer.push([callbackName, ...arguments]); - }; - }); - return handlers as Handlers; -}; - -// Arbitrary node IDs (representative of arbitrary keys) to be used in testing. -const NodeIds = { - first: "first", - second: "second", - third: "third", - swipeDisabled: "swipeDisabled", - multiPressable: "multiPressable", -}; - -describe("GestureStateMachine", () => { - let eventBuffer; - let stateMachine; - - beforeEach(() => { - eventBuffer = []; - stateMachine = new GestureStateMachine( - eventTrackers(eventBuffer), - { - swipeThresholdPx, - longPressWaitTimeMs, - holdIntervalMs, - }, - // @ts-expect-error TS2322 - [NodeIds.swipeDisabled], - [NodeIds.multiPressable], - ); - }); - - const assertEvents = (expectedEvents) => { - expect(eventBuffer).toStrictEqual(expectedEvents); - }; - - it("should trigger a tap on a simple button", () => { - const touchId = 1; - - // Trigger a touch start, followed immediately by a touch end. - stateMachine.onTouchStart(() => NodeIds.first, touchId, 0); - stateMachine.onTouchEnd(() => NodeIds.first, touchId, 0); - - // Assert that we saw a focus and a touch end, in that order. - const expectedEvents = [ - ["onFocus", NodeIds.first], - ["onTouchEnd", NodeIds.first], - ]; - assertEvents(expectedEvents); - }); - - it("should shift focus to a new button on move", () => { - const touchId = 1; - - // Trigger a touch start on one node before moving over another node and - // releasing. - stateMachine.onTouchStart(() => NodeIds.first, touchId, 0); - stateMachine.onTouchMove(() => NodeIds.second, touchId, 0); - stateMachine.onTouchEnd(() => NodeIds.second, touchId, 0); - - // Assert that we saw a focus on both nodes. - const expectedEvents = [ - ["onFocus", NodeIds.first], - ["onFocus", NodeIds.second], - ["onTouchEnd", NodeIds.second], - ]; - assertEvents(expectedEvents); - }); - - it("should trigger a long press on hold", () => { - const touchId = 1; - - /// Trigger a touch start. - stateMachine.onTouchStart(() => NodeIds.first, touchId, 0); - - // Assert that we see a focus event immediately. - const initialExpectedEvents = [["onFocus", NodeIds.first]]; - assertEvents(initialExpectedEvents); - - jest.advanceTimersByTime(longPressWaitTimeMs); - - const expectedEventsAfterLongPress = [ - ...initialExpectedEvents, - ["onLongPress", NodeIds.first], - ]; - assertEvents(expectedEventsAfterLongPress); - - // Finish up the interaction. - stateMachine.onTouchEnd(() => NodeIds.first, touchId, 0); - - // Assert that we still see a touch-end. - const expectedEventsAfterRelease = [ - ...expectedEventsAfterLongPress, - ["onTouchEnd", NodeIds.first], - ]; - assertEvents(expectedEventsAfterRelease); - }); - - it("should trigger multiple presses on hold", () => { - const touchId = 1; - - // Trigger a touch start on the multi-pressable node. - stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0); - - // Assert that we see an immediate focus and trigger. - const initialExpectedEvents = [ - ["onFocus", NodeIds.multiPressable], - ["onTrigger", NodeIds.multiPressable], - ]; - assertEvents(initialExpectedEvents); - - jest.advanceTimersByTime(holdIntervalMs); - - // Assert that we see an additional trigger after the delay. - const expectedEventsAfterHold = [ - ...initialExpectedEvents, - ["onTrigger", NodeIds.multiPressable], - ]; - assertEvents(expectedEventsAfterHold); - - // Now release, and verify that we see a blur, but no touch-end. - stateMachine.onTouchEnd(() => NodeIds.multiPressable, touchId, 0); - const expectedEventsAfterRelease = [ - ...expectedEventsAfterHold, - ["onBlur"], - ]; - assertEvents(expectedEventsAfterRelease); - }); - - it("should be robust to multiple touch starts", () => { - const touchId = 1; - - // Trigger a touch start on the multi-pressable node twice, because - // the webview was acting up. - stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0); - stateMachine.onTouchStart(() => NodeIds.multiPressable, touchId, 0); - - // Assert that we see only one set of focus and triggers. - const initialExpectedEvents = [ - ["onFocus", NodeIds.multiPressable], - ["onTrigger", NodeIds.multiPressable], - ]; - assertEvents(initialExpectedEvents); - - jest.advanceTimersByTime(holdIntervalMs); - - // Assert that we see an additional trigger after the delay. - const expectedEventsAfterHold = [ - ...initialExpectedEvents, - ["onTrigger", NodeIds.multiPressable], - ]; - assertEvents(expectedEventsAfterHold); - - // Now release, and verify that we see a blur, but no touch-end. - stateMachine.onTouchEnd(() => NodeIds.multiPressable, touchId, 0); - const expectedEventsAfterRelease = [ - ...expectedEventsAfterHold, - ["onBlur"], - ]; - assertEvents(expectedEventsAfterRelease); - - jest.advanceTimersByTime(holdIntervalMs); - // Ensure the touch end cleaned it up, and that we didn't - // create multiple listeners. - assertEvents(expectedEventsAfterRelease); - }); - - /* Swiping. */ - - it("should transition to a swipe", () => { - const touchId = 1; - - // Trigger a touch start, followed by a move past the swipe threshold. - const startX = 0; - const swipeDistancePx = swipeThresholdPx + 1; - stateMachine.onTouchStart(() => NodeIds.first, touchId, startX); - stateMachine.onTouchMove( - () => NodeIds.first, - touchId, - startX + swipeDistancePx, - true, - ); - stateMachine.onTouchEnd( - () => NodeIds.first, - touchId, - startX + swipeDistancePx, - ); - - // Assert that the node is focused and then blurred before transitioning - // to a swipe. - const expectedEvents = [ - ["onFocus", NodeIds.first], - ["onBlur"], - ["onSwipeChange", swipeDistancePx], - ["onSwipeEnd", swipeDistancePx], - ]; - assertEvents(expectedEvents); - }); - - it("should not transition to a swipe when swiping is diabled", () => { - const touchId = 1; - - // Trigger a touch start, followed by a move past the swipe threshold. - const startX = 0; - const swipeDistancePx = swipeThresholdPx + 1; - stateMachine.onTouchStart(() => NodeIds.first, touchId, startX); - stateMachine.onTouchMove( - () => NodeIds.first, - touchId, - startX + swipeDistancePx, - false, - ); - - // Assert that the node is focused but never blurred. - const expectedEvents = [["onFocus", NodeIds.first]]; - assertEvents(expectedEvents); - }); - - it("should not transition to a swipe on drag from a locked key", () => { - const touchId = 1; - - // Trigger a touch start, followed by a move past the swipe threshold. - const startX = 0; - const swipeDistancePx = swipeThresholdPx + 1; - stateMachine.onTouchStart(() => NodeIds.swipeDisabled, touchId, startX); - stateMachine.onTouchMove( - () => NodeIds.swipeDisabled, - touchId, - startX + swipeDistancePx, - true, - ); - - // Assert that the node is focused but never blurred. - const expectedEvents = [["onFocus", NodeIds.swipeDisabled]]; - assertEvents(expectedEvents); - }); - - /* Multi-touch. */ - - it("should respect simultaneous taps by two fingers", () => { - const firstTouchId = 1; - const secondTouchId = 2; - - // Tap down on the first node, then on the second node; then release - // on the second, and then the first. - stateMachine.onTouchStart(() => NodeIds.first, firstTouchId, 0); - stateMachine.onTouchStart(() => NodeIds.second, secondTouchId, 0); - stateMachine.onTouchEnd(() => NodeIds.second, secondTouchId, 0); - stateMachine.onTouchEnd(() => NodeIds.first, firstTouchId, 0); - - // Assert that we saw a focus and a touch end, in that order. - const expectedEvents = [ - ["onFocus", NodeIds.first], - ["onFocus", NodeIds.second], - ["onTouchEnd", NodeIds.second], - ["onTouchEnd", NodeIds.first], - ]; - assertEvents(expectedEvents); - }); - - it("should ignore any additional touches when swiping", () => { - const firstTouchId = 1; - const secondTouchId = 2; - const thirdTouchId = 3; - - // Tap down on the first node, then on the second node. Then use the - const startX = 0; - stateMachine.onTouchStart(() => NodeIds.first, firstTouchId, startX); - stateMachine.onTouchStart(() => NodeIds.second, secondTouchId, startX); - - // Now, swipe with the second finger. - const swipeDistancePx = swipeThresholdPx + 1; - stateMachine.onTouchMove( - () => NodeIds.second, - secondTouchId, - startX + swipeDistancePx, - true, - ); - - const expectedEventsAfterSwipeStart = [ - ["onFocus", NodeIds.first], - ["onFocus", NodeIds.second], - ["onBlur"], - ["onSwipeChange", startX + swipeDistancePx], - ]; - assertEvents(expectedEventsAfterSwipeStart); - - // Send some touch events via the non-swiping but active touch, - // simulating moving the finger over another node, and even moving it - // enough to swipe, before releasing. - stateMachine.onTouchMove(() => NodeIds.first, firstTouchId, 0); - stateMachine.onTouchMove(() => NodeIds.third, firstTouchId, 0); - stateMachine.onTouchMove( - () => NodeIds.third, - firstTouchId, - startX + swipeDistancePx, - true, - ); - stateMachine.onTouchEnd(() => NodeIds.third, firstTouchId, 0); - - // Assert that we see no new events. - assertEvents(expectedEventsAfterSwipeStart); - - // Start a new touch event, over any node. - stateMachine.onTouchStart(() => NodeIds.first, thirdTouchId, 0); - - // Assert that we still see no new events. - assertEvents(expectedEventsAfterSwipeStart); - - // Finally, release with the second finger, which is mid-swipe. - stateMachine.onTouchEnd( - () => NodeIds.second, - secondTouchId, - startX + swipeDistancePx, - ); - const expectedEventsAfterSwipeEnd = [ - ...expectedEventsAfterSwipeStart, - ["onSwipeEnd", startX + swipeDistancePx], - ]; - assertEvents(expectedEventsAfterSwipeEnd); - }); - - it("should track swipe displacement on a per-finger basis", () => { - const firstTouchId = 1; - const firstTouchStartX = 15; - const secondTouchId = 2; - const secondTouchStartX = firstTouchStartX + 2 * swipeThresholdPx; - - // Kick off two separate touch gestures at positions separated by more - // than the swipe displacement. - stateMachine.onTouchStart( - () => NodeIds.first, - firstTouchId, - firstTouchStartX, - ); - stateMachine.onTouchStart( - () => NodeIds.second, - secondTouchId, - secondTouchStartX, - ); - - // Move less than the swipe threshold with both fingers. - stateMachine.onTouchMove( - () => NodeIds.first, - firstTouchId, - firstTouchStartX + swipeThresholdPx - 1, - true, - ); - stateMachine.onTouchMove( - () => NodeIds.second, - secondTouchId, - secondTouchStartX + swipeThresholdPx - 1, - true, - ); - - // Assert that we haven't started swiping--all we've done is focused the - // various nodes. - const initialExpectedEvents = [ - ["onFocus", NodeIds.first], - ["onFocus", NodeIds.second], - ]; - assertEvents(initialExpectedEvents); - - // Swipe past the threshold with one finger. - const swipeDistancePx = swipeThresholdPx + 1; - stateMachine.onTouchMove( - () => NodeIds.first, - firstTouchId, - firstTouchStartX + swipeDistancePx, - true, - ); - const expectedEventsAfterSwipeStart = [ - ...initialExpectedEvents, - ["onBlur"], - ["onSwipeChange", swipeDistancePx], - ]; - assertEvents(expectedEventsAfterSwipeStart); - }); - - it("should be robust to extraneous fingers", () => { - const firstTouchId = 1; - const firstTouchStartX = 15; - const secondTouchId = 2; - const secondTouchStartX = firstTouchStartX + 2 * swipeThresholdPx; - - // The first finger initiates a gesture, but the second finger starts - // elsewhere on the screen and doesn't register a start... - stateMachine.onTouchStart( - () => NodeIds.first, - firstTouchId, - firstTouchStartX, - ); - - // Move the first finger, but less than the swipe threshold, and then - // start showing move events from the second finger (as it slides into - // the components we care about on screen) - stateMachine.onTouchMove( - () => NodeIds.first, - firstTouchId, - firstTouchStartX + swipeThresholdPx - 1, - true, - ); - stateMachine.onTouchMove( - () => NodeIds.second, - secondTouchId, - secondTouchStartX, - true, - ); - - // Assert we've started focusing but haven't blown up. - const initialExpectedEvents = [["onFocus", NodeIds.first]]; - assertEvents(initialExpectedEvents); - }); -}); diff --git a/packages/math-input/src/components/keypad-legacy/__tests__/node-manager.test.ts b/packages/math-input/src/components/keypad-legacy/__tests__/node-manager.test.ts deleted file mode 100644 index 1984261262..0000000000 --- a/packages/math-input/src/components/keypad-legacy/__tests__/node-manager.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import NodeManager from "../node-manager"; - -describe("NodeManager", () => { - let nodeManager; - - beforeEach(() => { - nodeManager = new NodeManager(); - }); - - it("should register a single node with no children", () => { - const nodeId = "1"; - nodeManager.registerDOMNode(nodeId, {}, []); - - expect(nodeManager._nodesById[nodeId]).toBeTruthy(); - expect(nodeManager._orderedIds.includes(nodeId)).toBeTruthy(); - }); - - it("should register a single node with children", () => { - const nodeId = "1"; - const childNodeIds = ["2", "3"]; - nodeManager.registerDOMNode(nodeId, {}, childNodeIds); - - expect(nodeManager._orderedIds.includes(nodeId)).toBeTruthy(); - expect(nodeManager._nodesById[nodeId]).toBeTruthy(); - - for (const childId of childNodeIds) { - // The children should appear in the list of ordered IDs, but not - // in the list of registered nodes. - expect(!nodeManager._nodesById[childId]).toBeTruthy(); - expect(nodeManager._orderedIds.includes(childId)).toBeTruthy(); - } - }); - - it("should order children ahead of their parents", () => { - const nodeId = "1"; - const childNodeIds = ["2", "3"]; - nodeManager.registerDOMNode(nodeId, {}, childNodeIds); - - const parentIndex = nodeManager._orderedIds.indexOf(nodeId); - for (const childId of childNodeIds) { - // The children should appear ahead of the parent in the ordered - // list. - const childIndex = nodeManager._orderedIds.indexOf(childId); - expect(childIndex < parentIndex).toBeTruthy(); - } - }); - - it("should de-dupe the list of node IDs", () => { - const nodeId = "1"; - const childNodeId = "2"; - - // Register both nodes. - nodeManager.registerDOMNode(nodeId, {}, [childNodeId]); - nodeManager.registerDOMNode(childNodeId, {}, []); - - // Verify that both were added to the list of DOM nodes. - for (const id of [nodeId, childNodeId]) { - expect(nodeManager._nodesById[id]).toBeTruthy(); - } - - // Verify that the child is ahead of the parent, and only appears once. - expect(nodeManager._orderedIds).toStrictEqual([childNodeId, nodeId]); - }); - - it("should handle multiple sets of children", () => { - const firstNodeId = "1"; - const firstNodeChildIds = ["2", "3"]; - const secondNodeId = "4"; - const secondNodeChildIds = ["5", "6"]; - const nodeChildIdPairs = [ - [firstNodeId, firstNodeChildIds], - [secondNodeId, secondNodeChildIds], - ]; - - for (const [nodeId, childNodeIds] of nodeChildIdPairs) { - nodeManager.registerDOMNode(nodeId, {}, childNodeIds); - } - - for (const [nodeId, childNodeIds] of nodeChildIdPairs) { - const parentIndex = nodeManager._orderedIds.indexOf(nodeId); - for (const childId of childNodeIds) { - // The children should appear ahead of the parent in the - // ordered list. - const childIndex = nodeManager._orderedIds.indexOf(childId); - expect(childIndex < parentIndex).toBeTruthy(); - } - } - }); -}); diff --git a/packages/math-input/src/components/keypad-legacy/compute-layout-parameters.ts b/packages/math-input/src/components/keypad-legacy/compute-layout-parameters.ts deleted file mode 100644 index 910cb3a498..0000000000 --- a/packages/math-input/src/components/keypad-legacy/compute-layout-parameters.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * An algorithm for computing the appropriate layout parameters for the keypad, - * including the size of the buttons and whether or not to render fullscreen, - * taking into account a number of factors including the size of the screen, the - * orientation of the screen, the presence of browser chrome, the presence of - * other exercise-related chrome, the size of the input box, the parameters that - * define the keypad (i.e., the number of rows, columns, and pages), and so - * forth. - * - * The computations herein make some strong assumptions about the sizes of - * various other elements and the situations under which they will be visible - * (e.g., browser chrome). However, this is just a heuristic--it's not crucial - * that our buttons are sized in a pixel-perfect manner, but rather, that we - * make a balanced use of space. - * - * Note that one goal of the algorithm is to avoid resizing the keypad in the - * face of dynamic browser chrome. In order to avoid that awkwardness, we tend - * to be conservative in our measurements and make things smaller than they - * might need to be. - */ - -import {DeviceOrientation, LayoutMode} from "../../enums"; -import { - pageIndicatorHeightPx, - toolbarHeightPx, - navigationPadWidthPx, - innerBorderWidthPx, -} from "../common-style"; - -import type {GridDimensions, WidthHeight} from "./store/types"; - -const minButtonHeight = 48; -const maxButtonSize = 64; -const minSpaceAboveKeypad = 32; - -// These values are taken from an iPhone 5, but should be consistent with the -// iPhone 4 as well. Regardless, these are meant to be representative of the -// possible types of browser chrome that could appear in various context, rather -// than pixel-perfect for every device. -const safariNavBarWhenShrunk = 44; -const safariNavBarWhenExpanded = 64; -const safariToolbar = 44; - -// In mobile Safari, the browser chrome is completely hidden in landscape, -// though a shrunken navbar and full-sized toolbar on scroll. In portrait, the -// shrunken navbar is always visible, but expands on scroll (and the toolbar -// appears as well). -const maxLandscapeBrowserChrome = safariNavBarWhenShrunk + safariToolbar; -const maxPortraitBrowserChrome = - safariToolbar + (safariNavBarWhenExpanded - safariNavBarWhenShrunk); - -// This represents the 'worst case' aspect ratio that we care about (for -// portrait layouts). It's taken from the iPhone 4. The height is computed by -// taking the height of the device and removing the persistent, shrunken navbar. -// (We don't need to account for the expanded navbar, since we include the -// difference when reserving space above the keypad.) -const worstCaseAspectRatio = 320 / (480 - safariNavBarWhenShrunk); - -type ComputedLayoutProperty = { - buttonDimensions: WidthHeight; - layoutMode: LayoutMode; -}; - -function getButtonWidth( - gridDimensions: GridDimensions, - containerDimensions: WidthHeight, - navigationPadEnabled: boolean, - paginationEnabled: boolean, - isLandscape: boolean, -): number { - const {numColumns, numPages} = gridDimensions; - - // We can use the container width as the effective width. - let effectiveWidth = containerDimensions.width; - if (navigationPadEnabled) { - effectiveWidth -= navigationPadWidthPx; - } - - let buttonWidthPx; - if (numPages > 1) { - const effectiveNumColumns = paginationEnabled - ? numColumns - : numColumns * numPages; - buttonWidthPx = effectiveWidth / effectiveNumColumns; - } else { - buttonWidthPx = isLandscape - ? maxButtonSize - : effectiveWidth / numColumns; - } - - return buttonWidthPx; -} - -function getButtonHeight( - gridDimensions: GridDimensions, - pageDimensions: WidthHeight, - containerDimensions: WidthHeight, - paginationEnabled: boolean, - toolbarEnabled: boolean, - isLandscape: boolean, -) { - const {numMaxVisibleRows} = gridDimensions; - - // In many cases, the browser chrome will already have been factored - // into `pageHeight`. But we have no way of knowing if that's - // the case or not. As such, we take a conservative approach and - // assume that the chrome is _never_ included in `pageHeight`. - const browserChromeHeight = isLandscape - ? maxLandscapeBrowserChrome - : maxPortraitBrowserChrome; - - // Count up all the space that we need to reserve on the page. - // Namely, we need to account for: - // 1. Space between the keypad and the top of the page. - // 2. The presence of the exercise toolbar. - // 3. The presence of the view pager indicator. - // 4. Any browser chrome that may appear later. - const reservedSpace = - minSpaceAboveKeypad + - browserChromeHeight + - (toolbarEnabled ? toolbarHeightPx : 0) + - (paginationEnabled ? pageIndicatorHeightPx : 0); - - // For the height, we take - // another conservative measure when in portrait by assuming that - // the device has the worst possible aspect ratio. In other words, - // we ignore the device height in portrait and assume the worst. - // This prevents the keypad from changing size when browser chrome - // appears and disappears. - const effectiveHeight = isLandscape - ? pageDimensions.height - : containerDimensions.width / worstCaseAspectRatio; - - // In computing the - // height, accommodate for the maximum number of rows that will ever be - // visible (since the toggling of popovers can increase the number of - // visible rows). - const maxKeypadHeight = effectiveHeight - reservedSpace; - - const buttonHeightPx = Math.max( - Math.min(maxKeypadHeight / numMaxVisibleRows, maxButtonSize), - minButtonHeight, - ); - - return buttonHeightPx; -} - -export const computeLayoutParameters = ( - gridDimensions: GridDimensions, - pageDimensions: WidthHeight, - containerDimensions: WidthHeight, - deviceOrientation: DeviceOrientation, - navigationPadEnabled: boolean, - paginationEnabled: boolean, - toolbarEnabled: boolean, -): ComputedLayoutProperty => { - const {numColumns, numPages} = gridDimensions; - - // First, compute some values that will be used in multiple computations. - const effectiveNumColumns = paginationEnabled - ? numColumns - : numColumns * numPages; - - // Then, compute the button dimensions based on the provided parameters. - const isLandscape = deviceOrientation === DeviceOrientation.LANDSCAPE; - - const buttonWidth = getButtonWidth( - gridDimensions, - containerDimensions, - navigationPadEnabled, - paginationEnabled, - isLandscape, - ); - - const buttonHeight = getButtonHeight( - gridDimensions, - pageDimensions, - containerDimensions, - paginationEnabled, - toolbarEnabled, - isLandscape, - ); - - const buttonDimensions = { - width: buttonWidth, - height: buttonHeight, - }; - - // Finally, determine whether the keypad should be rendered in the - // fullscreen layout by determining its resultant width. - const numSeparators = - (navigationPadEnabled ? 1 : 0) + - (!paginationEnabled ? numPages - 1 : 0); - const keypadWidth = - effectiveNumColumns * buttonDimensions.width + - (navigationPadEnabled ? navigationPadWidthPx : 0) + - numSeparators * innerBorderWidthPx; - return { - buttonDimensions, - layoutMode: - keypadWidth >= containerDimensions.width - ? LayoutMode.FULLSCREEN - : LayoutMode.COMPACT, - }; -}; diff --git a/packages/math-input/src/components/keypad-legacy/corner-decal.tsx b/packages/math-input/src/components/keypad-legacy/corner-decal.tsx deleted file mode 100644 index 8da0da80c4..0000000000 --- a/packages/math-input/src/components/keypad-legacy/corner-decal.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * A small triangular decal to sit in the corner of a parent component. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import {View} from "../../fake-react-native-web/index"; -import {offBlack} from "../common-style"; - -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -type Props = { - style: StyleType; -}; - -class CornerDecal extends React.Component { - render() { - const {style} = this.props; - - const containerStyle = [ - styles.container, - ...(Array.isArray(style) ? style : [style]), - ]; - - return ( - - - - - - ); - } -} - -const triangleSizePx = 7; - -const styles = StyleSheet.create({ - container: { - position: "absolute", - top: 0, - right: 0, - width: triangleSizePx, - height: triangleSizePx, - }, -}); - -export default CornerDecal; diff --git a/packages/math-input/src/components/keypad-legacy/echo-manager.tsx b/packages/math-input/src/components/keypad-legacy/echo-manager.tsx deleted file mode 100644 index bacc767670..0000000000 --- a/packages/math-input/src/components/keypad-legacy/echo-manager.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/** - * A component that renders and animates the selection state effect effect. - */ - -import * as React from "react"; -import {TransitionGroup, CSSTransition} from "react-transition-group"; - -import KeyConfigs from "../../data/key-configs"; -import {EchoAnimationType} from "../../enums"; - -import KeypadButton from "./keypad-button"; -import * as zIndexes from "./z-indexes"; - -import type Key from "../../data/keys"; -import type {Bound, Echo as EchoType} from "../../types"; - -type EchoProps = { - animationDurationMs: number; - id: Key; - initialBounds: Bound; - onAnimationFinish: () => void; -}; - -class Echo extends React.Component { - componentDidMount() { - // NOTE(charlie): This is somewhat unfortunate, as the component is - // encoding information about its own animation, of which it should be - // ignorant. However, there doesn't seem to be a cleaner way to make - // this happen, and at least here, all the animation context is - // colocated in this file. - const {animationDurationMs, onAnimationFinish} = this.props; - setTimeout(() => onAnimationFinish(), animationDurationMs); - } - - render() { - const {id, initialBounds} = this.props; - const {icon} = KeyConfigs[id]; - - const containerStyle: any = { - zIndex: zIndexes.echo, - position: "absolute", - pointerEvents: "none", - ...initialBounds, - }; - - // NOTE(charlie): In some browsers, Aphrodite doesn't seem to flush its - // styles quickly enough, so there's a flickering effect on the first - // animation. Thus, it's much safer to do the styles purely inline. - // makes this difficult because some of its defaults, which are - // applied via StyleSheet, will override our inlines. - return ( -
- -
- ); - } -} - -type EchoManagerProps = { - echoes: ReadonlyArray; - onAnimationFinish?: (animationId: string) => void; -}; - -class EchoManager extends React.Component { - _animationConfigForType = (animationType) => { - // NOTE(charlie): These must be kept in sync with the transition - // durations and classnames specified in echo.css. - let animationDurationMs; - let animationTransitionName; - - switch (animationType) { - case EchoAnimationType.SLIDE_AND_FADE: - animationDurationMs = 400; - animationTransitionName = "echo-slide-and-fade"; - break; - - case EchoAnimationType.FADE_ONLY: - animationDurationMs = 300; - animationTransitionName = "echo-fade-only"; - break; - - case EchoAnimationType.LONG_FADE_ONLY: - animationDurationMs = 400; - animationTransitionName = "echo-long-fade-only"; - break; - - default: - throw new Error( - `Invalid echo animation type: ${animationType}`, - ); - } - - return { - animationDurationMs, - animationTransitionName, - }; - }; - - render() { - const {echoes, onAnimationFinish} = this.props; - - return ( - - {Object.keys(EchoAnimationType).map((animationType) => { - // Collect the relevant parameters for the animation type, and - // filter for the appropriate echoes. - const {animationDurationMs, animationTransitionName} = - this._animationConfigForType(animationType); - const echoesForType = echoes.filter((echo) => { - return echo.animationType === animationType; - }); - - // TODO(charlie): Manage this animation with Aphrodite styles. - // Right now, there's a bug in the autoprefixer that breaks CSS - // transitions on mobile Safari. - // See: https://github.com/Khan/aphrodite/issues/68. - // As such, we have to do this with a stylesheet. - return ( - - {echoesForType.map((echo) => { - const {animationId} = echo; - return ( - - - onAnimationFinish?.(animationId) - } - {...echo} - /> - - ); - })} - - ); - })} - - ); - } -} - -export default EchoManager; diff --git a/packages/math-input/src/components/keypad-legacy/empty-keypad-button.tsx b/packages/math-input/src/components/keypad-legacy/empty-keypad-button.tsx deleted file mode 100644 index bb1362364f..0000000000 --- a/packages/math-input/src/components/keypad-legacy/empty-keypad-button.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * A keypad button containing no symbols and triggering no actions on click. - */ - -import * as React from "react"; -import {connect} from "react-redux"; - -import KeyConfigs from "../../data/key-configs"; - -import KeypadButton from "./keypad-button"; - -import type GestureManager from "./gesture-manager"; -import type {State} from "./store/types"; - -interface ReduxProps { - gestureManager: GestureManager; -} - -class EmptyKeypadButton extends React.Component { - render() { - const {gestureManager, ...rest} = this.props; - - // Register touch events on the button, but don't register its DOM node - // or compute focus state or anything like that. We want the gesture - // manager to know about touch events that start on empty buttons, but - // we don't need it to know about their DOM nodes, as it doesn't need - // to focus them or trigger presses. - return ( - ) => - gestureManager.onTouchStart(evt) - } - onTouchEnd={(evt: React.TouchEvent) => - gestureManager.onTouchEnd(evt) - } - onTouchMove={(evt: React.TouchEvent) => - gestureManager.onTouchMove(evt) - } - onTouchCancel={(evt: React.TouchEvent) => - gestureManager.onTouchCancel(evt) - } - {...KeyConfigs.NOOP} - {...rest} - /> - ); - } -} - -const mapStateToProps = (state: State): ReduxProps => { - const {gestures} = state; - return { - gestureManager: gestures.gestureManager, - }; -}; - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - EmptyKeypadButton, -); diff --git a/packages/math-input/src/components/keypad-legacy/expression-keypad.tsx b/packages/math-input/src/components/keypad-legacy/expression-keypad.tsx deleted file mode 100644 index 14ff6efcb2..0000000000 --- a/packages/math-input/src/components/keypad-legacy/expression-keypad.tsx +++ /dev/null @@ -1,315 +0,0 @@ -/** - * A keypad that includes all of the expression symbols. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; -import {connect} from "react-redux"; - -import KeyConfigs from "../../data/key-configs"; -import {BorderStyles} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import {valueGrey, controlGrey} from "../common-style"; -import {CursorContext} from "../input/cursor-contexts"; - -import ManyKeypadButton from "./many-keypad-button"; -import Styles from "./styles"; -import TouchableKeypadButton from "./touchable-keypad-button"; -import TwoPageKeypad from "./two-page-keypad"; - -import type {KeypadLayout} from "../../types"; -import type {State} from "./store/types"; - -const {row, column, oneColumn, fullWidth, roundedTopLeft, roundedTopRight} = - Styles; - -interface ReduxProps { - cursorContext?: typeof CursorContext[keyof typeof CursorContext]; - dynamicJumpOut: boolean; -} - -interface Props extends ReduxProps { - extraKeys?: ReadonlyArray; - roundTopLeft: boolean; - roundTopRight: boolean; -} - -export const expressionKeypadLayout: KeypadLayout = { - rows: 4, - columns: 5, - numPages: 2, - // Since we include a two-key popover in the top-right, when the popover - // is visible, the keypad will expand to fill the equivalent of five - // rows vertically. - maxVisibleRows: 4, -}; - -class ExpressionKeypad extends React.Component { - render() { - const { - cursorContext, - dynamicJumpOut, - extraKeys, - roundTopLeft, - roundTopRight, - } = this.props; - - let dismissOrJumpOutKey; - if (dynamicJumpOut) { - switch (cursorContext) { - case CursorContext.IN_PARENS: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_PARENTHESES; - break; - - case CursorContext.IN_SUPER_SCRIPT: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_EXPONENT; - break; - - case CursorContext.IN_SUB_SCRIPT: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_BASE; - break; - - case CursorContext.BEFORE_FRACTION: - dismissOrJumpOutKey = KeyConfigs.JUMP_INTO_NUMERATOR; - break; - - case CursorContext.IN_NUMERATOR: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_NUMERATOR; - break; - - case CursorContext.IN_DENOMINATOR: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_DENOMINATOR; - break; - - case CursorContext.NONE: - default: - dismissOrJumpOutKey = KeyConfigs.DISMISS; - break; - } - } else { - dismissOrJumpOutKey = KeyConfigs.DISMISS; - } - - const rightPageStyle = [ - row, - fullWidth, - styles.rightPage, - roundTopRight && roundedTopRight, - ]; - const rightPage = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - - const leftPageStyle = [ - row, - fullWidth, - styles.leftPage, - roundTopLeft && roundedTopLeft, - ]; - const leftPage = ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - - return ; - } -} - -const styles = StyleSheet.create({ - // NOTE(charlie): These backgrounds are applied to as to fill in some - // unfortunate 'cracks' in the layout. However, not all keys in the first - // page use this background color (namely, the 'command' keys, backspace and - // dismiss). - // TODO(charlie): Apply the proper background between the 'command' keys. - rightPage: { - backgroundColor: valueGrey, - }, - - leftPage: { - backgroundColor: controlGrey, - }, -}); - -const mapStateToProps = (state: State): ReduxProps => { - return { - cursorContext: state.input.cursor?.context, - dynamicJumpOut: !state.layout.navigationPadEnabled, - }; -}; - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - ExpressionKeypad, -); diff --git a/packages/math-input/src/components/keypad-legacy/fraction-keypad.tsx b/packages/math-input/src/components/keypad-legacy/fraction-keypad.tsx deleted file mode 100644 index e572dd5d7e..0000000000 --- a/packages/math-input/src/components/keypad-legacy/fraction-keypad.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/** - * A keypad that includes the digits, as well as the symbols required to deal - * with fractions, decimals, and percents. - */ - -import * as React from "react"; -import {connect} from "react-redux"; - -import KeyConfigs from "../../data/key-configs"; -import {BorderStyles} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import {CursorContext} from "../input/cursor-contexts"; - -import Keypad from "./keypad"; -import Styles from "./styles"; -import TouchableKeypadButton from "./touchable-keypad-button"; - -import type {KeypadLayout} from "../../types"; -import type {State} from "./store/types"; - -const {row, roundedTopLeft, roundedTopRight} = Styles; - -interface ReduxProps { - cursorContext?: typeof CursorContext[keyof typeof CursorContext]; - dynamicJumpOut: boolean; -} - -interface Props extends ReduxProps { - roundTopLeft: boolean; - roundTopRight: boolean; -} - -export const fractionKeypadLayout: KeypadLayout = { - rows: 4, - columns: 4, - numPages: 1, - // Since we include a two-key popover in the top-right, when the popover - // is visible, the keypad will expand to fill the equivalent of five - // rows vertically. - maxVisibleRows: 5, -}; - -class FractionKeypad extends React.Component { - render() { - const {cursorContext, dynamicJumpOut, roundTopLeft, roundTopRight} = - this.props; - - let dismissOrJumpOutKey; - if (dynamicJumpOut) { - switch (cursorContext) { - case CursorContext.IN_PARENS: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_PARENTHESES; - break; - - case CursorContext.IN_SUPER_SCRIPT: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_EXPONENT; - break; - - case CursorContext.IN_SUB_SCRIPT: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_BASE; - break; - - case CursorContext.BEFORE_FRACTION: - dismissOrJumpOutKey = KeyConfigs.JUMP_INTO_NUMERATOR; - break; - - case CursorContext.IN_NUMERATOR: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_NUMERATOR; - break; - - case CursorContext.IN_DENOMINATOR: - dismissOrJumpOutKey = KeyConfigs.JUMP_OUT_DENOMINATOR; - break; - - case CursorContext.NONE: - default: - dismissOrJumpOutKey = KeyConfigs.DISMISS; - break; - } - } else { - dismissOrJumpOutKey = KeyConfigs.DISMISS; - } - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } -} - -const mapStateToProps = (state: State): ReduxProps => { - return { - cursorContext: state.input.cursor?.context, - dynamicJumpOut: !state.layout.navigationPadEnabled, - }; -}; - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - FractionKeypad, -); diff --git a/packages/math-input/src/components/keypad-legacy/gesture-manager.ts b/packages/math-input/src/components/keypad-legacy/gesture-manager.ts deleted file mode 100644 index b53192d162..0000000000 --- a/packages/math-input/src/components/keypad-legacy/gesture-manager.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * A high-level manager for our gesture system. In particular, this class - * connects our various bits of logic for managing gestures and interactions, - * and links them together. - */ - -import GestureStateMachine from "./gesture-state-machine"; -import NodeManager from "./node-manager"; -import PopoverStateMachine from "./popover-state-machine"; - -import type Key from "../../data/keys"; -import type {ActiveNodesObj, LayoutProps} from "../../types"; -import type * as React from "react"; - -const coordsForEvent = (evt) => { - return [evt.changedTouches[0].clientX, evt.changedTouches[0].clientY]; -}; - -type Options = { - swipeEnabled: boolean; -}; - -type Handlers = { - onSwipeChange?: (dx: number) => void; - onSwipeEnd?: (dx: number) => void; - onActiveNodesChanged: (activeNodes: ActiveNodesObj) => void; - onClick: (key: Key, layoutProps: LayoutProps, inPopover: boolean) => void; -}; - -class GestureManager { - swipeEnabled: boolean; - trackEvents: boolean; - nodeManager: NodeManager; - popoverStateMachine: PopoverStateMachine; - gestureStateMachine: GestureStateMachine; - - constructor( - options: Options, - handlers: Handlers, - disabledSwipeKeys: ReadonlyArray, - multiPressableKeys: ReadonlyArray, - ) { - const {swipeEnabled} = options; - - this.swipeEnabled = swipeEnabled; - - // Events aren't tracked until event tracking is enabled. - this.trackEvents = false; - - this.nodeManager = new NodeManager(); - this.popoverStateMachine = new PopoverStateMachine({ - onActiveNodesChanged: (activeNodes) => { - const {popover, ...rest} = activeNodes; - handlers.onActiveNodesChanged({ - popover: popover && { - parentId: popover.parentId, - bounds: this.nodeManager.layoutPropsForId( - popover.parentId, - ).initialBounds, - childKeyIds: popover.childIds, - }, - ...rest, - }); - }, - /** - * `onClick` takes two arguments: - * - * @param {string} keyId - the identifier key that should initiate - * a click - * @param {string} domNodeId - the identifier of the DOM node on - * which the click should be considered - * to have occurred - * @param {bool} inPopover - whether the key was contained within a - * popover - * - * These two parameters will often be equivalent. They will differ, - * though, when a popover button is itself clicked, in which case - * we need to mimic the effects of clicking on its 'primary' child - * key, but animate the click on the popover button. - */ - onClick: (keyId, domNodeId, inPopover) => { - handlers.onClick( - keyId, - this.nodeManager.layoutPropsForId(domNodeId), - inPopover, - ); - }, - }); - this.gestureStateMachine = new GestureStateMachine( - { - onFocus: (id) => { - this.popoverStateMachine.onFocus(id); - }, - onLongPress: (id) => { - this.popoverStateMachine.onLongPress(id); - }, - onTouchEnd: (id) => { - this.popoverStateMachine.onTouchEnd(id); - }, - onBlur: () => { - this.popoverStateMachine.onBlur(); - }, - onSwipeChange: handlers.onSwipeChange, - onSwipeEnd: handlers.onSwipeEnd, - onTrigger: (id) => { - this.popoverStateMachine.onTrigger(id); - }, - }, - {}, - disabledSwipeKeys, - multiPressableKeys, - ); - } - - /** - * Handle a touch-start event that originated in a node registered with the - * gesture system. - * - * @param {React.TouchEvent} evt - the raw touch event from the browser - * @param {string} id - the identifier of the DOM node in which the touch - * occurred - */ - onTouchStart( - evt: React.TouchEvent, - id?: string | undefined, - ) { - if (!this.trackEvents) { - return; - } - - const [x] = coordsForEvent(evt); - - // TODO(charlie): It doesn't seem to be guaranteed that every touch - // event on `changedTouches` originates from the node through which this - // touch event was sent. In that case, we'd be inappropriately reporting - // the starting node ID. - for (let i = 0; i < evt.changedTouches.length; i++) { - this.gestureStateMachine.onTouchStart( - () => id, - evt.changedTouches[i].identifier, - x, - ); - } - - // If an event started in a view that we're managing, we'll handle it - // all the way through. - evt.preventDefault(); - } - - /** - * Handle a touch-move event that originated in a node registered with the - * gesture system. - * - * @param {React.TouchEvent} evt - the raw touch event from the browser - */ - onTouchMove(evt: React.TouchEvent) { - if (!this.trackEvents) { - return; - } - - const swipeLocked = this.popoverStateMachine.isPopoverVisible(); - const swipeEnabled = this.swipeEnabled && !swipeLocked; - const [x, y] = coordsForEvent(evt); - for (let i = 0; i < evt.changedTouches.length; i++) { - this.gestureStateMachine.onTouchMove( - () => this.nodeManager.idForCoords(x, y), - evt.changedTouches[i].identifier, - x, - swipeEnabled, - ); - } - } - - /** - * Handle a touch-end event that originated in a node registered with the - * gesture system. - * - * @param {React.TouchEvent} evt - the raw touch event from the browser - */ - onTouchEnd(evt: React.TouchEvent) { - if (!this.trackEvents) { - return; - } - - const [x, y] = coordsForEvent(evt); - for (let i = 0; i < evt.changedTouches.length; i++) { - this.gestureStateMachine.onTouchEnd( - () => this.nodeManager.idForCoords(x, y), - evt.changedTouches[i].identifier, - x, - ); - } - } - - /** - * Handle a touch-cancel event that originated in a node registered with the - * gesture system. - * - * @param {React.TouchEvent} evt - the raw touch event from the browser - */ - onTouchCancel(evt: React.TouchEvent) { - if (!this.trackEvents) { - return; - } - - for (let i = 0; i < evt.changedTouches.length; i++) { - this.gestureStateMachine.onTouchCancel( - evt.changedTouches[i].identifier, - ); - } - } - - /** - * Register a DOM node with a given identifier. - * - * @param {string} id - the identifier of the given node - * @param {node} domNode - the DOM node linked to the identifier - * @param {string[]} childIds - the identifiers of any DOM nodes that - * should be considered children of this node, - * in that they should take priority when - * intercepting touch events - * @param {object} borders - an opaque object describing the node's borders - */ - registerDOMNode(id, domNode, childIds) { - this.nodeManager.registerDOMNode(id, domNode, childIds); - this.popoverStateMachine.registerPopover(id, childIds); - } - - /** - * Unregister the DOM node with the given identifier. - * - * @param {string} id - the identifier of the node to unregister - */ - unregisterDOMNode(id) { - this.nodeManager.unregisterDOMNode(id); - this.popoverStateMachine.unregisterPopover(id); - } - - /** - * Enable event tracking for the gesture manager. - */ - enableEventTracking() { - this.trackEvents = true; - } - - /** - * Disable event tracking for the gesture manager. When called, the gesture - * manager will drop any events received by managed nodes. - */ - disableEventTracking() { - this.trackEvents = false; - } -} - -export default GestureManager; diff --git a/packages/math-input/src/components/keypad-legacy/gesture-state-machine.ts b/packages/math-input/src/components/keypad-legacy/gesture-state-machine.ts deleted file mode 100644 index 67acb9c43c..0000000000 --- a/packages/math-input/src/components/keypad-legacy/gesture-state-machine.ts +++ /dev/null @@ -1,329 +0,0 @@ -import type Key from "../../data/keys"; - -/** - * The state machine that backs our gesture system. In particular, this state - * machine manages the interplay between focuses, touch ups, and swiping. - * It is entirely ignorant of the existence of popovers and the positions of - * DOM nodes, operating solely on IDs. The state machine does accommodate for - * multi-touch interactions, tracking gesture state on a per-touch basis. - */ - -// exported for tests -export type Handlers = { - onFocus: (id: string) => void; - onBlur: () => void; - onTrigger: (id: string) => void; - onLongPress: (id: string) => void; - onSwipeChange?: (x: number) => void; - onSwipeEnd?: (x: number) => void; - onTouchEnd: (id: string) => void; -}; - -type Options = { - longPressWaitTimeMs: number; - swipeThresholdPx: number; - holdIntervalMs: number; -}; - -type TouchState = { - activeNodeId: Key; - pressAndHoldIntervalId: number | null; - longPressTimeoutId: number | null; - swipeLocked: boolean; - startX: number; -}; - -type TouchStateMap = Record; - -type SwipeState = { - touchId: Key; - startX: number; -}; - -const defaultOptions: Options = { - longPressWaitTimeMs: 50, - swipeThresholdPx: 20, - holdIntervalMs: 250, -}; - -class GestureStateMachine { - handlers: Handlers; - options: Options; - swipeDisabledNodeIds: ReadonlyArray; - multiPressableKeys: ReadonlyArray; - touchState: Partial; - swipeState: SwipeState | null; - - constructor( - handlers: Handlers, - options: Partial, - swipeDisabledNodeIds?: ReadonlyArray, - multiPressableKeys?: ReadonlyArray, - ) { - this.handlers = handlers; - this.options = { - ...defaultOptions, - ...options, - }; - this.swipeDisabledNodeIds = swipeDisabledNodeIds || []; - this.multiPressableKeys = multiPressableKeys || []; - - // TODO(charlie): Add types for this file. It's not great that we're now - // passing around these opaque state objects. - this.touchState = {}; - this.swipeState = null; - } - - _maybeCancelLongPressForTouch(touchId) { - const {longPressTimeoutId} = this.touchState[touchId]; - if (longPressTimeoutId) { - clearTimeout(longPressTimeoutId); - this.touchState[touchId] = { - ...this.touchState[touchId], - longPressTimeoutId: null, - }; - } - } - - _maybeCancelPressAndHoldForTouch(touchId) { - const {pressAndHoldIntervalId} = this.touchState[touchId]; - if (pressAndHoldIntervalId) { - // If there was an interval set to detect holds, clear it out. - clearInterval(pressAndHoldIntervalId); - this.touchState[touchId] = { - ...this.touchState[touchId], - pressAndHoldIntervalId: null, - }; - } - } - - _cleanupTouchEvent(touchId) { - this._maybeCancelLongPressForTouch(touchId); - this._maybeCancelPressAndHoldForTouch(touchId); - delete this.touchState[touchId]; - } - - /** - * Handle a focus event on the node with the given identifier, which may be - * `null` to indicate that the user has dragged their finger off of any - * registered nodes, but is still in the middle of a gesture. - * - * @param {string|null} id - the identifier of the newly focused node, or - * `null` if no node is focused - * @param {number} touchId - a unique identifier associated with the touch - */ - _onFocus(id, touchId) { - // If we're in the middle of a long-press, cancel it. - this._maybeCancelLongPressForTouch(touchId); - - // Reset any existing hold-detecting interval. - this._maybeCancelPressAndHoldForTouch(touchId); - - // Set the focused node ID and handle the focus event. - // Note: we can call `onFocus` with `null` IDs. The semantics of an - // `onFocus` with a `null` ID differs from that of `onBlur`. The former - // indicates that a gesture that can focus future nodes is still in - // progress, but that no node is currently focused. The latter - // indicates that the gesture has ended and nothing will be focused. - this.touchState[touchId] = { - ...this.touchState[touchId], - activeNodeId: id, - }; - this.handlers.onFocus(id); - - if (id) { - // Handle logic for repeating button presses. - if (this.multiPressableKeys.includes(id)) { - // Start by triggering a click, iOS style. - this.handlers.onTrigger(id); - - // Set up a new hold detector for the current button. - this.touchState[touchId] = { - ...this.touchState[touchId], - pressAndHoldIntervalId: setInterval(() => { - // On every cycle, trigger the click handler. - this.handlers.onTrigger(id); - }, this.options.holdIntervalMs), - }; - } else { - // Set up a new hold detector for the current button. - this.touchState[touchId] = { - ...this.touchState[touchId], - longPressTimeoutId: setTimeout(() => { - this.handlers.onLongPress(id); - this.touchState[touchId] = { - ...this.touchState[touchId], - longPressTimeoutId: null, - }; - }, this.options.longPressWaitTimeMs), - }; - } - } - } - - /** - * Clear out all active gesture information. - */ - _onSwipeStart() { - for (const activeTouchId of Object.keys(this.touchState)) { - this._maybeCancelLongPressForTouch(activeTouchId); - this._maybeCancelPressAndHoldForTouch(activeTouchId); - } - this.touchState = {}; - this.handlers.onBlur(); - } - - /** - * A function that returns the identifier of the node over which the touch - * event occurred. This is provided as a piece of lazy computation, as - * computing the DOM node for a given point is expensive, and the state - * machine won't always need that information. For example, if the user is - * swiping, then `onTouchMove` needs to be performant and doesn't care about - * the node over which the touch occurred. - * - * @typedef idComputation - * @returns {DOMNode} - the identifier of the node over which the touch - * occurred - */ - - /** - * Handle a touch-start event on the node with the given identifer. - * - * @param {idComputation} getId - a function that returns identifier of the - * node over which the start event occurred - * @param {number} touchId - a unique identifier associated with the touch - */ - onTouchStart(getId, touchId, pageX) { - // Ignore any touch events that start mid-swipe. - if (this.swipeState) { - return; - } - - if (this.touchState[touchId]) { - // It turns out we can get multiple touch starts with no - // intervening move, end, or cancel events in Android WebViews. - // TODO(benkomalo): it's not entirely clear why this happens, but - // it seems to happen with the backspace button. It may be related - // to FastClick (https://github.com/ftlabs/fastclick/issues/71) - // though I haven't verified, and it's probably good to be robust - // here anyways. - return; - } - - const startingNodeId = getId(); - this.touchState[touchId] = { - swipeLocked: this.swipeDisabledNodeIds.includes(startingNodeId), - startX: pageX, - }; - - this._onFocus(startingNodeId, touchId); - } - - /** - * Handle a touch-move event on the node with the given identifer. - * - * @param {idComputation} getId - a function that returns identifier of the - * node over which the move event occurred - * @param {number} touchId - a unique identifier associated with the touch - * @param {number} pageX - the x coordinate of the touch - * @param {boolean} swipeEnabled - whether the system should allow for - * transitions into a swiping state - */ - onTouchMove(getId, touchId, pageX, swipeEnabled) { - if (this.swipeState) { - // Only respect the finger that started a swipe. Any other lingering - // gestures are ignored. - if (this.swipeState.touchId === touchId) { - this.handlers.onSwipeChange?.(pageX - this.swipeState.startX); - } - } else if (this.touchState[touchId]) { - // It could be touch events started outside the keypad and - // moved into it; ignore them. - const {activeNodeId, startX, swipeLocked} = - this.touchState[touchId]; - - const dx = pageX - startX; - const shouldBeginSwiping = - swipeEnabled && - !swipeLocked && - Math.abs(dx) > this.options.swipeThresholdPx; - - if (shouldBeginSwiping) { - this._onSwipeStart(); - - // Trigger the swipe. - this.swipeState = { - touchId, - startX, - }; - this.handlers.onSwipeChange?.(pageX - this.swipeState.startX); - } else { - const id = getId(); - if (id !== activeNodeId) { - this._onFocus(id, touchId); - } - } - } - } - - /** - * Handle a touch-end event on the node with the given identifer. - * - * @param {idComputation} getId - a function that returns identifier of the - * node over which the end event occurred - * @param {number} touchId - a unique identifier associated with the touch - * @param {number} pageX - the x coordinate of the touch - */ - onTouchEnd(getId, touchId, pageX) { - if (this.swipeState) { - // Only respect the finger that started a swipe. Any other lingering - // gestures are ignored. - if (this.swipeState.touchId === touchId) { - this.handlers.onSwipeEnd?.(pageX - this.swipeState.startX); - this.swipeState = null; - } - } else if (this.touchState[touchId]) { - // It could be touch events started outside the keypad and - // moved into it; ignore them. - const {activeNodeId, pressAndHoldIntervalId} = - this.touchState[touchId]; - - this._cleanupTouchEvent(touchId); - - const didPressAndHold = !!pressAndHoldIntervalId; - if (didPressAndHold) { - // We don't trigger a touch end if there was a press and hold, - // because the key has been triggered at least once and calling - // the onTouchEnd handler would add an extra trigger. - this.handlers.onBlur(); - } else { - // Trigger a touch-end. There's no need to notify clients of a - // blur as clients are responsible for handling any cleanup in - // their touch-end handlers. - this.handlers.onTouchEnd(activeNodeId); - } - } - } - - /** - * Handle a touch-cancel event. - */ - onTouchCancel(touchId) { - // If a touch is cancelled and we're swiping, end the swipe with no - // displacement. - if (this.swipeState) { - if (this.swipeState.touchId === touchId) { - this.handlers.onSwipeEnd?.(0); - this.swipeState = null; - } - } else if (this.touchState[touchId]) { - // Otherwise, trigger a full blur. We don't want to trigger a - // touch-up, since the cancellation means that the user probably - // didn't release over a key intentionally. - this._cleanupTouchEvent(touchId); - this.handlers.onBlur(); - } - } -} - -export default GestureStateMachine; diff --git a/packages/math-input/src/components/keypad-legacy/icon.tsx b/packages/math-input/src/components/keypad-legacy/icon.tsx deleted file mode 100644 index 14e2619632..0000000000 --- a/packages/math-input/src/components/keypad-legacy/icon.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/** - * A component that renders an icon for a symbol with the given name. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import {IconType} from "../../enums"; -import {offBlack} from "../common-style"; - -import MathIcon from "./math-icon"; -import SvgIcon from "./svg-icon"; -import TextIcon from "./text-icon"; - -import type {IconConfig} from "../../types"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const focusedColor = "#FFF"; -const unfocusedColor = offBlack; - -type Props = { - focused: boolean; - icon: IconConfig; - style?: StyleType; -}; - -class Icon extends React.PureComponent { - render() { - const {focused, icon, style} = this.props; - - const styleWithFocus: StyleType = [ - focused ? styles.focused : styles.unfocused, - ...(Array.isArray(style) ? style : [style]), - ]; - - switch (icon.type) { - case IconType.MATH: - return ; - - case IconType.SVG: - // TODO(charlie): Support passing style objects to `SvgIcon`. - // This will require migrating the individual icons to use - // `currentColor` and accept a `className` prop, rather than - // relying on an explicit color prop. - return ( - - ); - - case IconType.TEXT: - return ( - - ); - default: - throw new Error("No icon or symbol provided"); - } - } -} - -const styles = StyleSheet.create({ - unfocused: { - color: unfocusedColor, - }, - - focused: { - color: focusedColor, - }, -}); - -export default Icon; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/arrow.js b/packages/math-input/src/components/keypad-legacy/iconography/arrow.js deleted file mode 100644 index 47e4a85a12..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/arrow.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * An arrow icon, used by the other navigational keys. - */ -import * as React from "react"; - -const Arrow = (props) => { - return ( - - - - - - ); -}; - -export default Arrow; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/backspace.js b/packages/math-input/src/components/keypad-legacy/iconography/backspace.js deleted file mode 100644 index eac4de6641..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/backspace.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the BACKSPACE iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const Backspace = () => { - return ( - - - - - - - - ); -}; - -export default Backspace; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/cdot.js b/packages/math-input/src/components/keypad-legacy/iconography/cdot.js deleted file mode 100644 index 28a10c05d4..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/cdot.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the CDOT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Cdot extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - - - ); - } -} - -export default Cdot; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/cos.js b/packages/math-input/src/components/keypad-legacy/iconography/cos.js deleted file mode 100644 index 6fbbbffa2b..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/cos.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * An autogenerated component that renders the COS iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Cos extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Cos; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/cube-root.js b/packages/math-input/src/components/keypad-legacy/iconography/cube-root.js deleted file mode 100644 index 7f314001d7..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/cube-root.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * An autogenerated component that renders the CUBE_ROOT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class CubeRoot extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default CubeRoot; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/dismiss.js b/packages/math-input/src/components/keypad-legacy/iconography/dismiss.js deleted file mode 100644 index bc3fd34f8c..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/dismiss.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * An autogenerated component that renders the DISMISS iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const Dismiss = () => { - return ( - - - - - - - ); -}; - -export default Dismiss; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/divide.js b/packages/math-input/src/components/keypad-legacy/iconography/divide.js deleted file mode 100644 index e8b337c378..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/divide.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * An autogenerated component that renders the DIVIDE iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Divide extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - - ); - } -} - -export default Divide; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/down.js b/packages/math-input/src/components/keypad-legacy/iconography/down.js deleted file mode 100644 index f14ca7b597..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/down.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * A component that renders the DOWN iconograpy in SVG. - */ -import * as React from "react"; - -import Arrow from "./arrow"; - -const Down = () => { - return ( - - - - ); -}; - -export default Down; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/equal.js b/packages/math-input/src/components/keypad-legacy/iconography/equal.js deleted file mode 100644 index 702961147c..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/equal.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the EQUAL iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Equal extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Equal; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/exp-2.js b/packages/math-input/src/components/keypad-legacy/iconography/exp-2.js deleted file mode 100644 index d94a45f0bf..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/exp-2.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the EXP_2 iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Exp2 extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Exp2; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/exp-3.js b/packages/math-input/src/components/keypad-legacy/iconography/exp-3.js deleted file mode 100644 index d1db8c3d68..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/exp-3.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the EXP_3 iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Exp3 extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Exp3; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/exp.js b/packages/math-input/src/components/keypad-legacy/iconography/exp.js deleted file mode 100644 index 16e4a0c5ef..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/exp.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the EXP iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Exp extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Exp; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/frac.js b/packages/math-input/src/components/keypad-legacy/iconography/frac.js deleted file mode 100644 index 6be8ece157..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/frac.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * An autogenerated component that renders the FRAC iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class FracInclusive extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - - - - - ); - } -} - -export default FracInclusive; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/geq.js b/packages/math-input/src/components/keypad-legacy/iconography/geq.js deleted file mode 100644 index 103811e946..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/geq.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the GEQ iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Geq extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Geq; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/gt.js b/packages/math-input/src/components/keypad-legacy/iconography/gt.js deleted file mode 100644 index 193397927c..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/gt.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the GT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Gt extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Gt; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/index.js b/packages/math-input/src/components/keypad-legacy/iconography/index.js deleted file mode 100644 index 86505311e8..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/index.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * A directory of autogenerated icon components. - */ - -export {default as COS} from "./cos"; -export {default as LOG} from "./log"; -export {default as EQUAL} from "./equal"; -export {default as BACKSPACE} from "./backspace"; -export {default as SQRT} from "./sqrt"; -export {default as EXP} from "./exp"; -export {default as NEQ} from "./neq"; -export {default as GEQ} from "./geq"; -export {default as LN} from "./ln"; -export {default as DISMISS} from "./dismiss"; -export {default as SIN} from "./sin"; -export {default as LT} from "./lt"; -export {default as CUBE_ROOT} from "./cube-root"; -export {default as PLUS} from "./plus"; -export {default as TAN} from "./tan"; -export {default as LEFT} from "./left"; -export {default as UP} from "./up"; -export {default as DOWN} from "./down"; -export {default as LEFT_PAREN} from "./left-paren"; -export {default as RIGHT_PAREN} from "./right-paren"; -export {default as GT} from "./gt"; -export {default as DIVIDE} from "./divide"; -export {default as PERIOD} from "./period"; -export {default as PERCENT} from "./percent"; -export {default as TIMES} from "./times"; -export {default as EXP_3} from "./exp-3"; -export {default as EXP_2} from "./exp-2"; -export {default as RIGHT} from "./right"; -export {default as CDOT} from "./cdot"; -export {default as LOG_N} from "./log-n"; -export {default as LEQ} from "./leq"; -export {default as MINUS} from "./minus"; -export {default as NEGATIVE} from "./minus"; -export {default as RADICAL} from "./radical"; -export {default as FRAC} from "./frac"; -export {default as JUMP_OUT_PARENTHESES} from "./jump-out-parentheses"; -export {default as JUMP_OUT_EXPONENT} from "./jump-out-exponent"; -export {default as JUMP_OUT_BASE} from "./jump-out-base"; -export {default as JUMP_INTO_NUMERATOR} from "./jump-into-numerator"; -export {default as JUMP_OUT_NUMERATOR} from "./jump-out-numerator"; -export {default as JUMP_OUT_DENOMINATOR} from "./jump-out-denominator"; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-into-numerator.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-into-numerator.js deleted file mode 100644 index a8a63336f7..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-into-numerator.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_INTO_NUMERATOR iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpIntoNumerator = () => { - return ( - - - - - - - - - - - ); -}; - -export default JumpIntoNumerator; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-base.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-out-base.js deleted file mode 100644 index f12d506b1d..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-base.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_OUT_BASE iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpOutBase = () => { - return ( - - - - - - - - - ); -}; - -export default JumpOutBase; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-denominator.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-out-denominator.js deleted file mode 100644 index 9bedcac0ef..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-denominator.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_OUT_DENOMINATOR iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpOutDenominator = () => { - return ( - - - - - - - - - - - ); -}; - -export default JumpOutDenominator; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-exponent.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-out-exponent.js deleted file mode 100644 index f557575a32..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-exponent.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_OUT_EXPONENT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpOutExponent = () => { - return ( - - - - - - - - - ); -}; - -export default JumpOutExponent; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-numerator.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-out-numerator.js deleted file mode 100644 index 413dfba0a1..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-numerator.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_OUT_NUMERATOR iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpOutNumerator = () => { - return ( - - - - - - - - - - - ); -}; - -export default JumpOutNumerator; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-parentheses.js b/packages/math-input/src/components/keypad-legacy/iconography/jump-out-parentheses.js deleted file mode 100644 index a3c8468d09..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/jump-out-parentheses.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the JUMP_OUT_PARENTHESES iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import * as React from "react"; - -const JumpOutParentheses = () => { - return ( - - - - - - - - - ); -}; - -export default JumpOutParentheses; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/left-paren.js b/packages/math-input/src/components/keypad-legacy/iconography/left-paren.js deleted file mode 100644 index 75cac2e257..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/left-paren.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the LEFT_PAREN iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class LeftParen extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default LeftParen; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/left.js b/packages/math-input/src/components/keypad-legacy/iconography/left.js deleted file mode 100644 index bfc979559d..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/left.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * An component that renders the LEFT iconograpy in SVG. - */ -import * as React from "react"; - -import Arrow from "./arrow"; - -const Left = () => { - return ( - - - - ); -}; - -export default Left; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/leq.js b/packages/math-input/src/components/keypad-legacy/iconography/leq.js deleted file mode 100644 index deb8b8653e..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/leq.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the LEQ iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Leq extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Leq; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/ln.js b/packages/math-input/src/components/keypad-legacy/iconography/ln.js deleted file mode 100644 index f43a7db2f1..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/ln.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the LN iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Ln extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Ln; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/log-n.js b/packages/math-input/src/components/keypad-legacy/iconography/log-n.js deleted file mode 100644 index c5950d6c75..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/log-n.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the LOG_N iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class LogN extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default LogN; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/log.js b/packages/math-input/src/components/keypad-legacy/iconography/log.js deleted file mode 100644 index 00906a947b..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/log.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * An autogenerated component that renders the LOG iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Log extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Log; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/lt.js b/packages/math-input/src/components/keypad-legacy/iconography/lt.js deleted file mode 100644 index 438d9c881a..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/lt.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the LT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Lt extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Lt; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/minus.js b/packages/math-input/src/components/keypad-legacy/iconography/minus.js deleted file mode 100644 index 9d094f4994..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/minus.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * An autogenerated component that renders the MINUS iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Minus extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Minus; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/neq.js b/packages/math-input/src/components/keypad-legacy/iconography/neq.js deleted file mode 100644 index 1ab8e81013..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/neq.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the NEQ iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Neq extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Neq; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/parens.js b/packages/math-input/src/components/keypad-legacy/iconography/parens.js deleted file mode 100644 index 7b982166c0..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/parens.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the PARENS iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Parens extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Parens; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/percent.js b/packages/math-input/src/components/keypad-legacy/iconography/percent.js deleted file mode 100644 index b80022143d..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/percent.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * An autogenerated component that renders the PERCENT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Percent extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - - - - - ); - } -} - -export default Percent; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/period.js b/packages/math-input/src/components/keypad-legacy/iconography/period.js deleted file mode 100644 index e9ae328ef9..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/period.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * An autogenerated component that renders the PERIOD iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Period extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Period; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/plus.js b/packages/math-input/src/components/keypad-legacy/iconography/plus.js deleted file mode 100644 index eaa98af764..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/plus.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * An autogenerated component that renders the PLUS iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Plus extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Plus; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/radical.js b/packages/math-input/src/components/keypad-legacy/iconography/radical.js deleted file mode 100644 index ea020296f8..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/radical.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * An autogenerated component that renders the RADICAL iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Radical extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Radical; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/right-paren.js b/packages/math-input/src/components/keypad-legacy/iconography/right-paren.js deleted file mode 100644 index f2440486ef..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/right-paren.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the RIGHT_PAREN iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class RightParen extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default RightParen; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/right.js b/packages/math-input/src/components/keypad-legacy/iconography/right.js deleted file mode 100644 index 9697a1b488..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/right.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * A component that renders the RIGHT iconograpy in SVG. - */ -import * as React from "react"; - -import Arrow from "./arrow"; - -const Right = () => { - return ( - - - - ); -}; - -export default Right; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/sin.js b/packages/math-input/src/components/keypad-legacy/iconography/sin.js deleted file mode 100644 index ecb30ca2e3..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/sin.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * An autogenerated component that renders the SIN iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Sin extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Sin; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/sqrt.js b/packages/math-input/src/components/keypad-legacy/iconography/sqrt.js deleted file mode 100644 index 985c755030..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/sqrt.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * An autogenerated component that renders the SQRT iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Sqrt extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - ); - } -} - -export default Sqrt; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/tan.js b/packages/math-input/src/components/keypad-legacy/iconography/tan.js deleted file mode 100644 index 61c8190934..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/tan.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * An autogenerated component that renders the TAN iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Tan extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Tan; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/times.js b/packages/math-input/src/components/keypad-legacy/iconography/times.js deleted file mode 100644 index 2b4a403280..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/times.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * An autogenerated component that renders the TIMES iconograpy in SVG. - * - * Generated with: https://gist.github.com/crm416/3c7abc88e520eaed72347af240b32590. - */ -import PropTypes from "prop-types"; -import * as React from "react"; - -class Times extends React.Component { - static propTypes = { - color: PropTypes.string.isRequired, - }; - - render() { - return ( - - - - - - - - ); - } -} - -export default Times; diff --git a/packages/math-input/src/components/keypad-legacy/iconography/up.js b/packages/math-input/src/components/keypad-legacy/iconography/up.js deleted file mode 100644 index 20d79876ee..0000000000 --- a/packages/math-input/src/components/keypad-legacy/iconography/up.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * A component that renders the UP iconograpy in SVG. - */ -import * as React from "react"; - -import Arrow from "./arrow"; - -const Up = () => { - return ( - - - - ); -}; - -export default Up; diff --git a/packages/math-input/src/components/keypad-legacy/index.ts b/packages/math-input/src/components/keypad-legacy/index.ts deleted file mode 100644 index 50c772e159..0000000000 --- a/packages/math-input/src/components/keypad-legacy/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default} from "./provided-keypad"; diff --git a/packages/math-input/src/components/keypad-legacy/keypad-button.tsx b/packages/math-input/src/components/keypad-legacy/keypad-button.tsx deleted file mode 100644 index 3e56c53ae5..0000000000 --- a/packages/math-input/src/components/keypad-legacy/keypad-button.tsx +++ /dev/null @@ -1,368 +0,0 @@ -/** - * A component that renders a keypad button. - */ - -import Color from "@khanacademy/wonder-blocks-color"; -import {StyleSheet, css} from "aphrodite"; -import * as React from "react"; -import {connect} from "react-redux"; - -import {BorderDirection, BorderStyles, KeyTypes} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import { - wonderBlocksBlue, - innerBorderColor, - innerBorderStyle, - innerBorderWidthPx, - valueGrey, - operatorGrey, - controlGrey, - emptyGrey, -} from "../common-style"; - -import CornerDecal from "./corner-decal"; -import Icon from "./icon"; -import MultiSymbolGrid from "./multi-symbol-grid"; - -import type {KeyType} from "../../enums"; -import type {Border, NonManyKeyConfig, IconConfig} from "../../types"; -import type {State} from "./store/types"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -interface ReduxProps { - heightPx: number; - widthPx: number; -} - -interface Props extends ReduxProps { - ariaLabel?: string; - borders: Border; - childKeys: ReadonlyArray; - disabled: boolean; - focused: boolean; - popoverEnabled: boolean; - type: KeyType; - icon?: IconConfig; - style?: StyleType; - onTouchCancel?: (evt: React.TouchEvent) => void; - onTouchEnd?: (evt: React.TouchEvent) => void; - onTouchMove?: (evt: React.TouchEvent) => void; - onTouchStart?: (evt: React.TouchEvent) => void; - // NOTE(matthewc)[LC-754] this is a normal React thing, but TS - // gets mad if I don't explicitly set it as a prop - ref?: (any) => void; -} - -// eslint-disable-next-line react/no-unsafe -class KeypadButton extends React.PureComponent { - buttonSizeStyle: StyleType | undefined; - - static defaultProps = { - borders: BorderStyles.ALL, - childKeys: [], - disabled: false, - focused: false, - popoverEnabled: false, - }; - - UNSAFE_componentWillMount() { - this.buttonSizeStyle = styleForButtonDimensions( - this.props.heightPx, - this.props.widthPx, - ); - } - - componentDidMount() { - this._preInjectStyles(); - } - - UNSAFE_componentWillUpdate(newProps, newState) { - // Only recompute the Aphrodite StyleSheet when the button height has - // changed. Though it is safe to recompute the StyleSheet (since - // they're content-addressable), it saves us a bunch of hashing and - // other work to cache it here. - if ( - newProps.heightPx !== this.props.heightPx || - newProps.widthPx !== this.props.widthPx - ) { - this.buttonSizeStyle = styleForButtonDimensions( - newProps.heightPx, - newProps.widthPx, - ); - - this._preInjectStyles(); - } - } - - _preInjectStyles = () => { - // HACK(charlie): Pre-inject all of the possible styles for the button. - // This avoids a flickering effect in the echo animation whereby the - // echoes vary in size as they animate. Note that we need to account for - // the "initial" styles that `View` will include, as these styles are - // applied to `View` components and Aphrodite will consolidate the style - // object. This method must be called whenever a property that - // influences the possible outcomes of `this._getFocusStyle` and - // `this._getButtonStyle` changes (such as `this.buttonSizeStyle`). - for (const type of KeyTypes) { - css(View.styles.initial, ...this._getFocusStyle(type)); - - for (const borders of Object.values(BorderStyles)) { - css( - View.styles.initial, - ...this._getButtonStyle(type, borders), - ); - } - } - }; - - _getFocusStyle = (type: KeyType) => { - let focusBackgroundStyle; - if (type === "INPUT_NAVIGATION" || type === "KEYPAD_NAVIGATION") { - focusBackgroundStyle = styles.light; - } else { - focusBackgroundStyle = styles.bright; - } - - return [styles.focusBox, focusBackgroundStyle]; - }; - - _getButtonStyle = (type, borders, style?) => { - // Select the appropriate style for the button. - let backgroundStyle; - switch (type) { - case "EMPTY": - backgroundStyle = styles.empty; - break; - - case "MANY": - case "VALUE": - backgroundStyle = styles.value; - break; - - case "OPERATOR": - backgroundStyle = styles.operator; - break; - - case "INPUT_NAVIGATION": - case "KEYPAD_NAVIGATION": - backgroundStyle = styles.control; - break; - - case "ECHO": - backgroundStyle = null; - break; - } - - const borderStyle = []; - if (borders.includes(BorderDirection.LEFT)) { - // @ts-expect-error TS2345 - borderStyle.push(styles.leftBorder); - } - if (borders.includes(BorderDirection.BOTTOM)) { - // @ts-expect-error TS2345 - borderStyle.push(styles.bottomBorder); - } - - return [ - styles.buttonBase, - backgroundStyle, - ...borderStyle, - type === "ECHO" && styles.echo, - this.buttonSizeStyle, - // React Native allows you to set the 'style' props on user defined - // components. - // See: https://facebook.github.io/react-native/docs/style.html - ...(Array.isArray(style) ? style : [style]), - ]; - }; - - render() { - const { - ariaLabel, - borders, - childKeys, - disabled, - focused, - icon, - onTouchCancel, - onTouchEnd, - onTouchMove, - onTouchStart, - popoverEnabled, - style, - type, - } = this.props; - - // We render in the focus state if the key is focused, or if it's an - // echo. - const renderFocused = - (!disabled && focused) || popoverEnabled || type === "ECHO"; - const buttonStyle = this._getButtonStyle(type, borders, style); - const focusStyle = this._getFocusStyle(type); - const iconWrapperStyle = [ - styles.iconWrapper, - disabled ? styles.disabled : undefined, - ]; - - const eventHandlers = { - onTouchCancel, - onTouchEnd, - onTouchMove, - onTouchStart, - }; - - const maybeFocusBox = renderFocused && ; - const maybeCornerDecal = !renderFocused && - !disabled && - childKeys && - childKeys.length > 0 && ; - - if (type === "EMPTY") { - return ; - } else if (type === "MANY") { - // TODO(charlie): Make the long-press interaction accessible. See - // the TODO in key-configs.js for more. - const manyButtonA11yMarkup = { - role: "button", - ariaLabel: childKeys[0].ariaLabel, - }; - const icons = childKeys.map((keyConfig) => { - return keyConfig.icon; - }) as ReadonlyArray; - return ( - - {maybeFocusBox} - - - - {maybeCornerDecal} - - ); - } else { - const a11yMarkup = { - role: "button", - ariaLabel: ariaLabel, - }; - - return ( - - {maybeFocusBox} - - - - {maybeCornerDecal} - - ); - } - } -} - -const focusInsetPx = 4; -const focusBoxZIndex = 0; - -const styles = StyleSheet.create({ - buttonBase: { - flex: 1, - cursor: "pointer", - // Make the text unselectable - userSelect: "none", - justifyContent: "center", - alignItems: "center", - // Borders are made selectively visible. - borderColor: innerBorderColor, - borderStyle: innerBorderStyle, - boxSizing: "border-box", - }, - - decalInset: { - top: focusInsetPx, - right: focusInsetPx, - }, - - // Overrides for the echo state, where we want to render the borders for - // layout purposes, but we don't want them to be visible. - echo: { - borderColor: "transparent", - }, - - // Background colors and other base styles that may vary between key types. - value: { - backgroundColor: valueGrey, - }, - operator: { - backgroundColor: operatorGrey, - }, - control: { - backgroundColor: controlGrey, - }, - empty: { - backgroundColor: emptyGrey, - cursor: "default", - }, - - bright: { - backgroundColor: wonderBlocksBlue, - }, - light: { - backgroundColor: Color.offBlack32, - }, - - iconWrapper: { - zIndex: focusBoxZIndex + 1, - }, - - focusBox: { - position: "absolute", - zIndex: focusBoxZIndex, - left: focusInsetPx, - right: focusInsetPx, - bottom: focusInsetPx, - top: focusInsetPx, - borderRadius: 1, - }, - - disabled: { - opacity: 0.3, - }, - - // Styles used to render the appropriate borders. Buttons are only allowed - // to render left and bottom borders, to simplify layout. - leftBorder: { - borderLeftWidth: innerBorderWidthPx, - }, - bottomBorder: { - borderBottomWidth: innerBorderWidthPx, - }, -}); - -const styleForButtonDimensions = (heightPx, widthPx) => { - return StyleSheet.create({ - // eslint-disable-next-line react-native/no-unused-styles - buttonSize: { - height: heightPx, - width: widthPx, - maxWidth: widthPx, - }, - }).buttonSize; -}; - -const mapStateToProps = (state: State): ReduxProps => { - return { - heightPx: state.layout.buttonDimensions.height, - widthPx: state.layout.buttonDimensions.width, - }; -}; - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - KeypadButton, -); diff --git a/packages/math-input/src/components/keypad-legacy/keypad-container.tsx b/packages/math-input/src/components/keypad-legacy/keypad-container.tsx deleted file mode 100644 index 09960f64fe..0000000000 --- a/packages/math-input/src/components/keypad-legacy/keypad-container.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import {StyleSheet} from "aphrodite"; -import * as React from "react"; -import {connect} from "react-redux"; - -import {LayoutMode, KeypadType} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import { - innerBorderColor, - innerBorderStyle, - innerBorderWidthPx, - compactKeypadBorderRadiusPx, -} from "../common-style"; - -import ExpressionKeypad from "./expression-keypad"; -import FractionKeypad from "./fraction-keypad"; -import NavigationPad from "./navigation-pad"; -import {setPageSize} from "./store/actions"; -import Styles from "./styles"; -import * as zIndexes from "./z-indexes"; - -import type {State as ReduxState} from "./store/types"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const {row, centered, fullWidth} = Styles; - -interface ReduxProps { - active?: boolean; - extraKeys?: ReadonlyArray; - keypadType?: KeypadType; - layoutMode?: LayoutMode; - navigationPadEnabled?: boolean; -} - -interface Props extends ReduxProps { - onDismiss?: () => void; - onElementMounted: (element: any) => void; - onPageSizeChange?: ( - pageWidth: number, - pageHeight: number, - containerWidth: number, - containerHeight: number, - ) => void; - style?: StyleType; -} - -type State = { - hasBeenActivated: boolean; - viewportWidth: string | number; -}; - -// eslint-disable-next-line react/no-unsafe -class KeypadContainer extends React.Component { - _containerRef = React.createRef(); - _containerResizeObserver: ResizeObserver | null = null; - _resizeTimeout: number | null | undefined; - hasMounted: boolean | undefined; - - state = { - hasBeenActivated: false, - viewportWidth: "100vw", - }; - - UNSAFE_componentWillMount() { - if (this.props.active) { - this.setState({ - hasBeenActivated: this.props.active, - }); - } - } - - componentDidMount() { - // Relay the initial size metrics. - this._onResize(); - - // And update it on resize. - window.addEventListener("resize", this._throttleResizeHandler); - window.addEventListener( - "orientationchange", - this._throttleResizeHandler, - ); - - // LC-1213: some common older browsers (as of 2023-09-07) - // don't support ResizeObserver - if ("ResizeObserver" in window) { - this._containerResizeObserver = new window.ResizeObserver( - this._throttleResizeHandler, - ); - - if (this._containerRef.current) { - this._containerResizeObserver.observe( - this._containerRef.current, - ); - } - } - } - - UNSAFE_componentWillReceiveProps(nextProps) { - if (!this.state.hasBeenActivated && nextProps.active) { - this.setState({ - hasBeenActivated: true, - }); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.active && !this.props.active) { - this.props.onDismiss && this.props.onDismiss(); - } - } - - componentWillUnmount() { - window.removeEventListener("resize", this._throttleResizeHandler); - window.removeEventListener( - "orientationchange", - this._throttleResizeHandler, - ); - this._containerResizeObserver?.disconnect(); - } - - _throttleResizeHandler = () => { - // Throttle the resize callbacks. - // https://developer.mozilla.org/en-US/docs/Web/Events/resize - if (this._resizeTimeout == null) { - this._resizeTimeout = window.setTimeout(() => { - this._resizeTimeout = null; - - this._onResize(); - }, 66); - } - }; - - _onResize = () => { - // Whenever the page resizes, we need to force an update, as the button - // heights and keypad width are computed based on horizontal space. - this.setState({ - viewportWidth: window.innerWidth, - }); - const containerWidth = this._containerRef.current?.clientWidth || 0; - const containerHeight = this._containerRef.current?.clientHeight || 0; - this.props.onPageSizeChange?.( - window.innerWidth, - window.innerHeight, - containerWidth, - containerHeight, - ); - }; - - renderKeypad = () => { - const {extraKeys, keypadType, layoutMode, navigationPadEnabled} = - this.props; - - const keypadProps = { - extraKeys, - // HACK(charlie): In order to properly round the corners of the - // compact keypad, we need to instruct some of our child views to - // crop themselves. At least we're colocating all the layout - // information in this component, though. - roundTopLeft: - layoutMode === LayoutMode.COMPACT && !navigationPadEnabled, - roundTopRight: layoutMode === LayoutMode.COMPACT, - }; - - // Select the appropriate keyboard given the type. - // TODO(charlie): In the future, we might want to move towards a - // data-driven approach to defining keyboard layouts, and have a - // generic keyboard that takes some "keyboard data" and renders it. - // However, the keyboards differ pretty heavily right now and it's not - // clear what that format would look like exactly. Plus, there aren't - // very many of them. So to keep us moving, we'll just hardcode. - switch (keypadType) { - case KeypadType.FRACTION: - return ; - - case KeypadType.EXPRESSION: - return ; - - default: - throw new Error("Invalid keypad type: " + keypadType); - } - }; - - render() { - const { - active, - layoutMode, - navigationPadEnabled, - onElementMounted, - style, - } = this.props; - const {hasBeenActivated} = this.state; - - // NOTE(charlie): We render the transforms as pure inline styles to - // avoid an Aphrodite bug in mobile Safari. - // See: https://github.com/Khan/aphrodite/issues/68. - let dynamicStyle = { - ...(active ? inlineStyles.active : inlineStyles.hidden), - }; - - if (!active && !hasBeenActivated) { - dynamicStyle = { - ...dynamicStyle, - ...inlineStyles.invisible, - }; - } - - const keypadContainerStyle = [ - row, - centered, - fullWidth, - styles.keypadContainer, - ...(Array.isArray(style) ? style : [style]), - ]; - - const keypadStyle = [ - row, - styles.keypadBorder, - layoutMode === LayoutMode.FULLSCREEN - ? styles.fullscreen - : styles.compact, - ]; - - // TODO(charlie): When the keypad is shorter than the width of the - // screen, add a border on its left and right edges, and round out the - // corners. - return ( - - { - if (!this.hasMounted && element) { - this.hasMounted = true; - onElementMounted(element); - } - }} - > - {navigationPadEnabled && ( - - )} - - {this.renderKeypad()} - - - - ); - } -} - -const keypadAnimationDurationMs = 300; -const borderWidthPx = 1; - -const styles = StyleSheet.create({ - keypadContainer: { - bottom: 0, - left: 0, - right: 0, - position: "fixed", - transition: `${keypadAnimationDurationMs}ms ease-out`, - transitionProperty: "transform", - zIndex: zIndexes.keypad, - }, - - keypadBorder: { - boxShadow: "0 1px 4px 0 rgba(0, 0, 0, 0.1)", - borderColor: "rgba(0, 0, 0, 0.2)", - borderStyle: "solid", - }, - - fullscreen: { - borderTopWidth: borderWidthPx, - }, - - compact: { - borderTopRightRadius: compactKeypadBorderRadiusPx, - borderTopLeftRadius: compactKeypadBorderRadiusPx, - - borderTopWidth: borderWidthPx, - borderRightWidth: borderWidthPx, - borderLeftWidth: borderWidthPx, - }, - - navigationPadContainer: { - // Add a separator between the navigation pad and the keypad. - borderRight: - `${innerBorderWidthPx}px ${innerBorderStyle} ` + - `${innerBorderColor}`, - boxSizing: "content-box", - }, - - // Defer to the navigation pad, such that the navigation pad is always - // rendered at full-width, and the keypad takes up just the remaining space. - // TODO(charlie): Avoid shrinking the keys and, instead, make the keypad - // scrollable. - keypadLayout: { - flexGrow: 1, - // Avoid unitless flex-basis, per: https://philipwalton.com/articles/normalizing-cross-browser-flexbox-bugs/ - flexBasis: "0%", - }, -}); - -// Note: these don't go through an autoprefixer/aphrodite. -const inlineStyles = { - // If the keypad is yet to have ever been activated, we keep it invisible - // so as to avoid, e.g., the keypad flashing at the bottom of the page - // during the initial render. - invisible: { - visibility: "hidden", - }, - - hidden: { - transform: "translate3d(0, 100%, 0)", - }, - - active: { - transform: "translate3d(0, 0, 0)", - }, -}; - -const mapStateToProps = (state: ReduxState): ReduxProps => { - return { - extraKeys: state.keypad.extraKeys, - keypadType: state.keypad.keypadType, - active: state.keypad.active, - layoutMode: state.layout.layoutMode, - navigationPadEnabled: state.layout.navigationPadEnabled, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - onPageSizeChange: ( - pageWidth: number, - pageHeight: number, - containerWidth: number, - containerHeight: number, - ) => { - dispatch( - setPageSize( - pageWidth, - pageHeight, - containerWidth, - containerHeight, - ), - ); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, -})(KeypadContainer); diff --git a/packages/math-input/src/components/keypad-legacy/keypad.tsx b/packages/math-input/src/components/keypad-legacy/keypad.tsx deleted file mode 100644 index 5b4adbbd7a..0000000000 --- a/packages/math-input/src/components/keypad-legacy/keypad.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/** - * A keypad component that acts as a container for rows or columns of buttons, - * and manages the rendering of echo animations on top of those buttons. - */ - -import * as React from "react"; -import ReactDOM from "react-dom"; -import {connect} from "react-redux"; - -import {View} from "../../fake-react-native-web/index"; - -import EchoManager from "./echo-manager"; -import PopoverManager from "./popover-manager"; -import {removeEcho} from "./store/actions"; - -import type {Bound, Popover, Echo} from "../../types"; -import type {State} from "./store/types"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -interface ReduxProps { - active: boolean; - echoes: ReadonlyArray; - popover: Popover | null; -} - -interface Props extends ReduxProps { - children: React.ReactNode; - style?: StyleType; - removeEcho?: (animationId: string) => void; -} - -// eslint-disable-next-line react/no-unsafe -class Keypad extends React.Component { - _isMounted: boolean | undefined; - _resizeTimeout: number | null | undefined; - _container: Bound | null | undefined; - - componentDidMount() { - this._isMounted = true; - - window.addEventListener("resize", this._onResize); - this._updateSizeAndPosition(); - } - - UNSAFE_componentWillReceiveProps(newProps) { - if (!this._container && (newProps.popover || newProps.echoes.length)) { - this._computeContainer(); - } - } - - componentWillUnmount() { - this._isMounted = false; - - window.removeEventListener("resize", this._onResize); - } - - _computeContainer = () => { - const domNode = ReactDOM.findDOMNode(this) as Element; - this._container = domNode.getBoundingClientRect(); - }; - - _updateSizeAndPosition = () => { - // Mark the container for recalculation next time the keypad is - // opened. - // TODO(charlie): Since we're not recalculating the container - // immediately, if you were to resize the page while a popover were - // active, you'd likely get unexpected behavior. This seems very - // difficult to do and, as such, incredibly unlikely, but we may - // want to reconsider the caching here. - this._container = null; - }; - - _onResize = () => { - // Whenever the page resizes, we need to recompute the container's - // bounding box. This is the only time that the bounding box can change. - - // Throttle resize events -- taken from: - // https://developer.mozilla.org/en-US/docs/Web/Events/resize - if (this._resizeTimeout == null) { - this._resizeTimeout = window.setTimeout(() => { - this._resizeTimeout = null; - - if (this._isMounted) { - this._updateSizeAndPosition(); - } - }, 66); - } - }; - - render() { - const {children, echoes, removeEcho, popover, style} = this.props; - - // Translate the echo boxes, as they'll be positioned absolutely to - // this relative container. - const relativeEchoes = echoes.map((echo) => { - const {initialBounds, ...rest} = echo; - return { - ...rest, - initialBounds: { - // @ts-expect-error TS2533 - top: initialBounds.top - this._container.top, - // @ts-expect-error TS2533 - right: initialBounds.right - this._container.left, - // @ts-expect-error TS2533 - bottom: initialBounds.bottom - this._container.top, - // @ts-expect-error TS2533 - left: initialBounds.left - this._container.left, - width: initialBounds.width, - height: initialBounds.height, - }, - }; - }); - - // Translate the popover bounds from page-absolute to keypad-relative. - // Note that we only need three bounds, since popovers are anchored to - // the bottom left corners of the keys over which they appear. - const relativePopover = popover && { - ...popover, - bounds: { - bottom: - // @ts-expect-error TS2533 - this._container.height - - // @ts-expect-error TS2533 - (popover.bounds.bottom - this._container.top), - // @ts-expect-error TS2533 - left: popover.bounds.left - this._container.left, - width: popover.bounds.width, - }, - }; - - return ( - - {children} - - - - ); - } -} - -const mapStateToProps = (state: State): ReduxProps => { - return { - echoes: state.echoes.echoes, - active: state.keypad.active, - popover: state.gestures.popover, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - removeEcho: (animationId) => { - dispatch(removeEcho(animationId)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, -})(Keypad); diff --git a/packages/math-input/src/components/keypad-legacy/many-keypad-button.tsx b/packages/math-input/src/components/keypad-legacy/many-keypad-button.tsx deleted file mode 100644 index b34e562414..0000000000 --- a/packages/math-input/src/components/keypad-legacy/many-keypad-button.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * A keypad button that displays an arbitrary number of symbols, with no - * 'default' symbol. - */ - -import * as React from "react"; - -import KeyConfigs from "../../data/key-configs"; -import {IconType} from "../../enums"; - -import EmptyKeypadButton from "./empty-keypad-button"; -import TouchableKeypadButton from "./touchable-keypad-button"; - -import type {KeyConfig} from "../../types"; - -type Props = { - keys: ReadonlyArray; -}; - -class ManyKeypadButton extends React.Component { - static defaultProps = { - keys: [], - }; - - render() { - const {keys, ...rest} = this.props; - - // If we have no extra symbols, render an empty button. If we have just - // one, render a standard button. Otherwise, capture them all in a - // single button. - if (keys.length === 0) { - return ; - } else if (keys.length === 1) { - const keyConfig = KeyConfigs[keys[0]]; - return ; - } else { - const keyConfig: KeyConfig = { - id: "MANY", - type: "MANY", - childKeyIds: keys, - ariaLabel: keys - .map((key) => KeyConfigs[key].ariaLabel) - .join(", "), - icon: { - type: IconType.SVG, - data: "many", - }, - }; - return ; - } - } -} - -export default ManyKeypadButton; diff --git a/packages/math-input/src/components/keypad-legacy/math-icon.tsx b/packages/math-input/src/components/keypad-legacy/math-icon.tsx deleted file mode 100644 index dde59941b4..0000000000 --- a/packages/math-input/src/components/keypad-legacy/math-icon.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * A component that renders an icon with math (via KaTeX). - */ - -import {StyleSheet} from "aphrodite"; -import katex from "katex"; -import * as React from "react"; -import ReactDOM from "react-dom"; - -import {View} from "../../fake-react-native-web/index"; -import {iconSizeHeightPx, iconSizeWidthPx} from "../common-style"; - -import Styles from "./styles"; - -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const {row, centered} = Styles; - -type Props = { - math: string; - style: StyleType; -}; - -class MathIcon extends React.Component { - componentDidMount() { - this._renderMath(); - } - - componentDidUpdate(prevProps) { - if (prevProps.math !== this.props.math) { - this._renderMath(); - } - } - - _renderMath = () => { - const {math} = this.props; - katex.render(math, ReactDOM.findDOMNode(this)); - }; - - render() { - const {style} = this.props; - - const containerStyle = [ - row, - centered, - styles.size, - styles.base, - ...(Array.isArray(style) ? style : [style]), - ]; - - return ; - } -} - -const styles = StyleSheet.create({ - size: { - height: iconSizeHeightPx, - width: iconSizeWidthPx, - }, - - base: { - fontSize: 25, - }, -}); - -export default MathIcon; diff --git a/packages/math-input/src/components/keypad-legacy/multi-symbol-grid.tsx b/packages/math-input/src/components/keypad-legacy/multi-symbol-grid.tsx deleted file mode 100644 index 7832fc296f..0000000000 --- a/packages/math-input/src/components/keypad-legacy/multi-symbol-grid.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * A grid of symbols, rendered as text and positioned based on the number of - * symbols provided. Up to four symbols will be shown. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import {IconType} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import {iconSizeHeightPx, iconSizeWidthPx} from "../common-style"; - -import Icon from "./icon"; -import Styles from "./styles"; - -import type {IconConfig} from "../../types"; - -const {row, column, centered, fullWidth} = Styles; - -type Props = { - focused: boolean; - icons: ReadonlyArray; -}; - -class MultiSymbolGrid extends React.Component { - render() { - const {focused, icons} = this.props; - - // Validate that we only received math-based icons. Right now, this - // component only supports math icons (and it should only be passed - // variables and Greek letters, which are always rendered as math). - // Supporting other types of icons is possible but would require - // some styles coercion and doesn't seem worthwhile right now. - icons.forEach((icon) => { - if (icon.type !== IconType.MATH) { - throw new Error( - `Received invalid icon: type=${icon.type}, ` + - `data=${icon.data}`, - ); - } - }); - - if (icons.length === 1) { - return ; - } else { - const primaryIconStyle = styles.base; - const secondaryIconStyle = [styles.base, styles.secondary]; - - if (icons.length === 2) { - return ( - - - - - - - - - ); - } else if (icons.length >= 3) { - return ( - - - - - - - - - - - - - - - {icons[3] && ( - - )} - - - - ); - } - } - - throw new Error(`Invalid number of icons: ${icons.length}`); - } -} - -const verticalInsetPx = 2; -const horizontalInsetPx = 4; - -const styles = StyleSheet.create({ - size: { - height: iconSizeHeightPx, - width: iconSizeWidthPx, - }, - - // For the three- and four-icon layouts. - bottomLeft: { - marginBottom: verticalInsetPx, - marginLeft: horizontalInsetPx, - }, - topLeft: { - marginTop: verticalInsetPx, - marginLeft: horizontalInsetPx, - }, - topRight: { - marginTop: verticalInsetPx, - marginRight: horizontalInsetPx, - }, - bottomRight: { - marginBottom: verticalInsetPx, - marginRight: horizontalInsetPx, - }, - - // For the two-icon layout. - middleLeft: { - marginLeft: horizontalInsetPx, - }, - middleRight: { - marginRight: horizontalInsetPx, - }, - - base: { - fontSize: 18, - }, - - secondary: { - opacity: 0.3, - }, -}); - -export default MultiSymbolGrid; diff --git a/packages/math-input/src/components/keypad-legacy/multi-symbol-popover.tsx b/packages/math-input/src/components/keypad-legacy/multi-symbol-popover.tsx deleted file mode 100644 index 1dfbcbd7ae..0000000000 --- a/packages/math-input/src/components/keypad-legacy/multi-symbol-popover.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * A popover that renders a set of keys floating above the page. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import {BorderStyles} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; - -import TouchableKeypadButton from "./touchable-keypad-button"; -import * as zIndexes from "./z-indexes"; - -import type {KeyConfig} from "../../types"; - -type Prop = { - keys: ReadonlyArray; -}; - -class MultiSymbolPopover extends React.Component { - render() { - const {keys} = this.props; - - // TODO(charlie): We have to require this lazily because of a cyclic - // dependence in our components. - return ( - - {keys.map((key) => { - return ( - - ); - })} - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flexDirection: "column-reverse", - position: "relative", - width: "100%", - borderRadius: 2, - boxShadow: "0 2px 6px rgba(0, 0, 0, 0.3)", - zIndex: zIndexes.popover, - }, - - // eslint-disable-next-line react-native/no-unused-styles - popoverButton: { - backgroundColor: "#FFF", - borderWidth: 0, - }, -}); - -export default MultiSymbolPopover; diff --git a/packages/math-input/src/components/keypad-legacy/navigation-pad.tsx b/packages/math-input/src/components/keypad-legacy/navigation-pad.tsx deleted file mode 100644 index 1e586787c0..0000000000 --- a/packages/math-input/src/components/keypad-legacy/navigation-pad.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * A component that renders a navigation pad, which consists of an arrow for - * each possible direction. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import KeyConfigs from "../../data/key-configs"; -import {BorderStyles} from "../../enums"; -import {View} from "../../fake-react-native-web/index"; -import { - navigationPadWidthPx, - controlGrey, - valueGrey, - offBlack16, -} from "../common-style"; - -import Styles from "./styles"; -import TouchableKeypadButton from "./touchable-keypad-button"; - -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const {row, column, centered, stretch, roundedTopLeft} = Styles; - -type Props = { - roundTopLeft: boolean; - style: StyleType; -}; - -class NavigationPad extends React.Component { - render() { - // TODO(charlie): Disable the navigational arrows depending on the - // cursor context. - const {roundTopLeft, style} = this.props; - - const containerStyle = [ - column, - centered, - styles.container, - roundTopLeft && roundedTopLeft, - ...(Array.isArray(style) ? style : [style]), - ]; - - return ( - - - - - - - - - - - - - - ); - } -} - -const buttonSizePx = 48; -const borderRadiusPx = 4; -const borderWidthPx = 1; - -const styles = StyleSheet.create({ - container: { - backgroundColor: controlGrey, - width: navigationPadWidthPx, - }, - - navigationKey: { - borderColor: offBlack16, - backgroundColor: valueGrey, - width: buttonSizePx, - height: buttonSizePx, - - // Override the default box-sizing so that our buttons are - // `buttonSizePx` exclusive of their borders. - boxSizing: "content-box", - }, - - topArrow: { - borderTopWidth: borderWidthPx, - borderLeftWidth: borderWidthPx, - borderRightWidth: borderWidthPx, - borderTopLeftRadius: borderRadiusPx, - borderTopRightRadius: borderRadiusPx, - }, - - rightArrow: { - borderTopWidth: borderWidthPx, - borderRightWidth: borderWidthPx, - borderBottomWidth: borderWidthPx, - borderTopRightRadius: borderRadiusPx, - borderBottomRightRadius: borderRadiusPx, - }, - - bottomArrow: { - borderBottomWidth: borderWidthPx, - borderLeftWidth: borderWidthPx, - borderRightWidth: borderWidthPx, - borderBottomLeftRadius: borderRadiusPx, - borderBottomRightRadius: borderRadiusPx, - }, - - leftArrow: { - borderTopWidth: borderWidthPx, - borderBottomWidth: borderWidthPx, - borderLeftWidth: borderWidthPx, - borderTopLeftRadius: borderRadiusPx, - borderBottomLeftRadius: borderRadiusPx, - }, - - horizontalSpacer: { - background: valueGrey, - // No need to set a height -- the spacer will be stretched by its - // parent. - width: buttonSizePx, - }, -}); - -export default NavigationPad; diff --git a/packages/math-input/src/components/keypad-legacy/node-manager.ts b/packages/math-input/src/components/keypad-legacy/node-manager.ts deleted file mode 100644 index f5bbb72189..0000000000 --- a/packages/math-input/src/components/keypad-legacy/node-manager.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type {LayoutProps, Bound} from "../../types"; -/** - * A manager for our node-to-ID system. In particular, this class is - * responsible for maintaing a mapping between DOM nodes and node IDs, and - * translating touch events from the raw positions at which they occur to the - * nodes over which they are occurring. This differs from browser behavior, in - * which touch events are only sent to the node in which a touch started. - */ - -class NodeManager { - _nodesById: Record; - _orderedIds: ReadonlyArray; - _cachedBoundingBoxesById: Record; - - constructor() { - // A mapping from IDs to DOM nodes. - this._nodesById = {}; - - // An ordered list of IDs, where DOM nodes that are "higher" on the - // page come earlier in the list. Note that an ID may be present in - // this ordered list but not be registered to a DOM node (i.e., if it - // is registered as a child of another DOM node, but hasn't appeared in - // the DOM yet). - this._orderedIds = []; - - // Cache bounding boxes aggressively, re-computing on page resize. Our - // caching here makes the strict assumption that if a node is reasonably - // assumed to be on-screen, its bounds won't change. For example, if we - // see that a touch occurred within the bounds of a node, we cache those - // bounds. - // TODO(charlie): It'd be great if we could pre-compute these when the - // page is idle and the keypad is visible (i.e., the nodes are in their - // proper positions). - this._cachedBoundingBoxesById = {}; - window.addEventListener("resize", () => { - this._cachedBoundingBoxesById = {}; - }); - } - - /** - * Register a DOM node with a given identifier. - * - * @param {string} id - the identifier of the given node - * @param {node} domNode - the DOM node linked to the identifier - * @param {object} borders - an opaque object describing the node's borders - */ - registerDOMNode( - id: string, - domNode: HTMLElement, - childIds: ReadonlyArray, - ) { - this._nodesById[id] = domNode; - - // Make sure that any children appear first. - // TODO(charlie): This is a very simplistic system that wouldn't - // properly handle multiple levels of nesting. - const allIds = [...(childIds || []), id, ...this._orderedIds]; - - // De-dupe the list of IDs. - const orderedIds = []; - const seenIds = {}; - for (const id of allIds) { - if (!seenIds[id]) { - // @ts-expect-error TS2345 - orderedIds.push(id); - seenIds[id] = true; - } - } - - this._orderedIds = orderedIds; - } - - /** - * Unregister the DOM node with the given identifier. - * - * @param {string} id - the identifier of the node to unregister - */ - unregisterDOMNode(id: string) { - delete this._nodesById[id]; - } - - /** - * Return the identifier of the topmost node located at the given - * coordinates. - * - * @param {number} x - the x coordinate at which to search for a node - * @param {number} y - the y coordinate at which to search for a node - * @returns {null|string} - null or the identifier of the topmost node at - * the given coordinates - */ - idForCoords(x: number, y: number): string | void { - for (const id of this._orderedIds) { - const domNode = this._nodesById[id]; - if (domNode) { - const bounds = domNode.getBoundingClientRect(); - if ( - bounds.left <= x && - bounds.right > x && - bounds.top <= y && - bounds.bottom > y - ) { - this._cachedBoundingBoxesById[id] = bounds; - return id; - } - } - } - } - - /** - * Return the necessary layout information, including the bounds and border - * values, for the node with the given identifier. - * - * @param {string} id - the identifier of the node for which to return the - * layout information - * @returns {object} - the bounding client rect for the given node, along - * with its borders - */ - layoutPropsForId(id: string): LayoutProps { - if (!this._cachedBoundingBoxesById[id]) { - const node = this._nodesById[id]; - - this._cachedBoundingBoxesById[id] = node - ? node.getBoundingClientRect() - : new DOMRect(); - } - - return { - initialBounds: this._cachedBoundingBoxesById[id], - }; - } -} - -export default NodeManager; diff --git a/packages/math-input/src/components/keypad-legacy/popover-manager.tsx b/packages/math-input/src/components/keypad-legacy/popover-manager.tsx deleted file mode 100644 index 9b6d62fce9..0000000000 --- a/packages/math-input/src/components/keypad-legacy/popover-manager.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * A component that renders and animates the popovers that appear over the - * multi-functional keys. - */ - -import * as React from "react"; -import {CSSTransition} from "react-transition-group"; - -import KeyConfigs from "../../data/key-configs"; - -import MultiSymbolPopover from "./multi-symbol-popover"; - -import type {Popover, KeyConfig} from "../../types"; - -// NOTE(charlie): These must be kept in sync with the transition durations and -// classnames specified in popover.less. -const animationTransitionName = "popover"; -const animationDurationMs = 200; - -type Props = { - // TODO(matthewc) should be something like Bound, but couldn't fix errors - bounds: any; - childKeys: ReadonlyArray; -}; - -// A container component used to position a popover absolutely at a specific -// position. -class PopoverContainer extends React.Component { - render() { - const {bounds, childKeys} = this.props; - - const containerStyle = { - position: "absolute", - ...bounds, - }; - - return ( -
- -
- ); - } -} - -type PopoverManagerProps = { - popover: Popover | null; -}; - -class PopoverManager extends React.Component { - render() { - const {popover} = this.props; - - return popover ? ( - - KeyConfigs[id])} - /> - - ) : null; - } -} - -export default PopoverManager; diff --git a/packages/math-input/src/components/keypad-legacy/popover-state-machine.ts b/packages/math-input/src/components/keypad-legacy/popover-state-machine.ts deleted file mode 100644 index 85ef3e6333..0000000000 --- a/packages/math-input/src/components/keypad-legacy/popover-state-machine.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type {ActiveNodesObj} from "../../types"; - -/** - * A state machine for the popover state. In particular, this class manages the - * mapping of parent nodes to their children, and translates touch events that - * traverse various nodes to actions that are conditioned on whether a popover - * is present. - */ - -type Handlers = { - onActiveNodesChanged: (activeNodes: ActiveNodesObj) => void; - onClick: (keyId: string, domNodeId: string, inPopover: boolean) => void; -}; - -class PopoverStateMachine { - handlers: Handlers; - popovers: Record>; - activePopover: string | null; - - constructor(handlers) { - this.handlers = handlers; - - this.activePopover = null; - this.popovers = {}; - } - - /** - * Register a popover container as containing a set of children. - * - * @param {string} id - the identifier of the popover container - * @param {string[]} childIds - the identifiers of the nodes contained in - * the popover container - */ - registerPopover(id, childIds) { - this.popovers[id] = childIds; - } - - /** - * Unregister a popover container. - * - * @param {string} id - the identifier of the popover container to - * unregister - */ - unregisterPopover(id) { - delete this.popovers[id]; - } - - /** - * @returns {boolean} - whether a popover is active and visible - */ - isPopoverVisible() { - return this.activePopover != null; - } - - /** - * Blur the active nodes. - */ - onBlur() { - this.activePopover = null; - this.handlers.onActiveNodesChanged({ - popover: null, - focus: null, - }); - } - - /** - * Handle a focus event on the node with the given identifier. - * - * @param {string} id - the identifier of the node that was focused - */ - onFocus(id) { - if (this.activePopover) { - // If we have a popover that is currently active, we focus this - // node if it's in the popover, and remove any highlight otherwise. - if (this._isNodeInsidePopover(this.activePopover, id)) { - this.handlers.onActiveNodesChanged({ - popover: { - parentId: this.activePopover, - childIds: this.popovers[this.activePopover], - }, - focus: id, - }); - } else { - this.handlers.onActiveNodesChanged({ - popover: { - parentId: this.activePopover, - childIds: this.popovers[this.activePopover], - }, - focus: null, - }); - } - } else { - this.activePopover = null; - this.handlers.onActiveNodesChanged({ - popover: null, - focus: id, - }); - } - } - - /** - * Handle a long press event on the node with the given identifier. - * - * @param {string} id - the identifier of the node that was long-pressed - */ - onLongPress(id) { - // We only care about long presses if they occur on a popover, and we - // don't already have a popover active. - if (!this.activePopover && this.popovers[id]) { - // NOTE(charlie): There's an assumption here that focusing the - // first child is the correct behavior for a newly focused popover. - // This relies on the fact that the children are rendered - // bottom-up. If that rendering changes, this logic will need to - // change as well. - this.activePopover = id; - this.handlers.onActiveNodesChanged({ - popover: { - parentId: id, - childIds: this.popovers[id], - }, - focus: this._defaultNodeForPopover(this.activePopover), - }); - } - } - - /** - * Handle the trigger (click or hold) of the node with the given identifier. - * - * @param {string} id - the identifier of the node that was triggered - */ - onTrigger(id) { - this.handlers.onClick(id, id, false); - } - - /** - * Handle a touch-end event on the node with the given identifier. - * - * @param {string} id - the identifier of the node over which the touch - * ended - */ - onTouchEnd(id) { - const inPopover = !!this.activePopover; - if (inPopover) { - // If we have a popover that is currently active, we trigger a - // click on this node if and only if it's in the popover, with the - // exception that, if the node passed back _is_ the active popover, - // then we trigger its default node. This latter case should only - // be triggered if the user were to tap down on a popover-enabled - // node, hold for long enough for the popover to appear, and then - // release without ever moving their finger, in which case, the - // underlying gesture system would have no idea that the popover's - // first child node was now focused. - if (this._isNodeInsidePopover(this.activePopover, id)) { - this.handlers.onClick(id, id, inPopover); - } else if (this.activePopover === id) { - const keyId = this._defaultNodeForPopover(id); - this.handlers.onClick(keyId, keyId, inPopover); - } - } else if (this.popovers[id]) { - // Otherwise, if the node is itself a popover revealer, trigger the - // clicking of its default node, but pass back the popover node ID - // for layout purposes. - const keyId = this._defaultNodeForPopover(id); - const domNodeId = id; - this.handlers.onClick(keyId, domNodeId, inPopover); - } else if (id != null) { - // Finally, if we have no active popover, and we touched up over a - // valid key, trigger a click. - this.onTrigger(id); - } - - this.onBlur(); - } - - _isNodeInsidePopover(popover, id) { - return this.popovers[popover].includes(id); - } - - _defaultNodeForPopover(popover) { - return this.popovers[popover][0]; - } -} - -export default PopoverStateMachine; diff --git a/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx b/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx deleted file mode 100644 index 0d28758222..0000000000 --- a/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import * as React from "react"; -import ReactDOM from "react-dom"; -import {Provider} from "react-redux"; - -import KeypadContainer from "./keypad-container"; -import { - activateKeypad, - dismissKeypad, - configureKeypad, - setCursor, - setKeyHandler, -} from "./store/actions"; -import {createStore} from "./store/index"; - -import type { - Cursor, - KeypadConfiguration, - KeyHandler, - KeypadAPI, -} from "../../types"; -import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -type Props = { - setKeypadActive: (keypadActive: boolean) => void; - keypadActive: boolean; - onElementMounted?: (arg1: any) => void; - onDismiss?: () => void; - style?: StyleType; - - onAnalyticsEvent: AnalyticsEventHandlerFn; -}; - -class ProvidedKeypad extends React.Component implements KeypadAPI { - store: any; - - constructor(props) { - super(props); - this.store = createStore(); - } - - componentDidUpdate(prevProps) { - if (this.props.keypadActive && !prevProps.keypadActive) { - this.store.dispatch(activateKeypad()); - } - - if (!this.props.keypadActive && prevProps.keypadActive) { - this.store.dispatch(dismissKeypad()); - } - } - - activate: () => void = () => { - this.props.setKeypadActive(true); - }; - - dismiss: () => void = () => { - this.props.setKeypadActive(false); - }; - - configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( - configuration, - cb, - ) => { - this.store.dispatch(configureKeypad(configuration)); - - // HACK(charlie): In Perseus, triggering a focus causes the keypad to - // animate into view and re-configure. We'd like to provide the option - // to re-render the re-configured keypad before animating it into view, - // to avoid jank in the animation. As such, we support passing a - // callback into `configureKeypad`. However, implementing this properly - // would require middleware, etc., so we just hack it on with - // `setTimeout` for now. - setTimeout(() => cb && cb()); - }; - - setCursor: (cursor: Cursor) => void = (cursor) => { - this.store.dispatch(setCursor(cursor)); - }; - - setKeyHandler: (keyHandler: KeyHandler) => void = (keyHandler) => { - this.store.dispatch(setKeyHandler(keyHandler)); - }; - - getDOMNode: () => ReturnType = () => { - return ReactDOM.findDOMNode(this); - }; - - onElementMounted: (element: any) => void = (element) => { - this.props.onAnalyticsEvent({ - type: "math-input:keypad-opened", - payload: { - virtualKeypadVersion: "MATH_INPUT_KEYPAD_V1", - }, - }); - - // Append the dispatch methods that we want to expose - // externally to the returned React element. - const elementWithDispatchMethods = { - ...element, - activate: this.activate, - dismiss: this.dismiss, - configure: this.configure, - setCursor: this.setCursor, - setKeyHandler: this.setKeyHandler, - getDOMNode: this.getDOMNode, - } as const; - this.props.onElementMounted?.(elementWithDispatchMethods); - }; - - onDismiss: () => void = () => { - this.props.onAnalyticsEvent({ - type: "math-input:keypad-closed", - payload: { - virtualKeypadVersion: "MATH_INPUT_KEYPAD_V1", - }, - }); - - this.props.onDismiss?.(); - }; - - render(): React.ReactNode { - const {style} = this.props; - - return ( - - - - ); - } -} - -export default ProvidedKeypad; diff --git a/packages/math-input/src/components/keypad-legacy/store/actions.ts b/packages/math-input/src/components/keypad-legacy/store/actions.ts deleted file mode 100644 index f8988824c0..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/actions.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type Key from "../../../data/keys"; -import type { - Bound, - KeypadConfiguration, - KeyHandler, - Cursor, - ActiveNodesObj, -} from "../../../types"; - -// naming convention: verb + noun -// the noun should be one of the other properties in the object that's -// being dispatched - -type DismissKeypadAction = { - type: "DismissKeypad"; -}; - -export const dismissKeypad = (): DismissKeypadAction => { - return { - type: "DismissKeypad", - }; -}; - -type ActivateKeypadAction = { - type: "ActivateKeypad"; -}; - -export const activateKeypad = (): ActivateKeypadAction => { - return { - type: "ActivateKeypad", - }; -}; - -/** - * Configure the keypad with the provided configuration parameters. - */ -type ConfigureKeypadAction = { - type: "ConfigureKeypad"; - configuration: KeypadConfiguration; -}; - -export const configureKeypad = ( - configuration: KeypadConfiguration, -): ConfigureKeypadAction => { - return { - type: "ConfigureKeypad", - configuration, - }; -}; - -type SetPageSizeAction = { - type: "SetPageSize"; - pageWidth: number; - pageHeight: number; - containerWidth: number; - containerHeight: number; -}; - -export const setPageSize = ( - pageWidth: number, - pageHeight: number, - containerWidth: number, - containerHeight: number, -): SetPageSizeAction => { - return { - type: "SetPageSize", - pageWidth, - pageHeight, - containerWidth, - containerHeight, - }; -}; - -type RemoveEchoAction = { - type: "RemoveEcho"; - animationId: string; -}; - -export const removeEcho = (animationId: string): RemoveEchoAction => { - return { - type: "RemoveEcho", - animationId, - }; -}; - -// Input-related actions. -type SetKeyHandlerAction = { - type: "SetKeyHandler"; - keyHandler: KeyHandler; -}; - -export const setKeyHandler = (keyHandler: KeyHandler): SetKeyHandlerAction => { - return { - type: "SetKeyHandler", - keyHandler, - }; -}; - -type SetCursorAction = { - type: "SetCursor"; - cursor: Cursor; -}; - -export const setCursor = (cursor: Cursor): SetCursorAction => { - return { - type: "SetCursor", - cursor, - }; -}; - -// Gesture actions -type SetActiveNodesAction = { - type: "SetActiveNodes"; - activeNodes: any; -}; - -export const setActiveNodes = ( - activeNodes: ActiveNodesObj, -): SetActiveNodesAction => { - return { - type: "SetActiveNodes", - activeNodes, - }; -}; - -type PressKeyAction = { - type: "PressKey"; - key: Key; - initialBounds: Bound; - inPopover: boolean; -}; - -export const pressKey = ( - key: Key, - initialBounds: Bound, - inPopover: any, -): PressKeyAction => { - return { - type: "PressKey", - key, - initialBounds, - inPopover, - }; -}; - -export type Action = - | DismissKeypadAction - | ActivateKeypadAction - | ConfigureKeypadAction - | SetPageSizeAction - | RemoveEchoAction - | SetKeyHandlerAction - | SetCursorAction - | SetActiveNodesAction - | PressKeyAction; diff --git a/packages/math-input/src/components/keypad-legacy/store/echo-reducer.ts b/packages/math-input/src/components/keypad-legacy/store/echo-reducer.ts deleted file mode 100644 index 56102628bf..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/echo-reducer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import KeyConfigs from "../../../data/key-configs"; -import {EchoAnimationType} from "../../../enums"; - -import type {Action} from "./actions"; -import type {EchoState} from "./types"; - -// Used to generate unique animation IDs for the echo animations. The actual -// values are irrelevant as long as they are unique. -let _lastAnimationId = 0; - -const initialEchoState = { - echoes: [], -} as const; - -const echoReducer = function ( - state: EchoState = initialEchoState, - action: Action, -): EchoState { - switch (action.type) { - case "PressKey": - const keyConfig = KeyConfigs[action.key]; - - // Add in the echo animation if the user performs a math - // operation. - if (keyConfig.type === "VALUE" || keyConfig.type === "OPERATOR") { - return { - ...state, - echoes: [ - ...state.echoes, - { - animationId: "" + _lastAnimationId++, - animationType: action.inPopover - ? EchoAnimationType.LONG_FADE_ONLY - : EchoAnimationType.FADE_ONLY, - id: keyConfig.id, - initialBounds: action.initialBounds, - }, - ], - }; - } - return state; - - case "RemoveEcho": - const remainingEchoes = state.echoes.filter((echo) => { - return echo.animationId !== action.animationId; - }); - return { - ...state, - echoes: remainingEchoes, - }; - - default: - return state; - } -}; - -export default echoReducer; diff --git a/packages/math-input/src/components/keypad-legacy/store/index.ts b/packages/math-input/src/components/keypad-legacy/store/index.ts deleted file mode 100644 index f6ff80bd1f..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as Redux from "redux"; - -import GestureManager from "../gesture-manager"; - -import {setActiveNodes, pressKey} from "./actions"; -import echoReducer from "./echo-reducer"; -import inputReducer from "./input-reducer"; -import keypadReducer from "./keypad-reducer"; -import layoutReducer from "./layout-reducer"; -import {defaultKeypadType, keypadForType} from "./shared"; - -import type Key from "../../../data/keys"; -import type {LayoutProps, ActiveNodesObj} from "../../../types"; -import type {Action} from "./actions"; -import type {GestureState} from "./types"; - -export const createStore = () => { - // TODO(matthewc)[LC-752]: gestureReducer can't be moved from this file - // because it depends on `store` being in scope (see note below) - const createGestureManager = (swipeEnabled: boolean) => { - return new GestureManager( - { - swipeEnabled, - }, - { - onActiveNodesChanged: (activeNodes: ActiveNodesObj) => { - store.dispatch(setActiveNodes(activeNodes)); - }, - onClick: ( - key: Key, - layoutProps: LayoutProps, - inPopover: boolean, - ) => { - store.dispatch( - pressKey(key, layoutProps.initialBounds, inPopover), - ); - }, - }, - [], - ["BACKSPACE", "UP", "RIGHT", "DOWN", "LEFT"], - ); - }; - - const initialGestureState = { - popover: null, - focus: null, - gestureManager: createGestureManager( - keypadForType[defaultKeypadType].numPages > 1, - ), - } as const; - - const gestureReducer = function ( - state: GestureState = initialGestureState, - action: Action, - ): GestureState { - switch (action.type) { - case "DismissKeypad": - // NOTE(charlie): In the past, we enforced the "gesture manager - // will not receive any events when the keypad is hidden" - // assumption by assuming that the keypad would be hidden when - // dismissed and, as such, that none of its managed DOM nodes - // would be able to receive touch events. However, on mobile - // Safari, we're seeing that some of the keys receive touch - // events even when off-screen, inexplicably. So, to guard - // against that bug and make the contract explicit, we enable - // and disable event tracking on activation and dismissal. - state.gestureManager.disableEventTracking(); - return state; - - case "ActivateKeypad": - state.gestureManager.enableEventTracking(); - return state; - - case "SetActiveNodes": - return { - ...state, - ...action.activeNodes, - }; - - case "ConfigureKeypad": - const {keypadType} = action.configuration; - const {numPages} = keypadForType[keypadType]; - const swipeEnabled = numPages > 1; - return { - popover: null, - focus: null, - gestureManager: createGestureManager(swipeEnabled), - }; - - default: - return state; - } - }; - - const reducer = Redux.combineReducers({ - input: inputReducer, - keypad: keypadReducer, - gestures: gestureReducer, - echoes: echoReducer, - layout: layoutReducer, - }); - - // TODO(charlie): This non-inlined return is necessary so as to allow the - // gesture manager to dispatch actions on the store in its callbacks. We - // should come up with a better pattern to remove the two-way dependency. - // eslint-disable-next-line import/no-deprecated - const store = Redux.createStore(reducer); - - return store; -}; diff --git a/packages/math-input/src/components/keypad-legacy/store/input-reducer.ts b/packages/math-input/src/components/keypad-legacy/store/input-reducer.ts deleted file mode 100644 index 6204665271..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/input-reducer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import KeyConfigs from "../../../data/key-configs"; -import {CursorContext} from "../../input/cursor-contexts"; - -import type {Cursor, KeyHandler} from "../../../types"; -import type {Action} from "./actions"; -import type {InputState} from "./types"; - -const initialInputState: { - keyHandler: KeyHandler | null; - cursor: Cursor; -} = { - keyHandler: null, - cursor: { - context: CursorContext.NONE, - }, -}; - -const inputReducer = function ( - state: InputState = initialInputState, - action: Action, -): InputState { - switch (action.type) { - case "SetKeyHandler": - return { - ...state, - keyHandler: action.keyHandler, - }; - - case "PressKey": - const keyConfig = KeyConfigs[action.key]; - if (keyConfig.type !== "KEYPAD_NAVIGATION") { - // This is probably an anti-pattern but it works for the - // case where we don't actually control the state but we - // still want to communicate with the other object - return { - ...state, - cursor: state.keyHandler?.(keyConfig.id), - }; - } - - // TODO(kevinb) get state from MathQuill and store it? - return state; - - case "SetCursor": - return { - ...state, - cursor: action.cursor, - }; - - default: - return state; - } -}; - -export default inputReducer; diff --git a/packages/math-input/src/components/keypad-legacy/store/keypad-reducer.ts b/packages/math-input/src/components/keypad-legacy/store/keypad-reducer.ts deleted file mode 100644 index bc3a1e5e2d..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/keypad-reducer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import KeyConfigs from "../../../data/key-configs"; - -import {defaultKeypadType} from "./shared"; - -import type {Action} from "./actions"; -import type {KeypadState} from "./types"; - -const initialKeypadState = { - extraKeys: ["x", "y", "THETA", "PI"] as const, - keypadType: defaultKeypadType, - active: false, -} as const; - -const keypadReducer = function ( - state: KeypadState = initialKeypadState, - action: Action, -): KeypadState { - switch (action.type) { - case "DismissKeypad": - return { - ...state, - active: false, - }; - - case "ActivateKeypad": - return { - ...state, - active: true, - }; - - case "ConfigureKeypad": - return { - ...state, - // Default `extraKeys` to the empty array. - extraKeys: [], - ...action.configuration, - }; - - case "PressKey": - const keyConfig = KeyConfigs[action.key]; - // NOTE(charlie): Our keypad system operates by triggering key - // presses with key IDs in a dumb manner, such that the keys - // don't know what they can do--instead, the store is - // responsible for interpreting key presses and triggering the - // right actions when they occur. Hence, we figure off a - // dismissal here rather than dispatching a dismiss action in - // the first place. - if (keyConfig.id === "DISMISS") { - return keypadReducer(state, {type: "DismissKeypad"}); - } - return state; - - default: - return state; - } -}; - -export default keypadReducer; diff --git a/packages/math-input/src/components/keypad-legacy/store/layout-reducer.test.ts b/packages/math-input/src/components/keypad-legacy/store/layout-reducer.test.ts deleted file mode 100644 index 3b288ce758..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/layout-reducer.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import {KeypadType} from "../../../enums"; - -import {configureKeypad, setPageSize} from "./actions"; -import layoutReducer from "./layout-reducer"; - -function initState() { - // This is just simulating the Redux action that initializes state - // @ts-expect-error TS2345 - let state = layoutReducer(undefined, {type: "@redux-init"}); - state = layoutReducer( - state, - configureKeypad({keypadType: KeypadType.EXPRESSION}), - ); - return state; -} - -describe("layout reducer", () => { - it("enables pagination on small screens portrait (iPhone)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(375, 812, 375, 812)); - - // Assert - expect(state.paginationEnabled).toBe(true); - }); - - it("does not enable navigation pad on small screens portrait (iPhone)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(375, 812, 375, 812)); - - // Assert - expect(state.navigationPadEnabled).toBe(false); - }); - - it("does not enable pagination on medium screens portrait (iPad Mini)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(768, 1024, 768, 1024)); - - // Assert - expect(state.paginationEnabled).toBe(false); - }); - - it("does not enable navigation pad on medium screens portrait (iPad Mini)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(768, 1024, 768, 1024)); - - // Assert - expect(state.navigationPadEnabled).toBe(false); - }); - - it("does not enable pagination on large screens portrait (iPad)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(810, 1080, 810, 1080)); - - // Assert - expect(state.paginationEnabled).toBe(false); - }); - - it("does enable navigation pad on large screens portrait (iPad)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(810, 1080, 810, 1080)); - - // Assert - expect(state.navigationPadEnabled).toBe(true); - }); - - it("does not enable pagination on small screens landscape (iPhone)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(812, 375, 812, 375)); - - // Assert - expect(state.paginationEnabled).toBe(false); - }); - - it("does enable navigation pad on small screens landscape (iPhone)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(812, 375, 812, 375)); - - // Assert - expect(state.navigationPadEnabled).toBe(true); - }); - - it("does not enable pagination on medium screens landscape (iPad Mini)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(1024, 768, 1024, 768)); - - // Assert - expect(state.paginationEnabled).toBe(false); - }); - - it("does enable navigation pad on medium screens landscape (iPad Mini)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(1024, 768, 1024, 768)); - - // Assert - expect(state.navigationPadEnabled).toBe(true); - }); - - it("does not enable pagination on large screens landscape (iPad)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(1080, 810, 1080, 810)); - - // Assert - expect(state.paginationEnabled).toBe(false); - }); - - it("does enable navigation pad on large screens landscape (iPad)", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(1080, 810, 1080, 810)); - - // Assert - expect(state.navigationPadEnabled).toBe(true); - }); - - it("does enable pagination in small containers on big screens", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(2000, 2000, 300, 300)); - - // Assert - expect(state.paginationEnabled).toBe(true); - }); - - it("does not enable navigation in small containers on big screens", () => { - // Arrange - let state = initState(); - - // Act - state = layoutReducer(state, setPageSize(2000, 2000, 300, 300)); - - // Assert - expect(state.navigationPadEnabled).toBe(false); - }); -}); diff --git a/packages/math-input/src/components/keypad-legacy/store/layout-reducer.ts b/packages/math-input/src/components/keypad-legacy/store/layout-reducer.ts deleted file mode 100644 index 5a1c47d12f..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/layout-reducer.ts +++ /dev/null @@ -1,129 +0,0 @@ -import {DeviceOrientation, LayoutMode} from "../../../enums"; -import {computeLayoutParameters} from "../compute-layout-parameters"; - -import {defaultKeypadType, keypadForType} from "./shared"; - -import type {Action} from "./actions"; -import type {GridDimensions, LayoutState, WidthHeight} from "./types"; - -const expandedViewThreshold = 682; -const navigationViewThreshold = 800; - -const initialLayoutState: LayoutState = { - gridDimensions: { - numRows: keypadForType[defaultKeypadType].rows, - numColumns: keypadForType[defaultKeypadType].columns, - numMaxVisibleRows: keypadForType[defaultKeypadType].maxVisibleRows, - numPages: keypadForType[defaultKeypadType].numPages, - }, - buttonDimensions: { - width: 48, - height: 48, - }, - pageDimensions: { - width: 0, - height: 0, - }, - containerDimensions: { - width: 0, - height: 0, - }, - layoutMode: LayoutMode.FULLSCREEN, - paginationEnabled: false, - navigationPadEnabled: false, -} as const; - -/** - * Compute the additional layout state based on the provided page and grid - * dimensions. - */ -const layoutParametersForDimensions = ( - pageDimensions: WidthHeight, - containerDimensions: WidthHeight, - gridDimensions: GridDimensions, -) => { - // Determine the device type and orientation. - const deviceOrientation = - containerDimensions.width > containerDimensions.height - ? DeviceOrientation.LANDSCAPE - : DeviceOrientation.PORTRAIT; - - // Using that information, make some decisions (or assumptions) - // about the resulting layout. - const navigationPadEnabled = - containerDimensions.width > navigationViewThreshold; - const paginationEnabled = containerDimensions.width < expandedViewThreshold; - const toolbarEnabled = true; - - return { - ...computeLayoutParameters( - gridDimensions, - pageDimensions, - containerDimensions, - deviceOrientation, - navigationPadEnabled, - paginationEnabled, - toolbarEnabled, - ), - // Pass along some of the layout information, so that other - // components in the heirarchy can adapt appropriately. - navigationPadEnabled, - paginationEnabled, - }; -}; - -const layoutReducer = function ( - state: LayoutState = initialLayoutState, - action: Action, -): LayoutState { - switch (action.type) { - case "ConfigureKeypad": - const {keypadType} = action.configuration; - const gridDimensions = { - numRows: keypadForType[keypadType].rows, - numColumns: keypadForType[keypadType].columns, - numMaxVisibleRows: keypadForType[keypadType].maxVisibleRows, - numPages: keypadForType[keypadType].numPages, - } as const; - - const layoutParams = layoutParametersForDimensions( - state.pageDimensions, - state.containerDimensions, - gridDimensions, - ); - - return { - ...state, - ...layoutParams, - gridDimensions, - }; - - case "SetPageSize": - const {pageWidth, pageHeight, containerWidth, containerHeight} = - action; - const pageDimensions = { - width: pageWidth, - height: pageHeight, - } as const; - const containerDimensions = { - width: containerWidth, - height: containerHeight, - } as const; - - return { - ...state, - ...layoutParametersForDimensions( - pageDimensions, - containerDimensions, - state.gridDimensions, - ), - pageDimensions, - containerDimensions, - }; - - default: - return state; - } -}; - -export default layoutReducer; diff --git a/packages/math-input/src/components/keypad-legacy/store/shared.ts b/packages/math-input/src/components/keypad-legacy/store/shared.ts deleted file mode 100644 index eb00788ff7..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/shared.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {KeypadType} from "../../../enums"; -import {expressionKeypadLayout} from "../expression-keypad"; -import {fractionKeypadLayout} from "../fraction-keypad"; - -const defaultKeypadType = KeypadType.EXPRESSION; - -const keypadForType = { - [KeypadType.FRACTION]: fractionKeypadLayout, - [KeypadType.EXPRESSION]: expressionKeypadLayout, -} as const; - -export {keypadForType, defaultKeypadType}; diff --git a/packages/math-input/src/components/keypad-legacy/store/types.ts b/packages/math-input/src/components/keypad-legacy/store/types.ts deleted file mode 100644 index 0bf3c9c209..0000000000 --- a/packages/math-input/src/components/keypad-legacy/store/types.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type Key from "../../../data/keys"; -import type {LayoutMode, KeypadType} from "../../../enums"; -import type {Cursor, KeyHandler, Popover, Echo} from "../../../types"; -import type GestureManager from "../gesture-manager"; - -// Interaction between keypad and input -export interface InputState { - // This is the callback to tell the input - // that a key was pressed. It returns information - // about where the cursor is - keyHandler: KeyHandler | null; - // Information about where the cursor is, which we use to - // conditionally render buttons to help navigate - // where the cursor should go next - cursor: Cursor | undefined; -} - -// Managing high-level keypad state -export interface KeypadState { - // Additional symbols that get grouped in a - // ManyKeypadButton; for variables and - // special symbols (pi) - extraKeys: ReadonlyArray; - // Keypad variations (Fraction vs Expression) - keypadType: KeypadType; - // Whether or not to show the keypad - active: boolean; -} - -// Handles things like: -// long-press: to open multikey popover -// swipe: for pagination -// press: regular pushing of a button -export interface GestureState { - // The current multikey popover? - popover: Popover | null; - // ?? Maybe which key is currently focused? - focus: Key | null; - // Complex object that interprets touches as actions - gestureManager: GestureManager; -} - -// Manages the animations for pressing keys -export interface EchoState { - // Which echoes are in the process of animating - echoes: ReadonlyArray; -} - -export type GridDimensions = { - numRows: number; - numColumns: number; - numMaxVisibleRows: number; - numPages: number; -}; - -export type WidthHeight = { - width: number; - height: number; -}; - -// Layout (size, where to put buttons, etc) -export interface LayoutState { - gridDimensions: GridDimensions; - buttonDimensions: WidthHeight; - containerDimensions: WidthHeight; - pageDimensions: WidthHeight; - layoutMode: LayoutMode; - paginationEnabled: boolean; - navigationPadEnabled: boolean; -} - -export interface State { - input: InputState; - keypad: KeypadState; - gestures: GestureState; - echoes: EchoState; - layout: LayoutState; -} diff --git a/packages/math-input/src/components/keypad-legacy/styles.ts b/packages/math-input/src/components/keypad-legacy/styles.ts deleted file mode 100644 index 87c14449b0..0000000000 --- a/packages/math-input/src/components/keypad-legacy/styles.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Common styles shared across components. - */ - -import {StyleSheet} from "aphrodite"; - -import {compactKeypadBorderRadiusPx} from "../common-style"; - -export default StyleSheet.create({ - row: { - flexDirection: "row", - }, - column: { - flexDirection: "column", - }, - oneColumn: { - flexGrow: 1, - }, - fullWidth: { - width: "100%", - }, - stretch: { - alignItems: "stretch", - }, - centered: { - justifyContent: "center", - alignItems: "center", - }, - centeredText: { - textAlign: "center", - }, - roundedTopLeft: { - borderTopLeftRadius: compactKeypadBorderRadiusPx, - }, - roundedTopRight: { - borderTopRightRadius: compactKeypadBorderRadiusPx, - }, -}); diff --git a/packages/math-input/src/components/keypad-legacy/svg-icon.tsx b/packages/math-input/src/components/keypad-legacy/svg-icon.tsx deleted file mode 100644 index b3c7903afb..0000000000 --- a/packages/math-input/src/components/keypad-legacy/svg-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * A component that renders a single SVG icon. - */ - -import * as React from "react"; - -import * as Iconography from "./iconography/index"; - -type Props = { - color: string; - name: string; -}; - -class SvgIcon extends React.Component { - render() { - const {color, name} = this.props; - - // eslint-disable-next-line import/namespace - const SvgForName = Iconography[name]; - return ; - } -} - -export default SvgIcon; diff --git a/packages/math-input/src/components/keypad-legacy/text-icon.tsx b/packages/math-input/src/components/keypad-legacy/text-icon.tsx deleted file mode 100644 index c34b63ffb5..0000000000 --- a/packages/math-input/src/components/keypad-legacy/text-icon.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * A component that renders a text-based icon. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; - -import {View, Text} from "../../fake-react-native-web/index"; -import {iconSizeHeightPx, iconSizeWidthPx} from "../common-style"; - -import Styles from "./styles"; - -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const {row, centered} = Styles; - -type Props = { - character: string; - style?: StyleType; -}; - -class TextIcon extends React.Component { - render() { - const {character, style} = this.props; - - const containerStyle = [ - row, - centered, - styles.size, - styles.base, - ...(Array.isArray(style) ? style : [style]), - ]; - return ( - - {character} - - ); - } -} - -const styles = StyleSheet.create({ - size: { - height: iconSizeHeightPx, - width: iconSizeWidthPx, - }, - - base: { - fontFamily: "'Lato', sans-serif", - fontSize: 25, - }, -}); - -export default TextIcon; diff --git a/packages/math-input/src/components/keypad-legacy/touchable-keypad-button.tsx b/packages/math-input/src/components/keypad-legacy/touchable-keypad-button.tsx deleted file mode 100644 index e5eb36a009..0000000000 --- a/packages/math-input/src/components/keypad-legacy/touchable-keypad-button.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/** - * A touchable wrapper around the base KeypadButton component. This button is - * responsible for keeping our button ID system (which will be used to handle - * touch events globally) opaque to the KeypadButton. - */ - -import {StyleSheet} from "aphrodite"; -import * as React from "react"; -import ReactDOM from "react-dom"; -import {connect} from "react-redux"; - -import KeyConfigs from "../../data/key-configs"; - -import KeypadButton from "./keypad-button"; - -import type Key from "../../data/keys"; -import type {KeyType} from "../../enums"; -import type {Border, IconConfig, KeyConfig} from "../../types"; -import type GestureManager from "./gesture-manager"; -import type {State} from "./store/types"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -interface SharedProps { - borders?: Border; - disabled?: boolean; - style?: StyleType; -} - -interface OwnProps extends SharedProps { - keyConfig: KeyConfig; -} - -interface Props extends SharedProps { - childKeyIds?: ReadonlyArray; - gestureManager: GestureManager; - id: Key; - focused: boolean; - popoverEnabled: boolean; - childKeys?: ReadonlyArray; - ariaLabel?: string; - icon?: IconConfig; - type: KeyType; -} - -class TouchableKeypadButton extends React.Component { - shouldComponentUpdate(newProps: Props) { - // We take advantage of a few different properties of our key - // configuration system. Namely, we know that the other props flow - // directly from the ID, and thus don't need to be checked. If a key has - // a custom style, we bail out (this should be rare). - return ( - newProps.id !== this.props.id || - newProps.gestureManager !== this.props.gestureManager || - newProps.focused !== this.props.focused || - newProps.disabled !== this.props.disabled || - newProps.popoverEnabled !== this.props.popoverEnabled || - newProps.type !== this.props.type || - !!newProps.style - ); - } - - componentWillUnmount() { - const {gestureManager, id} = this.props; - gestureManager.unregisterDOMNode(id); - } - - render() { - const { - borders, - childKeyIds, - disabled, - gestureManager, - id, - style, - ...rest - } = this.props; - - // Only bind the relevant event handlers if the key is enabled. - const eventHandlers = disabled - ? { - onTouchStart: (evt) => evt.preventDefault(), - } - : { - onTouchStart: (evt) => gestureManager.onTouchStart(evt, id), - onTouchEnd: (evt) => gestureManager.onTouchEnd(evt), - onTouchMove: (evt) => gestureManager.onTouchMove(evt), - onTouchCancel: (evt) => gestureManager.onTouchCancel(evt), - }; - - const styleWithAddons = [ - ...(Array.isArray(style) ? style : [style]), - styles.preventScrolls, - ]; - - return ( - - gestureManager.registerDOMNode( - id, - ReactDOM.findDOMNode(node), - childKeyIds, - ) - } - borders={borders} - disabled={disabled} - style={styleWithAddons} - {...eventHandlers} - {...rest} - /> - ); - } -} - -const extractProps = (keyConfig: KeyConfig) => { - const {ariaLabel, icon, type} = keyConfig; - return {ariaLabel, icon, type}; -}; - -const mapStateToProps = (state: State, ownProps: OwnProps): Props => { - const {gestures} = state; - - const {keyConfig, ...rest} = ownProps; - const {id, type} = keyConfig; - - const childKeyIds = - "childKeyIds" in keyConfig ? keyConfig.childKeyIds : undefined; - - const childKeys: readonly KeyConfig[] | undefined = childKeyIds - ? childKeyIds.map((id) => KeyConfigs[id]) - : undefined; - - // Override with the default child props, if the key is a multi-symbol key - // (but not a many-symbol key, which operates under different rules). - const useFirstChildProps = - type !== "MANY" && childKeys && childKeys.length > 0; - - return { - ...rest, - childKeyIds: childKeyIds, - gestureManager: gestures.gestureManager, - id: id, - - // Add in some gesture state. - focused: gestures.focus === id, - popoverEnabled: gestures.popover?.parentId === id, - - // Pass down the child keys and any extracted props. - childKeys, - ...extractProps(useFirstChildProps ? childKeys[0] : keyConfig), - }; -}; - -const styles = StyleSheet.create({ - preventScrolls: { - // Touch events that start in the touchable buttons shouldn't be - // allowed to produce page scrolls. - touchAction: "none", - }, -}); - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - TouchableKeypadButton, -); diff --git a/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx b/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx deleted file mode 100644 index 31e843ebd8..0000000000 --- a/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/** - * A keypad with two pages of keys. - */ - -import Color from "@khanacademy/wonder-blocks-color"; -import {StyleSheet} from "aphrodite"; -import * as React from "react"; -import {connect} from "react-redux"; - -import {View} from "../../fake-react-native-web/index"; -import { - innerBorderColor, - innerBorderStyle, - innerBorderWidthPx, - offBlack16, -} from "../common-style"; -import Tabbar from "../tabbar"; - -import Keypad from "./keypad"; -import Styles from "./styles"; - -import type {KeypadPageType} from "../../types"; -import type {State as ReduxState} from "./store/types"; - -const {column, row, fullWidth} = Styles; - -interface ReduxProps { - paginationEnabled: boolean; -} - -interface Props extends ReduxProps { - leftPage: React.ReactNode; - rightPage: React.ReactNode; -} - -type State = { - selectedPage: KeypadPageType; -}; - -class TwoPageKeypad extends React.Component { - state: State = { - selectedPage: "Numbers", - }; - - render() { - const {leftPage, paginationEnabled, rightPage} = this.props; - - const {selectedPage} = this.state; - - if (paginationEnabled) { - return ( - - { - this.setState({selectedPage: selectedItem}); - }} - style={styles.tabbar} - /> - - {selectedPage === "Numbers" && rightPage} - {selectedPage === "Operators" && leftPage} - - - ); - } else { - return ( - - - {leftPage} - - {rightPage} - - - - ); - } - } -} - -const styles = StyleSheet.create({ - keypad: { - // Set the background to light grey, so that when the user drags the - // keypad pages past the edges, there's a grey backdrop. - backgroundColor: offBlack16, - }, - - borderTop: { - borderTop: - `${innerBorderWidthPx}px ${innerBorderStyle} ` + - `${innerBorderColor}`, - }, - borderLeft: { - borderLeft: - `${innerBorderWidthPx}px ${innerBorderStyle} ` + - `${innerBorderColor}`, - boxSizing: "content-box", - }, - tabbar: { - background: Color.offWhite, - borderTop: `1px solid ${Color.offBlack50}`, - borderBottom: `1px solid ${Color.offBlack50}`, - }, -}); - -const mapStateToProps = (state: ReduxState): ReduxProps => { - return { - paginationEnabled: state.layout.paginationEnabled, - }; -}; - -export default connect(mapStateToProps, null, null, {forwardRef: true})( - TwoPageKeypad, -); diff --git a/packages/math-input/src/components/keypad-legacy/z-indexes.ts b/packages/math-input/src/components/keypad-legacy/z-indexes.ts deleted file mode 100644 index 4d9120af00..0000000000 --- a/packages/math-input/src/components/keypad-legacy/z-indexes.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * This file contains all of the z-index values used throughout the math-input - * component and its children. - */ - -export const popover = 1; -export const echo = 2; -export const keypad = 1060; diff --git a/packages/math-input/src/components/keypad-switch.tsx b/packages/math-input/src/components/keypad-switch.tsx deleted file mode 100644 index a1ead10df1..0000000000 --- a/packages/math-input/src/components/keypad-switch.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from "react"; - -import {MobileKeypad} from "./keypad"; -import {KeypadContext} from "./keypad-context"; -import LegacyKeypad from "./keypad-legacy"; - -import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -type Props = { - onElementMounted?: (arg1: any) => void; - onDismiss?: () => void; - style?: StyleType; - - useV2Keypad?: boolean; - onAnalyticsEvent: AnalyticsEventHandlerFn; -}; - -function KeypadSwitch(props: Props) { - const {useV2Keypad = false, ...rest} = props; - - const KeypadComponent = useV2Keypad ? MobileKeypad : LegacyKeypad; - - // Note: Although we pass the "onAnalyticsEvent" to both keypad components, - // only the current one uses it. There's no point in instrumenting the - // legacy keypad given that it's on its way out the door. - return ( - - {({setKeypadActive, keypadActive}) => { - return ( - - ); - }} - - ); -} - -export default KeypadSwitch; diff --git a/packages/math-input/src/components/keypad/__tests__/mobile-keypad.test.tsx b/packages/math-input/src/components/keypad/__tests__/mobile-keypad.test.tsx index f3f3403440..95550935c4 100644 --- a/packages/math-input/src/components/keypad/__tests__/mobile-keypad.test.tsx +++ b/packages/math-input/src/components/keypad/__tests__/mobile-keypad.test.tsx @@ -2,14 +2,14 @@ import {render, screen} from "@testing-library/react"; import * as React from "react"; import "@testing-library/jest-dom"; -import MobileKeypad from "../mobile-keypad"; +import MobileKeypadInternals from "../mobile-keypad-internals"; describe("mobile keypad", () => { it("should render keypad when active", () => { // Arrange // Act const {container} = render( - undefined} setKeypadActive={(keypadActive: boolean) => undefined} keypadActive={true} @@ -24,7 +24,7 @@ describe("mobile keypad", () => { // Arrange // Act const {container} = render( - undefined} setKeypadActive={(keypadActive: boolean) => undefined} keypadActive={false} @@ -38,7 +38,7 @@ describe("mobile keypad", () => { it("should render the keypad when going from keypadActive=false to keypadActive=true", () => { // Arrange const {rerender} = render( - undefined} setKeypadActive={(keypadActive: boolean) => undefined} keypadActive={false} @@ -49,7 +49,7 @@ describe("mobile keypad", () => { // Act rerender( - undefined} setKeypadActive={(keypadActive: boolean) => undefined} keypadActive={true} @@ -66,7 +66,7 @@ describe("mobile keypad", () => { // Act render( - undefined} keypadActive={true} @@ -87,7 +87,7 @@ describe("mobile keypad", () => { // Arrange const {rerender, unmount} = render( - undefined} keypadActive={true} @@ -96,7 +96,7 @@ describe("mobile keypad", () => { // Act rerender( - undefined} keypadActive={false} diff --git a/packages/math-input/src/components/keypad/index.tsx b/packages/math-input/src/components/keypad/index.tsx index 8ba6ba6454..88654778bc 100644 --- a/packages/math-input/src/components/keypad/index.tsx +++ b/packages/math-input/src/components/keypad/index.tsx @@ -1,2 +1,2 @@ export {default} from "./keypad"; -export {default as MobileKeypad} from "./mobile-keypad"; +export {MobileKeypad} from "./mobile-keypad"; diff --git a/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx b/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx new file mode 100644 index 0000000000..139077db3c --- /dev/null +++ b/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx @@ -0,0 +1,240 @@ +import {StyleSheet} from "aphrodite"; +import * as React from "react"; +import ReactDOM from "react-dom"; + +import {View} from "../../fake-react-native-web/index"; +import AphroditeCssTransitionGroup from "../aphrodite-css-transition-group"; + +import Keypad from "./keypad"; +import {expandedViewThreshold} from "./utils"; + +import type Key from "../../data/keys"; +import type { + Cursor, + KeypadConfiguration, + KeyHandler, + KeypadAPI, +} from "../../types"; +import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; +import type {StyleType} from "@khanacademy/wonder-blocks-core"; + +const AnimationDurationInMS = 200; + +type Props = { + onElementMounted?: (arg1: any) => void; + onDismiss?: () => void; + style?: StyleType; + onAnalyticsEvent: AnalyticsEventHandlerFn; + setKeypadActive: (keypadActive: boolean) => void; + keypadActive: boolean; +}; + +type State = { + containerWidth: number; + keypadConfig?: KeypadConfiguration; + keyHandler?: KeyHandler; + cursor?: Cursor; +}; + +/** + * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same + * external API so that it can be hot-swapped with the v1 keypad and + * is responsible for connecting the keypad with MathInput and the Renderer. + * + * Ideally this strategy of attaching methods on the class component for + * other components to call will be replaced props/callbacks since React + * doesn't support this type of code anymore (functional components + * can't have methods attached to them). + */ +class MobileKeypadInternals + extends React.Component + implements KeypadAPI +{ + _containerRef = React.createRef(); + _containerResizeObserver: ResizeObserver | null = null; + _throttleResize = false; + + state: State = { + containerWidth: 0, + }; + + componentDidMount() { + this._resize(); + + window.addEventListener("resize", this._throttleResizeHandler); + window.addEventListener( + "orientationchange", + this._throttleResizeHandler, + ); + + // LC-1213: some common older browsers (as of 2023-09-07) + // don't support ResizeObserver + if ("ResizeObserver" in window) { + this._containerResizeObserver = new window.ResizeObserver( + this._throttleResizeHandler, + ); + + if (this._containerRef.current) { + this._containerResizeObserver.observe( + this._containerRef.current, + ); + } + } + + this.props.onElementMounted?.({ + activate: this.activate, + dismiss: this.dismiss, + configure: this.configure, + setCursor: this.setCursor, + setKeyHandler: this.setKeyHandler, + getDOMNode: this.getDOMNode, + }); + } + + componentWillUnmount() { + window.removeEventListener("resize", this._throttleResizeHandler); + window.removeEventListener( + "orientationchange", + this._throttleResizeHandler, + ); + this._containerResizeObserver?.disconnect(); + } + + _resize = () => { + const containerWidth = this._containerRef.current?.clientWidth || 0; + this.setState({containerWidth}); + }; + + _throttleResizeHandler = () => { + if (this._throttleResize) { + return; + } + + this._throttleResize = true; + + setTimeout(() => { + this._resize(); + this._throttleResize = false; + }, 100); + }; + + activate: () => void = () => { + this.props.setKeypadActive(true); + }; + + dismiss: () => void = () => { + this.props.setKeypadActive(false); + this.props.onDismiss?.(); + }; + + configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( + configuration, + cb, + ) => { + this.setState({keypadConfig: configuration}); + + // TODO(matthewc)[LC-1080]: this was brought in from v1's ProvidedKeypad. + // We need to investigate whether we still need this. + // HACK(charlie): In Perseus, triggering a focus causes the keypad to + // animate into view and re-configure. We'd like to provide the option + // to re-render the re-configured keypad before animating it into view, + // to avoid jank in the animation. As such, we support passing a + // callback into `configureKeypad`. However, implementing this properly + // would require middleware, etc., so we just hack it on with + // `setTimeout` for now. + setTimeout(() => cb && cb()); + }; + + setCursor: (cursor: Cursor) => void = (cursor) => { + this.setState({cursor}); + }; + + setKeyHandler: (keyHandler: KeyHandler) => void = (keyHandler) => { + this.setState({keyHandler}); + }; + + getDOMNode: () => ReturnType = () => { + return ReactDOM.findDOMNode(this); + }; + + _handleClickKey(key: Key) { + if (key === "DISMISS") { + this.dismiss(); + return; + } + + const cursor = this.state.keyHandler?.(key); + this.setState({cursor}); + } + + render(): React.ReactNode { + const {keypadActive, style} = this.props; + const {containerWidth, cursor, keypadConfig} = this.state; + + const containerStyle = [ + styles.keypadContainer, + // styles passed as props + ...(Array.isArray(style) ? style : [style]), + ]; + + const isExpression = keypadConfig?.keypadType === "EXPRESSION"; + const convertDotToTimes = keypadConfig?.times; + + return ( + + + {keypadActive ? ( + this._handleClickKey(key)} + cursorContext={cursor?.context} + fractionsOnly={!isExpression} + convertDotToTimes={convertDotToTimes} + divisionKey={isExpression} + trigonometry={isExpression} + preAlgebra={isExpression} + logarithms={isExpression} + basicRelations={isExpression} + advancedRelations={isExpression} + expandedView={ + containerWidth > expandedViewThreshold + } + showDismiss + /> + ) : null} + + + ); + } +} + +const styles = StyleSheet.create({ + keypadContainer: { + bottom: 0, + left: 0, + right: 0, + position: "fixed", + }, +}); + +export default MobileKeypadInternals; diff --git a/packages/math-input/src/components/keypad/mobile-keypad.tsx b/packages/math-input/src/components/keypad/mobile-keypad.tsx index 441dea17ce..754ce070b7 100644 --- a/packages/math-input/src/components/keypad/mobile-keypad.tsx +++ b/packages/math-input/src/components/keypad/mobile-keypad.tsx @@ -1,237 +1,24 @@ -import {StyleSheet} from "aphrodite"; import * as React from "react"; -import ReactDOM from "react-dom"; -import {View} from "../../fake-react-native-web/index"; -import AphroditeCssTransitionGroup from "../aphrodite-css-transition-group"; - -import Keypad from "./keypad"; -import {expandedViewThreshold} from "./utils"; - -import type Key from "../../data/keys"; -import type { - Cursor, - KeypadConfiguration, - KeyHandler, - KeypadAPI, -} from "../../types"; -import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; -import type {StyleType} from "@khanacademy/wonder-blocks-core"; - -const AnimationDurationInMS = 200; - -type Props = { - onElementMounted?: (arg1: any) => void; - onDismiss?: () => void; - style?: StyleType; - onAnalyticsEvent: AnalyticsEventHandlerFn; - setKeypadActive: (keypadActive: boolean) => void; - keypadActive: boolean; -}; - -type State = { - containerWidth: number; - keypadConfig?: KeypadConfiguration; - keyHandler?: KeyHandler; - cursor?: Cursor; -}; - -/** - * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same - * external API so that it can be hot-swapped with the v1 keypad and - * is responsible for connecting the keypad with MathInput and the Renderer. - * - * Ideally this strategy of attaching methods on the class component for - * other components to call will be replaced props/callbacks since React - * doesn't support this type of code anymore (functional components - * can't have methods attached to them). - */ -class MobileKeypad extends React.Component implements KeypadAPI { - _containerRef = React.createRef(); - _containerResizeObserver: ResizeObserver | null = null; - _throttleResize = false; - - state: State = { - containerWidth: 0, - }; - - componentDidMount() { - this._resize(); - - window.addEventListener("resize", this._throttleResizeHandler); - window.addEventListener( - "orientationchange", - this._throttleResizeHandler, - ); - - // LC-1213: some common older browsers (as of 2023-09-07) - // don't support ResizeObserver - if ("ResizeObserver" in window) { - this._containerResizeObserver = new window.ResizeObserver( - this._throttleResizeHandler, - ); - - if (this._containerRef.current) { - this._containerResizeObserver.observe( - this._containerRef.current, - ); - } - } - - this.props.onElementMounted?.({ - activate: this.activate, - dismiss: this.dismiss, - configure: this.configure, - setCursor: this.setCursor, - setKeyHandler: this.setKeyHandler, - getDOMNode: this.getDOMNode, - }); - } - - componentWillUnmount() { - window.removeEventListener("resize", this._throttleResizeHandler); - window.removeEventListener( - "orientationchange", - this._throttleResizeHandler, - ); - this._containerResizeObserver?.disconnect(); - } - - _resize = () => { - const containerWidth = this._containerRef.current?.clientWidth || 0; - this.setState({containerWidth}); - }; - - _throttleResizeHandler = () => { - if (this._throttleResize) { - return; - } - - this._throttleResize = true; - - setTimeout(() => { - this._resize(); - this._throttleResize = false; - }, 100); - }; - - activate: () => void = () => { - this.props.setKeypadActive(true); - }; - - dismiss: () => void = () => { - this.props.setKeypadActive(false); - this.props.onDismiss?.(); - }; - - configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( - configuration, - cb, - ) => { - this.setState({keypadConfig: configuration}); - - // TODO(matthewc)[LC-1080]: this was brought in from v1's ProvidedKeypad. - // We need to investigate whether we still need this. - // HACK(charlie): In Perseus, triggering a focus causes the keypad to - // animate into view and re-configure. We'd like to provide the option - // to re-render the re-configured keypad before animating it into view, - // to avoid jank in the animation. As such, we support passing a - // callback into `configureKeypad`. However, implementing this properly - // would require middleware, etc., so we just hack it on with - // `setTimeout` for now. - setTimeout(() => cb && cb()); - }; - - setCursor: (cursor: Cursor) => void = (cursor) => { - this.setState({cursor}); - }; - - setKeyHandler: (keyHandler: KeyHandler) => void = (keyHandler) => { - this.setState({keyHandler}); - }; - - getDOMNode: () => ReturnType = () => { - return ReactDOM.findDOMNode(this); - }; - - _handleClickKey(key: Key) { - if (key === "DISMISS") { - this.dismiss(); - return; - } - - const cursor = this.state.keyHandler?.(key); - this.setState({cursor}); - } - - render(): React.ReactNode { - const {keypadActive, style} = this.props; - const {containerWidth, cursor, keypadConfig} = this.state; - - const containerStyle = [ - styles.keypadContainer, - // styles passed as props - ...(Array.isArray(style) ? style : [style]), - ]; - - const isExpression = keypadConfig?.keypadType === "EXPRESSION"; - const convertDotToTimes = keypadConfig?.times; - - return ( - - - {keypadActive ? ( - this._handleClickKey(key)} - cursorContext={cursor?.context} - fractionsOnly={!isExpression} - convertDotToTimes={convertDotToTimes} - divisionKey={isExpression} - trigonometry={isExpression} - preAlgebra={isExpression} - logarithms={isExpression} - basicRelations={isExpression} - advancedRelations={isExpression} - expandedView={ - containerWidth > expandedViewThreshold - } - showDismiss - /> - ) : null} - - - ); - } +import {KeypadContext} from "../keypad-context"; + +import MobileKeypadInternals from "./mobile-keypad-internals"; + +type Props = Omit< + React.ComponentProps, + "keypadActive" | "setKeypadActive" +>; + +export function MobileKeypad(props: Props) { + return ( + + {({keypadActive, setKeypadActive}) => ( + + )} + + ); } - -const styles = StyleSheet.create({ - keypadContainer: { - bottom: 0, - left: 0, - right: 0, - position: "fixed", - }, -}); - -export default MobileKeypad; diff --git a/packages/math-input/src/full-mobile-input.stories.tsx b/packages/math-input/src/full-mobile-input.stories.tsx index ab843d08eb..a4b8588272 100644 --- a/packages/math-input/src/full-mobile-input.stories.tsx +++ b/packages/math-input/src/full-mobile-input.stories.tsx @@ -93,7 +93,6 @@ const Basic = ({keypadElement, setKeypadElement}) => { } }} onDismiss={() => {}} - useV2Keypad={v2Keypad} onAnalyticsEvent={async (e) => action("onAnalyticsEvent")(e)} /> diff --git a/packages/math-input/src/index.ts b/packages/math-input/src/index.ts index d566b07a0a..e19d6bbc35 100644 --- a/packages/math-input/src/index.ts +++ b/packages/math-input/src/index.ts @@ -27,7 +27,7 @@ export {CursorContext} from "./components/input/cursor-contexts"; export {getCursorContext} from "./components/input/mathquill-helpers"; // Wrapper around v1 and v2 mobile keypads to switch between them -export {default as MobileKeypad} from "./components/keypad-switch"; +export {MobileKeypad} from "./components/keypad"; // Unwrapped v2 keypad for desktop export {default as DesktopKeypad} from "./components/keypad"; diff --git a/packages/perseus/src/__tests__/article-renderer.test.tsx b/packages/perseus/src/__tests__/article-renderer.test.tsx index 1a2d641a72..a0c490b400 100644 --- a/packages/perseus/src/__tests__/article-renderer.test.tsx +++ b/packages/perseus/src/__tests__/article-renderer.test.tsx @@ -1,6 +1,7 @@ import { StatefulKeypadContextProvider, KeypadContext, + MobileKeypad, } from "@khanacademy/math-input"; import {RenderStateRoot} from "@khanacademy/wonder-blocks-core"; import {screen, render, fireEvent, waitFor} from "@testing-library/react"; @@ -11,7 +12,6 @@ import { testDependencies, testDependenciesV2, } from "../../../../testing/test-dependencies"; -import KeypadSwitch from "../../../math-input/src/components/keypad-switch"; import {articleWithExpression} from "../__testdata__/article-renderer.testdata"; import ArticleRenderer from "../article-renderer"; import * as Dependencies from "../dependencies"; @@ -24,11 +24,10 @@ function KeypadWithContext() { {({setKeypadElement}) => { return ( - {}} onAnalyticsEvent={async () => {}} - useV2Keypad /> ); }} diff --git a/packages/perseus/src/mixins/provide-keypad.tsx b/packages/perseus/src/mixins/provide-keypad.tsx index 16e342f18a..900f448f09 100644 --- a/packages/perseus/src/mixins/provide-keypad.tsx +++ b/packages/perseus/src/mixins/provide-keypad.tsx @@ -102,7 +102,6 @@ const ProvideKeypad = { }} // @ts-expect-error - TS2339 - Property 'props' does not exist on type '{ readonly propTypes: { readonly apiOptions: React.PropType<{ customKeypad?: boolean | undefined; nativeKeypadProxy?: ((...a: readonly any[]) => unknown) | undefined; }>; readonly keypadStyle: Requireable; }; readonly getInitialState: () => { ...; }; readonly componentDidMount: () => void; readonly componentWil...'. style={_this.props.keypadStyle} - useV2Keypad={apiOptions.useV2Keypad} onAnalyticsEvent={async () => { // Intentionally left empty. This component is // deprecated and so we don't want/need to instrument diff --git a/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx b/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx index 004e5f49fe..f4fc63ef60 100644 --- a/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx +++ b/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx @@ -17,7 +17,6 @@ const Footer = (): React.ReactElement => { onElementMounted={setKeypadElement} onDismiss={() => renderer && renderer.blur()} style={styles.keypad} - useV2Keypad={true} onAnalyticsEvent={async (e) => { action("onAnalyticsEvent")(e); }} diff --git a/packages/perseus/src/widgets/__tests__/expression-mobile.test.tsx b/packages/perseus/src/widgets/__tests__/expression-mobile.test.tsx index 52b2bbfb42..213aa9d2a0 100644 --- a/packages/perseus/src/widgets/__tests__/expression-mobile.test.tsx +++ b/packages/perseus/src/widgets/__tests__/expression-mobile.test.tsx @@ -60,7 +60,6 @@ function KeypadWithContext() { onElementMounted={setKeypadElement} onDismiss={() => {}} onAnalyticsEvent={async () => {}} - useV2Keypad /> ); }}