From b93f9f7484929c1c4e3c99a4b7ce8ab1216b1079 Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Thu, 17 Aug 2023 11:38:44 -0700 Subject: [PATCH] Add Fraction Keypad View (#667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Added the fraction keypad view to the new v2 keypad as part of our overhaul of MathInput. This fraction view is used by the following Perseus Widgets: - InputNumber - Matrix - NumberLine - NumericInput - Table I've also updated existing stories to be able to demonstrate this new view. As it turns out, we're not doing anything custom with our numberpad page for the Fraction Keypad, so we can reuse the preexisting page. Note: I've settled on the prop name of "fractionsOnly" but I am open to any suggestions on a more descriptive name. I felt "fractionsOnly" was the closest I could get to a balance of being descriptive against the tab props, yet brief. ## Screenshots: (Forcing the "IN_NUMERATOR" calculator context in the _Full Mobile MathInput_ Story to show the context button) ![Screenshot 2023-08-15 at 3 43 51 PM](https://github.com/Khan/perseus/assets/12463099/16dc4ca1-a1ba-4cbc-85e6-86ed3678b644) (No cursor context using _Full Keypad_ Story) ![Screenshot 2023-08-15 at 3 40 34 PM](https://github.com/Khan/perseus/assets/12463099/ffa3ce6c-057a-4146-8d5c-2984098535d5) (Based on our [Figma Designs](https://www.figma.com/file/2lUPOSbOP8tbW7RLqbBFLh/Expression-Widget?type=design&node-id=4674-87332&mode=design&t=gZTp9zvbYilUKfYa-0)) Issue: LC-1098 ## Test plan: - manual testing - new stories Author: SonicScrewdriver Reviewers: handeyeco, SonicScrewdriver Required Reviewers: Approved By: handeyeco, handeyeco Checks: ✅ finish_coverage, ✅ Publish npm snapshot (ubuntu-latest, 16.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 16.x), ✅ Extract i18n strings (ubuntu-latest, 16.x), ✅ Cypress Coverage (ubuntu-latest, 16.x), ✅ Check for .changeset file (ubuntu-latest, 16.x), ✅ Check builds for changes in size (ubuntu-latest, 16.x), ✅ Jest Coverage (ubuntu-latest, 16.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/667 --- .changeset/seven-trees-design.md | 5 + .../keypad/__tests__/keypad.test.tsx | 15 +++ .../keypad/keypad-pages/fractions-page.tsx | 125 ++++++++++++++++++ .../src/components/keypad/keypad.stories.tsx | 13 ++ .../src/components/keypad/keypad.tsx | 72 +++++++--- .../src/components/keypad/mobile-keypad.tsx | 1 + .../src/components/keypad/shared-keys.tsx | 28 +--- .../math-input/src/components/keypad/utils.ts | 30 +++++ .../math-input/src/components/tabbar/types.ts | 1 + .../src/full-math-input.stories.tsx | 38 +++++- 10 files changed, 280 insertions(+), 48 deletions(-) create mode 100644 .changeset/seven-trees-design.md create mode 100644 packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx create mode 100644 packages/math-input/src/components/keypad/utils.ts diff --git a/.changeset/seven-trees-design.md b/.changeset/seven-trees-design.md new file mode 100644 index 0000000000..e031280e08 --- /dev/null +++ b/.changeset/seven-trees-design.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/math-input": minor +--- + +Added new Mobile Fraction Keypad View to the V2 Keypad diff --git a/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx b/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx index ec901d9ded..5d54af85bc 100644 --- a/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx +++ b/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx @@ -74,4 +74,19 @@ describe("keypad", () => { }), ).not.toBeInTheDocument(); }); + + it(`hides the tabs if providing the Fraction Keypad`, () => { + // Arrange + // Act + render( + {}} + fractionsOnly={true} + sendEvent={async () => {}} + />, + ); + + // Assert + expect(screen.queryByRole("tab")).not.toBeInTheDocument(); + }); }); diff --git a/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx b/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx new file mode 100644 index 0000000000..eec3afd122 --- /dev/null +++ b/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx @@ -0,0 +1,125 @@ +import * as React from "react"; + +import Keys from "../../../data/key-configs"; +import {KeypadButton} from "../keypad-button"; +import {getCursorContextConfig} from "../utils"; + +import type {ClickKeyCallback} from "../../../types"; +import type {CursorContext} from "../../input/cursor-contexts"; + +type Props = { + onClickKey: ClickKeyCallback; + cursorContext?: typeof CursorContext[keyof typeof CursorContext]; +}; + +export default function FractionsPage(props: Props) { + const {onClickKey, cursorContext} = props; + const cursorKeyConfig = getCursorContextConfig(cursorContext); + // These keys are arranged sequentially so that tabbing follows numerical order. This + // allows us to visually mimic a keypad without affecting a11y. The visual order of the + // keys in the keypad is determined by their coordinates, not their order in the DOM. + return ( + <> + {/* Row 4 */} + + + + + {/* Row 3 */} + + + + + {/* Row 2 */} + + + + + {/* Row 1 */} + + + + {/* Side Column */} + + + + {cursorKeyConfig && ( + + )} + + + ); +} diff --git a/packages/math-input/src/components/keypad/keypad.stories.tsx b/packages/math-input/src/components/keypad/keypad.stories.tsx index fb57909c4f..c33588e5db 100644 --- a/packages/math-input/src/components/keypad/keypad.stories.tsx +++ b/packages/math-input/src/components/keypad/keypad.stories.tsx @@ -10,6 +10,7 @@ import type {ComponentStory} from "@storybook/react"; const opsPage = "Operators Page"; const numsPage = "Numbers Page"; const geoPage = "Geometry Page"; +const fracPage = "Fractions Page"; export default { title: "Full Keypad", @@ -25,6 +26,7 @@ export default { basicRelations: false, divisionKey: false, logarithms: false, + fractionsOnly: false, multiplicationDot: false, preAlgebra: false, trigonometry: false, @@ -55,6 +57,12 @@ export default { category: opsPage, }, }, + fractionsOnly: { + control: "boolean", + table: { + category: fracPage, + }, + }, multiplicationDot: { control: "boolean", table: { @@ -93,6 +101,11 @@ Trigonometry.args = { trigonometry: true, }; +export const FractionsOnly = Template.bind({}); +FractionsOnly.args = { + fractionsOnly: true, +}; + export const Everything = Template.bind({}); Everything.args = { advancedRelations: true, diff --git a/packages/math-input/src/components/keypad/keypad.tsx b/packages/math-input/src/components/keypad/keypad.tsx index 0115ff106d..4acd4eb047 100644 --- a/packages/math-input/src/components/keypad/keypad.tsx +++ b/packages/math-input/src/components/keypad/keypad.tsx @@ -7,6 +7,7 @@ import {useEffect} from "react"; import Tabbar from "../tabbar"; import ExtrasPage from "./keypad-pages/extras-page"; +import FractionsPage from "./keypad-pages/fractions-page"; import GeometryPage from "./keypad-pages/geometry-page"; import NumbersPage from "./keypad-pages/numbers-page"; import OperatorsPage from "./keypad-pages/operators-page"; @@ -31,6 +32,7 @@ export type Props = { logarithms?: boolean; basicRelations?: boolean; advancedRelations?: boolean; + fractionsOnly?: boolean; onClickKey: ClickKeyCallback; sendEvent?: SendEventFn; @@ -40,9 +42,13 @@ const defaultProps = { extraKeys: [], }; -function allPages(props: Props): ReadonlyArray { - const pages: Array = ["Numbers"]; +function getAvailableTabs(props: Props): ReadonlyArray { + // We don't want to show any available tabs on the fractions keypad + if (props.fractionsOnly) { + return []; + } + const tabs: Array = ["Numbers"]; if ( // OperatorsButtonSets props.preAlgebra || @@ -50,28 +56,32 @@ function allPages(props: Props): ReadonlyArray { props.basicRelations || props.advancedRelations ) { - pages.push("Operators"); + tabs.push("Operators"); } if (props.trigonometry) { - pages.push("Geometry"); + tabs.push("Geometry"); } if (props.extraKeys?.length) { - pages.push("Extras"); + tabs.push("Extras"); } - return pages; + return tabs; } // The main (v2) Keypad. Use this component to present an accessible, onscreen // keypad to learners for entering math expressions. export default function Keypad(props: Props) { + // If we're using the Fractions keyapd, we want to default select that page + // Otherwise, we want to default to the Numbers page + const defaultSelectedPage = props.fractionsOnly ? "Fractions" : "Numbers"; const [selectedPage, setSelectedPage] = - React.useState("Numbers"); + React.useState(defaultSelectedPage); const [isMounted, setIsMounted] = React.useState(false); - const availablePages = allPages(props); + // We don't want any tabs available on mobile fractions keypad + const availableTabs = getAvailableTabs(props); const { onClickKey, @@ -84,9 +94,20 @@ export default function Keypad(props: Props) { basicRelations, advancedRelations, showDismiss, + fractionsOnly, sendEvent, } = props; + // Use a different grid for our fraction keypad + const gridStyle = fractionsOnly + ? styles.fractionsGrid + : styles.expressionGrid; + + // This useeffect is only used to ensure that we can test the keypad in storybook + useEffect(() => { + setSelectedPage(defaultSelectedPage); + }, [fractionsOnly, defaultSelectedPage]); + useEffect(() => { if (!isMounted) { sendEvent?.({ @@ -109,7 +130,7 @@ export default function Keypad(props: Props) { return ( { setSelectedPage(tabbarItem); @@ -121,11 +142,17 @@ export default function Keypad(props: Props) { /> + {selectedPage === "Fractions" && ( + + )} {selectedPage === "Numbers" && ( )} @@ -144,13 +171,15 @@ export default function Keypad(props: Props) { {selectedPage === "Geometry" && ( )} - + {!fractionsOnly && ( + + )} ); @@ -162,10 +191,15 @@ const styles = StyleSheet.create({ tabbar: { background: Color.white, }, - grid: { + keypadGrid: { display: "grid", - gridTemplateColumns: "repeat(6, 1fr)", gridTemplateRows: "repeat(4, 1fr)", backgroundColor: "#DBDCDD", }, + expressionGrid: { + gridTemplateColumns: "repeat(6, 1fr)", + }, + fractionsGrid: { + gridTemplateColumns: "repeat(5, 1fr)", + }, }); diff --git a/packages/math-input/src/components/keypad/mobile-keypad.tsx b/packages/math-input/src/components/keypad/mobile-keypad.tsx index 350fe1309f..9456bf8d02 100644 --- a/packages/math-input/src/components/keypad/mobile-keypad.tsx +++ b/packages/math-input/src/components/keypad/mobile-keypad.tsx @@ -142,6 +142,7 @@ class MobileKeypad extends React.Component implements KeypadAPI { extraKeys={keypadConfig?.extraKeys} onClickKey={(key) => this._handleClickKey(key)} cursorContext={cursor?.context} + fractionsOnly={!isExpression} multiplicationDot={isExpression} divisionKey={isExpression} trigonometry={isExpression} diff --git a/packages/math-input/src/components/keypad/shared-keys.tsx b/packages/math-input/src/components/keypad/shared-keys.tsx index ae03f519ac..581280b25a 100644 --- a/packages/math-input/src/components/keypad/shared-keys.tsx +++ b/packages/math-input/src/components/keypad/shared-keys.tsx @@ -1,11 +1,12 @@ import * as React from "react"; import Keys from "../../data/key-configs"; -import {CursorContext} from "../input/cursor-contexts"; import {KeypadButton} from "./keypad-button"; +import {getCursorContextConfig} from "./utils"; import type {ClickKeyCallback} from "../../types"; +import type {CursorContext} from "../input/cursor-contexts"; import type {TabbarItemType} from "../tabbar"; type Props = { @@ -16,31 +17,6 @@ type Props = { divisionKey?: boolean; }; -function getCursorContextConfig( - cursorContext?: typeof CursorContext[keyof typeof CursorContext], -) { - if (!cursorContext) { - return null; - } - - switch (cursorContext) { - case CursorContext.NONE: - return null; - case CursorContext.IN_PARENS: - return Keys.JUMP_OUT_PARENTHESES; - case CursorContext.IN_SUPER_SCRIPT: - return Keys.JUMP_OUT_EXPONENT; - case CursorContext.IN_SUB_SCRIPT: - return Keys.JUMP_OUT_BASE; - case CursorContext.IN_NUMERATOR: - return Keys.JUMP_OUT_NUMERATOR; - case CursorContext.IN_DENOMINATOR: - return Keys.JUMP_OUT_DENOMINATOR; - case CursorContext.BEFORE_FRACTION: - return Keys.JUMP_INTO_NUMERATOR; - } -} - export default function SharedKeys(props: Props) { const { onClickKey, diff --git a/packages/math-input/src/components/keypad/utils.ts b/packages/math-input/src/components/keypad/utils.ts new file mode 100644 index 0000000000..bb70945b18 --- /dev/null +++ b/packages/math-input/src/components/keypad/utils.ts @@ -0,0 +1,30 @@ +import Keys from "../../data/key-configs"; +import {CursorContext} from "../input/cursor-contexts"; + +// This is a helper function that returns the correct context for the cursor +// based on the cursorContext prop. It is used in the keypad to determine +// which key to render as the "jump out" key. +export function getCursorContextConfig( + cursorContext?: typeof CursorContext[keyof typeof CursorContext], +) { + if (!cursorContext) { + return null; + } + + switch (cursorContext) { + case CursorContext.NONE: + return null; + case CursorContext.IN_PARENS: + return Keys.JUMP_OUT_PARENTHESES; + case CursorContext.IN_SUPER_SCRIPT: + return Keys.JUMP_OUT_EXPONENT; + case CursorContext.IN_SUB_SCRIPT: + return Keys.JUMP_OUT_BASE; + case CursorContext.IN_NUMERATOR: + return Keys.JUMP_OUT_NUMERATOR; + case CursorContext.IN_DENOMINATOR: + return Keys.JUMP_OUT_DENOMINATOR; + case CursorContext.BEFORE_FRACTION: + return Keys.JUMP_INTO_NUMERATOR; + } +} diff --git a/packages/math-input/src/components/tabbar/types.ts b/packages/math-input/src/components/tabbar/types.ts index 2f8a52f5e0..a0f4bcc1cf 100644 --- a/packages/math-input/src/components/tabbar/types.ts +++ b/packages/math-input/src/components/tabbar/types.ts @@ -2,5 +2,6 @@ export type TabbarItemType = | "Geometry" | "Operators" | "Numbers" + | "Fractions" | "Extras" | "Dismiss"; diff --git a/packages/math-input/src/full-math-input.stories.tsx b/packages/math-input/src/full-math-input.stories.tsx index 22fecd75d6..633f1e2754 100644 --- a/packages/math-input/src/full-math-input.stories.tsx +++ b/packages/math-input/src/full-math-input.stories.tsx @@ -1,3 +1,4 @@ +import {INITIAL_VIEWPORTS} from "@storybook/addon-viewport"; import * as React from "react"; import type {KeypadAPI} from "./types"; @@ -6,6 +7,17 @@ import {KeypadInput, KeypadType, MobileKeypad} from "./index"; export default { title: "Full Mobile MathInput", + parameters: { + backgrounds: { + default: "light background", + values: [ + // We want a slightly darker default bg so that we can + // see the top of the keypad when it is open + {name: "light background", value: "lightgrey", default: true}, + ], + }, + viewport: {defaultViewport: "iphone6", viewports: INITIAL_VIEWPORTS}, + }, }; export const Basic = () => { @@ -13,9 +25,20 @@ export const Basic = () => { // Reference to the keypad const [keypadElement, setKeypadElement] = React.useState(); // Whether to use Expression or Fraction keypad - const [expression, setExpression] = React.useState(true); + const [expression, setExpression] = React.useState(false); // Whether to use v1 or v2 keypad - const [v2Keypad, setV2Keypad] = React.useState(false); + const [v2Keypad, setV2Keypad] = React.useState(true); + // Whether the keypad is open or not + const [keypadOpen, setKeypadOpen] = React.useState(false); + + const toggleKeypad = () => { + if (keypadOpen) { + keypadElement?.dismiss(); + } else { + keypadElement?.activate(); + } + setKeypadOpen(!keypadOpen); + }; React.useEffect(() => { keypadElement?.configure( @@ -30,14 +53,23 @@ export const Basic = () => { }, [keypadElement, expression]); return ( -
+
+ + NOTE: To properly test the input interaction, you will need + to simulate a device using the dev tools. + +