Skip to content

Commit

Permalink
Add Fraction Keypad View (#667)
Browse files Browse the repository at this point in the history
## 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: #667
  • Loading branch information
SonicScrewdriver authored Aug 17, 2023
1 parent 1905432 commit b93f9f7
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 48 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-trees-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/math-input": minor
---

Added new Mobile Fraction Keypad View to the V2 Keypad
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,19 @@ describe("keypad", () => {
}),
).not.toBeInTheDocument();
});

it(`hides the tabs if providing the Fraction Keypad`, () => {
// Arrange
// Act
render(
<Keypad
onClickKey={() => {}}
fractionsOnly={true}
sendEvent={async () => {}}
/>,
);

// Assert
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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 */}
<KeypadButton
keyConfig={Keys.NUM_1}
onClickKey={onClickKey}
coord={[0, 2]}
/>
<KeypadButton
keyConfig={Keys.NUM_2}
onClickKey={onClickKey}
coord={[1, 2]}
/>
<KeypadButton
keyConfig={Keys.NUM_3}
onClickKey={onClickKey}
coord={[2, 2]}
/>

{/* Row 3 */}
<KeypadButton
keyConfig={Keys.NUM_4}
onClickKey={onClickKey}
coord={[0, 1]}
/>
<KeypadButton
keyConfig={Keys.NUM_5}
onClickKey={onClickKey}
coord={[1, 1]}
/>
<KeypadButton
keyConfig={Keys.NUM_6}
onClickKey={onClickKey}
coord={[2, 1]}
/>

{/* Row 2 */}
<KeypadButton
keyConfig={Keys.NUM_7}
onClickKey={onClickKey}
coord={[0, 0]}
/>
<KeypadButton
keyConfig={Keys.NUM_8}
onClickKey={onClickKey}
coord={[1, 0]}
/>
<KeypadButton
keyConfig={Keys.NUM_9}
onClickKey={onClickKey}
coord={[2, 0]}
/>

{/* Row 1 */}
<KeypadButton
keyConfig={Keys.NUM_0}
onClickKey={onClickKey}
coord={[0, 3]}
/>
<KeypadButton
keyConfig={Keys.DECIMAL}
onClickKey={onClickKey}
coord={[1, 3]}
/>
<KeypadButton
keyConfig={Keys.NEGATIVE}
onClickKey={onClickKey}
coord={[2, 3]}
/>
{/* Side Column */}
<KeypadButton
keyConfig={Keys.PERCENT}
onClickKey={onClickKey}
coord={[3, 0]}
secondary
/>
<KeypadButton
keyConfig={Keys.PI}
onClickKey={onClickKey}
coord={[3, 1]}
secondary
/>
<KeypadButton
keyConfig={Keys.FRAC_INCLUSIVE}
onClickKey={onClickKey}
coord={[3, 2]}
secondary
/>
{cursorKeyConfig && (
<KeypadButton
keyConfig={cursorKeyConfig}
onClickKey={onClickKey}
coord={[3, 3]}
secondary
/>
)}
<KeypadButton
keyConfig={Keys.BACKSPACE}
onClickKey={onClickKey}
coord={[4, 3]}
action
/>
</>
);
}
13 changes: 13 additions & 0 deletions packages/math-input/src/components/keypad/keypad.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -25,6 +26,7 @@ export default {
basicRelations: false,
divisionKey: false,
logarithms: false,
fractionsOnly: false,
multiplicationDot: false,
preAlgebra: false,
trigonometry: false,
Expand Down Expand Up @@ -55,6 +57,12 @@ export default {
category: opsPage,
},
},
fractionsOnly: {
control: "boolean",
table: {
category: fracPage,
},
},
multiplicationDot: {
control: "boolean",
table: {
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 53 additions & 19 deletions packages/math-input/src/components/keypad/keypad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,6 +32,7 @@ export type Props = {
logarithms?: boolean;
basicRelations?: boolean;
advancedRelations?: boolean;
fractionsOnly?: boolean;

onClickKey: ClickKeyCallback;
sendEvent?: SendEventFn;
Expand All @@ -40,38 +42,46 @@ const defaultProps = {
extraKeys: [],
};

function allPages(props: Props): ReadonlyArray<TabbarItemType> {
const pages: Array<TabbarItemType> = ["Numbers"];
function getAvailableTabs(props: Props): ReadonlyArray<TabbarItemType> {
// We don't want to show any available tabs on the fractions keypad
if (props.fractionsOnly) {
return [];
}

const tabs: Array<TabbarItemType> = ["Numbers"];
if (
// OperatorsButtonSets
props.preAlgebra ||
props.logarithms ||
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<TabbarItemType>("Numbers");
React.useState<TabbarItemType>(defaultSelectedPage);
const [isMounted, setIsMounted] = React.useState<boolean>(false);

const availablePages = allPages(props);
// We don't want any tabs available on mobile fractions keypad
const availableTabs = getAvailableTabs(props);

const {
onClickKey,
Expand All @@ -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?.({
Expand All @@ -109,7 +130,7 @@ export default function Keypad(props: Props) {
return (
<View>
<Tabbar
items={availablePages}
items={availableTabs}
selectedItem={selectedPage}
onSelectItem={(tabbarItem: TabbarItemType) => {
setSelectedPage(tabbarItem);
Expand All @@ -121,11 +142,17 @@ export default function Keypad(props: Props) {
/>

<View
style={styles.grid}
style={[styles.keypadGrid, gridStyle]}
role="grid"
tabIndex={0}
aria-label="Keypad"
>
{selectedPage === "Fractions" && (
<FractionsPage
onClickKey={onClickKey}
cursorContext={cursorContext}
/>
)}
{selectedPage === "Numbers" && (
<NumbersPage onClickKey={onClickKey} />
)}
Expand All @@ -144,13 +171,15 @@ export default function Keypad(props: Props) {
{selectedPage === "Geometry" && (
<GeometryPage onClickKey={onClickKey} />
)}
<SharedKeys
onClickKey={onClickKey}
cursorContext={cursorContext}
multiplicationDot={multiplicationDot}
divisionKey={divisionKey}
selectedPage={selectedPage}
/>
{!fractionsOnly && (
<SharedKeys
onClickKey={onClickKey}
cursorContext={cursorContext}
multiplicationDot={multiplicationDot}
divisionKey={divisionKey}
selectedPage={selectedPage}
/>
)}
</View>
</View>
);
Expand All @@ -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)",
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class MobileKeypad extends React.Component<Props, State> implements KeypadAPI {
extraKeys={keypadConfig?.extraKeys}
onClickKey={(key) => this._handleClickKey(key)}
cursorContext={cursor?.context}
fractionsOnly={!isExpression}
multiplicationDot={isExpression}
divisionKey={isExpression}
trigonometry={isExpression}
Expand Down
Loading

0 comments on commit b93f9f7

Please sign in to comment.