@khanacademy/math-input 4.3.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/components/input/math-wrapper.d.ts +2 -2
  3. package/dist/components/input/math-wrapper.js.flow +2 -4
  4. package/dist/components/input/mathquill-helpers.d.ts +1 -1
  5. package/dist/components/input/mathquill-helpers.js.flow +2 -2
  6. package/dist/components/input/mathquill-types.d.ts +270 -10
  7. package/dist/components/input/mathquill-types.js.flow +312 -10
  8. package/dist/components/keypad/index.d.ts +11 -1
  9. package/dist/components/keypad/index.js.flow +14 -1
  10. package/dist/components/keypad/shared-keys.d.ts +4 -0
  11. package/dist/components/keypad/shared-keys.js.flow +4 -0
  12. package/dist/components/tabbar/tabbar.d.ts +1 -0
  13. package/dist/components/tabbar/tabbar.js.flow +1 -0
  14. package/dist/components/tabbar/types.d.ts +1 -1
  15. package/dist/components/tabbar/types.js.flow +6 -1
  16. package/dist/es/index.js +173 -48
  17. package/dist/es/index.js.map +1 -1
  18. package/dist/index.js +173 -48
  19. package/dist/index.js.map +1 -1
  20. package/package.json +2 -1
  21. package/src/components/input/__tests__/mathquill-helpers.test.ts +105 -0
  22. package/src/components/input/math-input.tsx +1 -1
  23. package/src/components/input/math-wrapper.ts +6 -10
  24. package/src/components/input/mathquill-helpers.ts +8 -1
  25. package/src/components/input/mathquill-types.ts +308 -40
  26. package/src/components/key-handlers/__tests__/handle-jump-out.test.ts +94 -0
  27. package/src/components/key-handlers/handle-jump-out.ts +3 -2
  28. package/src/components/keypad/__tests__/keypad-v2-mathquill.test.tsx +42 -39
  29. package/src/components/keypad/__tests__/keypad.test.tsx +42 -0
  30. package/src/components/keypad/button-assets.tsx +34 -6
  31. package/src/components/keypad/index.tsx +25 -2
  32. package/src/components/keypad/keypad-mathquill.stories.tsx +19 -0
  33. package/src/components/keypad/keypad-pages/extras-page.tsx +1 -1
  34. package/src/components/keypad/keypad-pages/numbers-page.tsx +25 -16
  35. package/src/components/keypad/shared-keys.tsx +56 -8
  36. package/src/components/tabbar/__tests__/tabbar.test.tsx +54 -14
  37. package/src/components/tabbar/icons.tsx +34 -12
  38. package/src/components/tabbar/item.tsx +2 -0
  39. package/src/components/tabbar/tabbar.tsx +32 -12
  40. package/src/components/tabbar/types.ts +6 -1
  41. package/tsconfig-build.json +3 -1
  42. package/tsconfig-build.tsbuildinfo +1 -1
@@ -3,7 +3,7 @@ import {CursorContext} from "../input/cursor-contexts";
3
3
  import {
4
4
  isFraction,
5
5
  isParens,
6
- contextForCursor,
6
+ getCursorContext,
7
7
  getCursor,
8
8
  } from "../input/mathquill-helpers";
9
9
  import {mathQuillInstance} from "../input/mathquill-instance";
@@ -26,7 +26,7 @@ const KeysForJumpContext = {
26
26
  */
27
27
  function handleJumpOut(mathField: MathFieldInterface, key: Key): void {
28
28
  const cursor = getCursor(mathField);
29
- const context = contextForCursor(cursor);
29
+ const context = getCursorContext(mathField);
30
30
 
31
31
  // Validate that the current cursor context matches the key's intent.
32
32
  if (KeysForJumpContext[context] !== key) {
@@ -43,6 +43,7 @@ function handleJumpOut(mathField: MathFieldInterface, key: Key): void {
43
43
  // Insert at the end of the parentheses, and then navigate right
44
44
  // once more to get 'beyond' the parentheses.
45
45
  cursor.insRightOf(cursor.parent.parent);
46
+ mathField.keystroke("Right");
46
47
  break;
47
48
 
48
49
  case CursorContext.BEFORE_FRACTION:
@@ -65,6 +65,7 @@ function V2KeypadWithMathquill(props: Props) {
65
65
  multiplicationDot
66
66
  preAlgebra
67
67
  trigonometry
68
+ sendEvent={async () => {}}
68
69
  />
69
70
  </div>
70
71
  }
@@ -95,26 +96,26 @@ describe("Keypad v2 with MathQuill", () => {
95
96
  // Act
96
97
 
97
98
  // a^2
98
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
99
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
99
100
  userEvent.click(screen.getByRole("button", {name: "a"}));
100
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
101
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
101
102
  userEvent.click(screen.getByRole("button", {name: "Square"}));
102
103
 
103
104
  // +
104
- userEvent.click(screen.getByRole("button", {name: "Numbers"}));
105
+ userEvent.click(screen.getByRole("tab", {name: "Numbers"}));
105
106
  userEvent.click(screen.getByRole("button", {name: "Plus"}));
106
107
 
107
108
  // b^2 =
108
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
109
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
109
110
  userEvent.click(screen.getByRole("button", {name: "b"}));
110
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
111
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
111
112
  userEvent.click(screen.getByRole("button", {name: "Square"}));
112
113
  userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
113
114
 
114
115
  // c^2
115
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
116
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
116
117
  userEvent.click(screen.getByRole("button", {name: "c"}));
117
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
118
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
118
119
  userEvent.click(screen.getByRole("button", {name: "Square"}));
119
120
 
120
121
  // Assert
@@ -131,26 +132,26 @@ describe("Keypad v2 with MathQuill", () => {
131
132
  // Act
132
133
 
133
134
  // c = /Square root
134
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
135
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
135
136
  userEvent.click(screen.getByRole("button", {name: "c"}));
136
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
137
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
137
138
  userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
138
139
  userEvent.click(screen.getByRole("button", {name: "Square root"}));
139
140
 
140
141
  // a^2
141
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
142
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
142
143
  userEvent.click(screen.getByRole("button", {name: "a"}));
143
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
144
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
144
145
  userEvent.click(screen.getByRole("button", {name: "Square"}));
145
146
 
146
147
  // +
147
- userEvent.click(screen.getByRole("button", {name: "Numbers"}));
148
+ userEvent.click(screen.getByRole("tab", {name: "Numbers"}));
148
149
  userEvent.click(screen.getByRole("button", {name: "Plus"}));
149
150
 
150
151
  // b^2
151
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
152
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
152
153
  userEvent.click(screen.getByRole("button", {name: "b"}));
153
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
154
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
154
155
  userEvent.click(screen.getByRole("button", {name: "Square"}));
155
156
 
156
157
  // Assert
@@ -170,15 +171,15 @@ describe("Keypad v2 with MathQuill", () => {
170
171
 
171
172
  // Argument is empty because mathquill generates textarea w/o label
172
173
  userEvent.type(screen.getByRole("textbox"), "a");
173
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
174
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
174
175
  userEvent.click(screen.getByRole("button", {name: "Square"}));
175
176
 
176
177
  userEvent.type(screen.getByRole("textbox"), "+");
177
178
 
178
179
  // b^2
179
- userEvent.click(screen.getByRole("button", {name: "Extras"}));
180
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
180
181
  userEvent.click(screen.getByRole("button", {name: "b"}));
181
- userEvent.click(screen.getByRole("button", {name: "Operators"}));
182
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
182
183
  userEvent.click(screen.getByRole("button", {name: "Square"}));
183
184
  userEvent.type(screen.getByRole("textbox"), "=c^2");
184
185
 
@@ -194,33 +195,35 @@ describe("Keypad v2 with MathQuill", () => {
194
195
  );
195
196
 
196
197
  // Act
197
- // Write `a^2+b^2=c^2` using the keypad
198
- const buttonPressesForFormula = [
199
- "Extras",
200
- "a",
201
- "Operators",
202
- "Square",
203
- "Numbers",
204
- "Plus",
205
- "Extras",
206
- "b",
207
- "Operators",
208
- "Square",
209
- "Equals sign",
210
- "Extras",
211
- "c",
212
- "Operators",
213
- "Square",
214
- ];
215
- buttonPressesForFormula.forEach((button) => {
216
- userEvent.click(screen.getByRole("button", {name: button}));
217
- });
198
+
199
+ // a^2
200
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
201
+ userEvent.click(screen.getByRole("button", {name: "a"}));
202
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
203
+ userEvent.click(screen.getByRole("button", {name: "Square"}));
204
+
205
+ // +
206
+ userEvent.click(screen.getByRole("tab", {name: "Numbers"}));
207
+ userEvent.click(screen.getByRole("button", {name: "Plus"}));
208
+
209
+ // b^2
210
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
211
+ userEvent.click(screen.getByRole("button", {name: "b"}));
212
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
213
+ userEvent.click(screen.getByRole("button", {name: "Square"}));
214
+
215
+ // =c^2
216
+ userEvent.click(screen.getByRole("button", {name: "Equals sign"}));
217
+ userEvent.click(screen.getByRole("tab", {name: "Extras"}));
218
+ userEvent.click(screen.getByRole("button", {name: "c"}));
219
+ userEvent.click(screen.getByRole("tab", {name: "Operators"}));
220
+ userEvent.click(screen.getByRole("button", {name: "Square"}));
218
221
 
219
222
  // Assert
220
223
  // make sure the formula was typed correctly
221
224
  expect(mockMathInputCallback).toHaveBeenLastCalledWith("a^2+b^2=c^2");
222
225
 
223
- userEvent.click(screen.getByRole("button", {name: "Numbers"}));
226
+ userEvent.click(screen.getByRole("tab", {name: "Numbers"}));
224
227
  // delete: need 14 backspaces in MathQuill to delete `a^2+b^2=c^2`
225
228
  for (let i = 0; i < 14; i++) {
226
229
  userEvent.click(screen.getByRole("button", {name: "Delete"}));
@@ -0,0 +1,42 @@
1
+ import {render, screen} from "@testing-library/react";
2
+ import * as React from "react";
3
+
4
+ import keyConfigs from "../../../data/key-configs";
5
+ import {CursorContext} from "../../input/cursor-contexts";
6
+ import Keypad from "../index";
7
+
8
+ const contextToKeyAria = {
9
+ [CursorContext.IN_PARENS]: keyConfigs.JUMP_OUT_PARENTHESES.ariaLabel,
10
+ [CursorContext.IN_SUPER_SCRIPT]: keyConfigs.JUMP_OUT_EXPONENT.ariaLabel,
11
+ [CursorContext.IN_SUB_SCRIPT]: keyConfigs.JUMP_OUT_BASE.ariaLabel,
12
+ [CursorContext.IN_NUMERATOR]: keyConfigs.JUMP_OUT_NUMERATOR.ariaLabel,
13
+ [CursorContext.IN_DENOMINATOR]: keyConfigs.JUMP_OUT_DENOMINATOR.ariaLabel,
14
+ [CursorContext.BEFORE_FRACTION]: keyConfigs.JUMP_INTO_NUMERATOR.ariaLabel,
15
+ };
16
+
17
+ describe("keypad", () => {
18
+ describe("shows navigation buttons", () => {
19
+ Object.entries(contextToKeyAria).forEach(([context, ariaLabel]) => {
20
+ it(`shows button for ${context}`, () => {
21
+ // Arrange
22
+ // Act
23
+ render(
24
+ <Keypad
25
+ onClickKey={() => {}}
26
+ cursorContext={context as CursorContext}
27
+ sendEvent={async () => {
28
+ /* TODO: verify correct analytics event sent */
29
+ }}
30
+ />,
31
+ );
32
+
33
+ // Assert
34
+ expect(
35
+ screen.getByRole("button", {
36
+ name: ariaLabel,
37
+ }),
38
+ );
39
+ });
40
+ });
41
+ });
42
+ });
@@ -706,12 +706,17 @@ export default function ButtonAsset({id}: Props): React.ReactElement {
706
706
  return (
707
707
  <svg
708
708
  xmlns="http://www.w3.org/2000/svg"
709
- width="20"
710
- height="20"
711
- fill="currentColor"
712
- viewBox="0 0 256 256"
709
+ width="40"
710
+ height="40"
711
+ fill="none"
712
+ viewBox="0 0 40 40"
713
713
  >
714
- <path d="M205.66,61.64l-144,144a8,8,0,0,1-11.32-11.32l144-144a8,8,0,0,1,11.32,11.31ZM50.54,101.44a36,36,0,0,1,50.92-50.91h0a36,36,0,0,1-50.92,50.91ZM56,76A20,20,0,1,0,90.14,61.84h0A20,20,0,0,0,56,76ZM216,180a36,36,0,1,1-10.54-25.46h0A35.76,35.76,0,0,1,216,180Zm-16,0a20,20,0,1,0-5.86,14.14A19.87,19.87,0,0,0,200,180Z" />
714
+ <path
715
+ fill="currentColor"
716
+ fillRule="evenodd"
717
+ d="M24.447 11.106a1 1 0 0 1 .447 1.341l-8 16a1 1 0 1 1-1.788-.894l8-16a1 1 0 0 1 1.341-.447ZM15 13a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm12 10a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm2-4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z"
718
+ clipRule="evenodd"
719
+ />
715
720
  </svg>
716
721
  );
717
722
  case "PI":
@@ -821,6 +826,30 @@ export default function ButtonAsset({id}: Props): React.ReactElement {
821
826
  />
822
827
  </svg>
823
828
  );
829
+ case "JUMP_OUT_BASE":
830
+ return (
831
+ <svg
832
+ width="40"
833
+ height="40"
834
+ viewBox="0 0 40 40"
835
+ fill="none"
836
+ style={{display: "block", transform: "scale(1,-1)"}}
837
+ xmlns="http://www.w3.org/2000/svg"
838
+ >
839
+ <path
840
+ fillRule="evenodd"
841
+ clipRule="evenodd"
842
+ d="M18.2929 15.2929c.3905-.3905 1.0237-.3905 1.4142 0L26 21.5858V20c0-.5523.4477-1 1-1s1 .4477 1 1v4.003c-.0004.1345-.0273.2627-.0759.3798-.0477.1152-.1178.2234-.2105.3177a.809004.809004 0 01-.0131.0131c-.1797.1765-.4259.2856-.6975.2864H23c-.5523 0-1-.4477-1-1s.4477-1 1-1h1.5858l-6.2929-6.2929c-.3905-.3905-.3905-1.0237 0-1.4142zM31 33c-.5523 0-1-.4477-1-1V16c0-.5523.4477-1 1-1s1 .4477 1 1v16c0 .5523-.4477 1-1 1z"
843
+ fill="#1865F2"
844
+ />
845
+ <path
846
+ fillRule="evenodd"
847
+ clipRule="evenodd"
848
+ d="M9 9c-.55228 0-1 .44772-1 1v6c0 .5523.44772 1 1 1h6c.5523 0 1-.4477 1-1v-6c0-.55228-.4477-1-1-1H9zm5 2h-4v4h4v-4z"
849
+ fill="#21242C"
850
+ />
851
+ </svg>
852
+ );
824
853
  case "JUMP_INTO_NUMERATOR":
825
854
  return (
826
855
  <svg
@@ -964,7 +993,6 @@ export default function ButtonAsset({id}: Props): React.ReactElement {
964
993
  case "DOWN":
965
994
  case "LEFT":
966
995
  case "RIGHT":
967
- case "JUMP_OUT_BASE":
968
996
  case "PHI":
969
997
  case "NTHROOT3":
970
998
  case "POW":
@@ -5,6 +5,7 @@ import * as React from "react";
5
5
 
6
6
  import Key from "../../data/keys";
7
7
  import {ClickKeyCallback} from "../../types";
8
+ import {CursorContext} from "../input/cursor-contexts";
8
9
  import Tabbar from "../tabbar/tabbar";
9
10
  import {TabbarItemType} from "../tabbar/types";
10
11
 
@@ -14,8 +15,11 @@ import NumbersPage from "./keypad-pages/numbers-page";
14
15
  import OperatorsPage from "./keypad-pages/operators-page";
15
16
  import SharedKeys from "./shared-keys";
16
17
 
18
+ import type {SendEventFn} from "@khanacademy/perseus-core";
19
+
17
20
  export type Props = {
18
21
  onClickKey: ClickKeyCallback;
22
+ cursorContext?: CursorContext;
19
23
  trigonometry?: boolean;
20
24
  extraKeys: ReadonlyArray<Key>;
21
25
  multiplicationDot?: boolean;
@@ -24,6 +28,12 @@ export type Props = {
24
28
  logarithms?: boolean;
25
29
  basicRelations?: boolean;
26
30
  advancedRelations?: boolean;
31
+
32
+ sendEvent: SendEventFn;
33
+ };
34
+
35
+ const defaultProps = {
36
+ extraKeys: [],
27
37
  };
28
38
 
29
39
  function allPages(props: Props): ReadonlyArray<TabbarItemType> {
@@ -50,6 +60,8 @@ function allPages(props: Props): ReadonlyArray<TabbarItemType> {
50
60
  return pages;
51
61
  }
52
62
 
63
+ // The main (v2) Keypad. Use this component to present an accessible, onscreen
64
+ // keypad to learners for entering math expressions.
53
65
  export default function Keypad(props: Props) {
54
66
  const [selectedPage, setSelectedPage] =
55
67
  React.useState<TabbarItemType>("Numbers");
@@ -58,7 +70,8 @@ export default function Keypad(props: Props) {
58
70
 
59
71
  const {
60
72
  onClickKey,
61
- extraKeys = [],
73
+ cursorContext,
74
+ extraKeys,
62
75
  multiplicationDot,
63
76
  divisionKey,
64
77
  preAlgebra,
@@ -76,9 +89,15 @@ export default function Keypad(props: Props) {
76
89
  setSelectedPage(tabbarItem);
77
90
  }}
78
91
  style={styles.tabbar}
92
+ onClickClose={() => onClickKey("DISMISS")}
79
93
  />
80
94
 
81
- <View style={styles.grid}>
95
+ <View
96
+ style={styles.grid}
97
+ role="grid"
98
+ tabIndex={0}
99
+ aria-label="Keypad"
100
+ >
82
101
  {selectedPage === "Numbers" && (
83
102
  <NumbersPage onClickKey={onClickKey} />
84
103
  )}
@@ -99,14 +118,18 @@ export default function Keypad(props: Props) {
99
118
  )}
100
119
  <SharedKeys
101
120
  onClickKey={onClickKey}
121
+ cursorContext={cursorContext}
102
122
  multiplicationDot={multiplicationDot}
103
123
  divisionKey={divisionKey}
124
+ selectedPage={selectedPage}
104
125
  />
105
126
  </View>
106
127
  </View>
107
128
  );
108
129
  }
109
130
 
131
+ Keypad.defaultProps = defaultProps;
132
+
110
133
  const styles = StyleSheet.create({
111
134
  tabbar: {
112
135
  background: Color.white,
@@ -3,6 +3,8 @@ import {Popover, PopoverContentCore} from "@khanacademy/wonder-blocks-popover";
3
3
  import * as React from "react";
4
4
 
5
5
  import Key from "../../data/keys";
6
+ import {CursorContext} from "../input/cursor-contexts";
7
+ import {getCursorContext} from "../input/mathquill-helpers";
6
8
  import {createMathField} from "../input/mathquill-instance";
7
9
  import {MathFieldInterface} from "../input/mathquill-types";
8
10
  import keyTranslator from "../key-handlers/key-translator";
@@ -16,11 +18,22 @@ export default {
16
18
  export function V2KeypadWithMathquill() {
17
19
  const mathFieldWrapperRef = React.useRef<HTMLDivElement>(null);
18
20
  const [mathField, setMathField] = React.useState<MathFieldInterface>();
21
+ const [cursorContext, setCursorContext] = React.useState<CursorContext>(
22
+ CursorContext.NONE,
23
+ );
19
24
 
20
25
  React.useEffect(() => {
21
26
  if (!mathField && mathFieldWrapperRef.current) {
22
27
  const mathFieldInstance = createMathField(
23
28
  mathFieldWrapperRef.current,
29
+ (baseConfig) => ({
30
+ ...baseConfig,
31
+ handlers: {
32
+ edit: (_mathField) => {
33
+ setCursorContext(getCursorContext(_mathField));
34
+ },
35
+ },
36
+ }),
24
37
  );
25
38
  setMathField(mathFieldInstance);
26
39
  }
@@ -34,6 +47,7 @@ export function V2KeypadWithMathquill() {
34
47
  const mathFieldCallback = keyTranslator[key];
35
48
  if (mathFieldCallback) {
36
49
  mathFieldCallback(mathField, key);
50
+ setCursorContext(getCursorContext(mathField));
37
51
  } else {
38
52
  // eslint-disable-next-line no-console
39
53
  console.warn(`No translation to Mathquill for: ${key}`);
@@ -53,6 +67,7 @@ export function V2KeypadWithMathquill() {
53
67
  <Keypad
54
68
  extraKeys={["x", "y", "PI", "THETA"]}
55
69
  onClickKey={handleClickKey}
70
+ cursorContext={cursorContext}
56
71
  advancedRelations
57
72
  basicRelations
58
73
  divisionKey
@@ -60,6 +75,10 @@ export function V2KeypadWithMathquill() {
60
75
  multiplicationDot
61
76
  preAlgebra
62
77
  trigonometry
78
+ sendEvent={async (event) => {
79
+ // eslint-disable-next-line no-console
80
+ console.log("Send Event:", event);
81
+ }}
63
82
  />
64
83
  </PopoverContentCore>
65
84
  }
@@ -10,7 +10,7 @@ type Props = {
10
10
  onClickKey: ClickKeyCallback;
11
11
  };
12
12
 
13
- const columns = 4;
13
+ const columns = 3;
14
14
 
15
15
  export default function ExtrasPage(props: Props) {
16
16
  const {extraKeys, onClickKey} = props;
@@ -10,26 +10,29 @@ type Props = {
10
10
 
11
11
  export default function NumbersPage(props: Props) {
12
12
  const {onClickKey} = props;
13
+ // These keys are arranged sequentially so that tabbing follows numerical order. This
14
+ // allows us to visually mimic a keypad without affecting a11y. The visual order of the
15
+ // keys in the keypad is determined by their coordinates, not their order in the DOM.
13
16
  return (
14
17
  <>
15
- {/* Row 1 */}
18
+ {/* Row 4 */}
16
19
  <KeypadButton
17
- keyConfig={Keys.NUM_7}
20
+ keyConfig={Keys.NUM_1}
18
21
  onClickKey={onClickKey}
19
- coord={[0, 0]}
22
+ coord={[0, 2]}
20
23
  />
21
24
  <KeypadButton
22
- keyConfig={Keys.NUM_8}
25
+ keyConfig={Keys.NUM_2}
23
26
  onClickKey={onClickKey}
24
- coord={[1, 0]}
27
+ coord={[1, 2]}
25
28
  />
26
29
  <KeypadButton
27
- keyConfig={Keys.NUM_9}
30
+ keyConfig={Keys.NUM_3}
28
31
  onClickKey={onClickKey}
29
- coord={[2, 0]}
32
+ coord={[2, 2]}
30
33
  />
31
34
 
32
- {/* Row 2 */}
35
+ {/* Row 3 */}
33
36
  <KeypadButton
34
37
  keyConfig={Keys.NUM_4}
35
38
  onClickKey={onClickKey}
@@ -46,24 +49,24 @@ export default function NumbersPage(props: Props) {
46
49
  coord={[2, 1]}
47
50
  />
48
51
 
49
- {/* Row 3 */}
52
+ {/* Row 2 */}
50
53
  <KeypadButton
51
- keyConfig={Keys.NUM_1}
54
+ keyConfig={Keys.NUM_7}
52
55
  onClickKey={onClickKey}
53
- coord={[0, 2]}
56
+ coord={[0, 0]}
54
57
  />
55
58
  <KeypadButton
56
- keyConfig={Keys.NUM_2}
59
+ keyConfig={Keys.NUM_8}
57
60
  onClickKey={onClickKey}
58
- coord={[1, 2]}
61
+ coord={[1, 0]}
59
62
  />
60
63
  <KeypadButton
61
- keyConfig={Keys.NUM_3}
64
+ keyConfig={Keys.NUM_9}
62
65
  onClickKey={onClickKey}
63
- coord={[2, 2]}
66
+ coord={[2, 0]}
64
67
  />
65
68
 
66
- {/* Row 4 */}
69
+ {/* Row 1 */}
67
70
  <KeypadButton
68
71
  keyConfig={Keys.NUM_0}
69
72
  onClickKey={onClickKey}
@@ -79,6 +82,12 @@ export default function NumbersPage(props: Props) {
79
82
  onClickKey={onClickKey}
80
83
  coord={[2, 3]}
81
84
  />
85
+ <KeypadButton
86
+ keyConfig={Keys.PERCENT}
87
+ onClickKey={onClickKey}
88
+ coord={[3, 0]}
89
+ secondary
90
+ />
82
91
  </>
83
92
  );
84
93
  }
@@ -2,21 +2,61 @@ import * as React from "react";
2
2
 
3
3
  import Keys from "../../data/key-configs";
4
4
  import {ClickKeyCallback} from "../../types";
5
+ import {CursorContext} from "../input/cursor-contexts";
6
+ import {TabbarItemType} from "../tabbar/types";
5
7
 
6
8
  import {KeypadButton} from "./keypad-button";
7
9
 
8
10
  type Props = {
9
11
  onClickKey: ClickKeyCallback;
12
+ selectedPage: TabbarItemType;
13
+ cursorContext?: CursorContext;
10
14
  multiplicationDot?: boolean;
11
15
  divisionKey?: boolean;
12
16
  };
13
17
 
18
+ function getCursorContextConfig(cursorContext?: CursorContext) {
19
+ if (!cursorContext) {
20
+ return null;
21
+ }
22
+
23
+ switch (cursorContext) {
24
+ case CursorContext.NONE:
25
+ return null;
26
+ case CursorContext.IN_PARENS:
27
+ return Keys.JUMP_OUT_PARENTHESES;
28
+ case CursorContext.IN_SUPER_SCRIPT:
29
+ return Keys.JUMP_OUT_EXPONENT;
30
+ case CursorContext.IN_SUB_SCRIPT:
31
+ return Keys.JUMP_OUT_BASE;
32
+ case CursorContext.IN_NUMERATOR:
33
+ return Keys.JUMP_OUT_NUMERATOR;
34
+ case CursorContext.IN_DENOMINATOR:
35
+ return Keys.JUMP_OUT_DENOMINATOR;
36
+ case CursorContext.BEFORE_FRACTION:
37
+ return Keys.JUMP_INTO_NUMERATOR;
38
+ }
39
+ }
40
+
14
41
  export default function SharedKeys(props: Props) {
15
- const {onClickKey, divisionKey, multiplicationDot} = props;
42
+ const {
43
+ onClickKey,
44
+ cursorContext,
45
+ divisionKey,
46
+ multiplicationDot,
47
+ selectedPage,
48
+ } = props;
49
+
50
+ const cursorKeyConfig = getCursorContextConfig(cursorContext);
51
+
52
+ // Fraction position depends on the page
53
+ const fractionCoord: readonly [number, number] =
54
+ selectedPage === "Numbers" || selectedPage === "Operators"
55
+ ? [3, 1]
56
+ : [3, 0];
16
57
 
17
58
  return (
18
59
  <>
19
- {/* Row 1 */}
20
60
  <KeypadButton
21
61
  keyConfig={Keys.PLUS}
22
62
  onClickKey={onClickKey}
@@ -29,6 +69,12 @@ export default function SharedKeys(props: Props) {
29
69
  coord={[5, 0]}
30
70
  secondary
31
71
  />
72
+ <KeypadButton
73
+ keyConfig={Keys.FRAC_INCLUSIVE}
74
+ onClickKey={onClickKey}
75
+ coord={fractionCoord}
76
+ secondary
77
+ />
32
78
 
33
79
  {/* Row 2 */}
34
80
  <KeypadButton
@@ -61,12 +107,14 @@ export default function SharedKeys(props: Props) {
61
107
  />
62
108
 
63
109
  {/* Row 4 */}
64
- <KeypadButton
65
- keyConfig={Keys.FRAC_INCLUSIVE}
66
- onClickKey={onClickKey}
67
- coord={[4, 3]}
68
- secondary
69
- />
110
+ {cursorKeyConfig && (
111
+ <KeypadButton
112
+ keyConfig={cursorKeyConfig}
113
+ onClickKey={onClickKey}
114
+ coord={[4, 3]}
115
+ secondary
116
+ />
117
+ )}
70
118
  <KeypadButton
71
119
  keyConfig={Keys.BACKSPACE}
72
120
  onClickKey={onClickKey}