Skip to content

Commit 46623c8

Browse files
authoredJan 27, 2025··
[Numeric Input] Update UI of editor (#2015)
## Summary: Many of the editor functions for Numeric Input were hidden behind a small configuration icon button, which was unintuitive. Also, the tooltip icons for the settings didn't align with their setting, making it difficult to understand where to get help for the options. To fix these issues, and to modernize the UI, the editor screen was re-organized to make all functions easily findable. Also, Wonder Blocks components were used instead of custom items to make the editor experience consistent with other parts of the app. Issue: LEMS-2456 ## Test plan: 1. Open the [Editor Demo page](https://650db21c3f5d1b2f13c02952-ptwmwtryzl.chromatic.com/?path=/story/perseuseditor-editorpage--demo) in Storybook. 2. Add a Numeric Input widget to the page. 3. Using the revised UI, all settings options function just like with the original editor. ## Affected behavior: ### Before ![Editor - Before](https://github.com/user-attachments/assets/6c76991d-fe61-4c27-97a4-e2ca6967bfd5) ### After ![Editor - After](https://github.com/user-attachments/assets/27846422-6233-4884-b8f4-1a7d9b8eba47) Author: mark-fitzgerald Reviewers: SonicScrewdriver, mark-fitzgerald Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x) Pull Request URL: #2015
1 parent 29a1c65 commit 46623c8

9 files changed

+646
-429
lines changed
 

‎.changeset/flat-peaches-know.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-editor": minor
3+
---
4+
5+
[Numeric Input] Re-organize editor and improve its UI

‎.changeset/nice-turkeys-dress.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-editor": minor
3+
---
4+
5+
[Numeric Input] - Adjust editor to organize settings more logically

‎.eslintrc.js

+3
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ module.exports = {
324324
"react/no-string-refs": "off", // on in react/recommended, but we have #legacy-code
325325
"react/no-find-dom-node": "off", // on in react/recommended, but we have #legacy-code
326326
"react/display-name": "off", // on in react/recommended, but doesn't seem that useful to fix
327+
// On in react/recommended, but doesn't seem helpful
328+
// (requires quotes to be escaped to catch developer mistakes when other characters are misplaced)
329+
"react/no-unescaped-entities": "off",
327330
// This rule results in false-positives when using some types of React
328331
// components (such as functional components or hooks). Since
329332
// TypeScript is already checking that components are only using props

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@khanacademy/mathjax-renderer": "^2.1.1",
3333
"@khanacademy/wonder-blocks-button": "7.0.5",
3434
"@khanacademy/wonder-blocks-layout": "3.0.5",
35+
"@khanacademy/wonder-blocks-pill": "3.0.5",
3536
"@khanacademy/wonder-blocks-spacing": "^4.0.1",
3637
"@popperjs/core": "^2.10.2",
3738
"@rollup/plugin-alias": "^3.1.9",

‎packages/perseus-editor/src/components/heading.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const styles = StyleSheet.create({
5151
marginInline: -10,
5252
backgroundColor: color.offBlack8,
5353
padding: spacing.xSmall_8,
54+
width: "calc(100% + 20px)",
5455
},
5556
heading: {
5657
flexDirection: "row",

‎packages/perseus-editor/src/components/perseus-editor-accordion.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as React from "react";
77
import type {StyleType} from "@khanacademy/wonder-blocks-core";
88

99
type Props = {
10+
animated?: boolean;
1011
children: React.ReactNode | React.ReactNode[];
1112
header: string | React.ReactElement;
1213
expanded?: boolean;
@@ -16,8 +17,15 @@ type Props = {
1617
};
1718

1819
const PerseusEditorAccordion = (props: Props) => {
19-
const {children, header, expanded, containerStyle, panelStyle, onToggle} =
20-
props;
20+
const {
21+
animated,
22+
children,
23+
header,
24+
expanded,
25+
containerStyle,
26+
panelStyle,
27+
onToggle,
28+
} = props;
2129

2230
return (
2331
<View
@@ -27,6 +35,7 @@ const PerseusEditorAccordion = (props: Props) => {
2735
className="perseus-editor-accordion"
2836
>
2937
<AccordionSection
38+
animated={animated}
3039
expanded={expanded}
3140
onToggle={onToggle}
3241
style={[styles.container, containerStyle]}

‎packages/perseus-editor/src/styles/perseus-editor.less

+111-40
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,33 @@
218218
&.leave {
219219
display: none;
220220
}
221+
222+
.inline-options {
223+
float: inline-start; /* flexbox and inline-block don't work on <legend> elements, so going old-school here */
224+
line-height: 24px; /* for alignment with items in same line (like pills or buttons) */
225+
padding-inline-end: 0.5em;
226+
}
227+
228+
.tooltip-for-legend {
229+
display: inline-block;
230+
line-height: 24px;
231+
}
221232
}
222233

223234
// Are any widgets capable of overflowing in the editor interface?
224235
.categorizer-container {
225236
overflow-x: scroll;
226237
}
238+
239+
.section-accordion {
240+
display: flex;
241+
flex-direction: row;
242+
}
243+
244+
.delete-item-button {
245+
align-self: center;
246+
padding-right: 0.5em;
247+
}
227248
}
228249

229250
.perseus-widget-editor-title-id > svg {
@@ -232,6 +253,33 @@
232253
margin-right: 10px;
233254
}
234255

256+
.perseus-editor-accordion-container {
257+
display: inline-grid;
258+
width: 100%;
259+
260+
&.collapsed {
261+
grid-template-rows: 0fr;
262+
min-height: 0;
263+
visibility: hidden;
264+
transition:
265+
all 0.25s step-end,
266+
grid-template-rows 0.25s;
267+
}
268+
269+
&.expanded {
270+
grid-template-rows: 1fr;
271+
min-height: 100%;
272+
visibility: visible;
273+
transition: grid-template-rows 0.5s;
274+
}
275+
276+
.perseus-editor-accordion-content {
277+
overflow: hidden;
278+
margin: 0 -1px; /* allows focus ring on accordion to show */
279+
padding: 0 1px;
280+
}
281+
}
282+
235283
.perseus-editor-widgets-selectors {
236284
background-color: @grayExtraLight;
237285
border: 1px solid @grayLighter;
@@ -538,25 +586,27 @@
538586
// Input Number / Text Input
539587
//
540588
.perseus-input-number-editor {
541-
font-size: 14px;
542-
543-
.ui-title,
544-
.msg-title {
545-
display: inline-block;
546-
text-align: center;
547-
}
548-
549-
.ui-title {
550-
width: 100px;
551-
}
552-
553-
.msg-title {
554-
margin-left: 5px;
555-
width: 230px;
589+
font-family: Lato, "Noto Sans", sans-serif;
590+
font-weight: 400;
591+
font-size: 16px;
592+
line-height: 20px;
593+
594+
.answer-option {
595+
.unsimplified-options {
596+
min-height: 48px;
597+
}
556598
}
557599

558-
.options-container {
559-
padding-left: 30px;
600+
.perseus-textarea-pair {
601+
font-size: 16px;
602+
.perseus-textarea-underlay {
603+
margin-bottom: 26px;
604+
}
605+
textarea {
606+
background-color: #ffffff;
607+
border: 1px solid rgba(33, 36, 44, 0.5);
608+
border-radius: 4px;
609+
}
560610
}
561611

562612
.input-answer-editor-value,
@@ -565,38 +615,59 @@
565615
}
566616

567617
.input-answer-editor-value-container {
568-
border: @widgetBorder;
569-
border-radius: @widgetBorderRadius;
570-
float: left;
571-
.size(100px, 53px);
572-
overflow: hidden;
573-
position: relative;
618+
display: block;
619+
620+
input {
621+
background: #ffffff;
622+
border: 1px solid rgba(33, 36, 44, 0.5);
623+
border-radius: 4px;
624+
color: #21242c;
625+
font-family: Lato, "Noto Sans", sans-serif;
626+
font-weight: 400;
627+
font-size: 16px;
628+
line-height: 20px;
629+
outline-offset: -2px;
630+
}
574631

575632
.numeric-input-value {
576-
border: 0;
577-
font-size: 13px;
578-
outline-offset: -3px;
579-
width: 100%;
633+
margin-left: 8px;
634+
width: 6em;
635+
}
636+
637+
.max-error-input-value {
638+
display: none;
639+
width: 3em;
640+
}
641+
642+
.max-error-plusmn {
643+
cursor: default;
644+
display: none;
645+
height: 32px;
646+
padding-top: 4px;
647+
text-align: center;
648+
vertical-align: top;
649+
width: 1em;
580650
}
581651

582-
&.with-max-error {
652+
&.with-max-error,
653+
&:focus-within {
583654
.numeric-input-value {
584-
width: 60%;
655+
border-right: none;
656+
border-top-right-radius: 0;
657+
border-bottom-right-radius: 0;
585658
}
586-
}
587659

588-
.max-error-container {
589-
display: inline-block;
590-
width: 40%;
591-
.max-error-plusmn {
592-
cursor: default;
660+
.max-error-input-value {
661+
border-left: none;
662+
border-top-left-radius: 0;
663+
border-bottom-left-radius: 0;
593664
display: inline-block;
594-
width: 20%;
595665
}
596-
.number-input {
597-
border: 0;
598-
font-size: 13px;
599-
width: 80%;
666+
667+
.max-error-plusmn {
668+
border-top: 1px solid rgba(33, 36, 44, 0.5);
669+
border-bottom: 1px solid rgba(33, 36, 44, 0.5);
670+
display: inline-block;
600671
}
601672
}
602673
}

‎packages/perseus-editor/src/widgets/__tests__/numeric-input-editor.test.tsx

+30-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Dependencies} from "@khanacademy/perseus";
2-
import {render, screen, waitFor} from "@testing-library/react";
2+
import {render, screen, waitFor, within} from "@testing-library/react";
33
import {userEvent as userEventLib} from "@testing-library/user-event";
44
import * as React from "react";
55

@@ -36,7 +36,10 @@ describe("numeric-input-editor", () => {
3636
render(<NumericInputEditor onChange={onChangeMock} />);
3737

3838
await userEvent.click(
39-
screen.getByRole("button", {name: "Normal (80px)"}),
39+
within(screen.getByRole("group", {name: /^Width/})).getByRole(
40+
"radio",
41+
{name: "Normal (80px)"},
42+
),
4043
);
4144

4245
expect(onChangeMock).toBeCalledWith(
@@ -51,7 +54,10 @@ describe("numeric-input-editor", () => {
5154
render(<NumericInputEditor onChange={onChangeMock} />);
5255

5356
await userEvent.click(
54-
screen.getByRole("button", {name: "Small (40px)"}),
57+
within(screen.getByRole("group", {name: /^Width/})).getByRole(
58+
"radio",
59+
{name: "Small (40px)"},
60+
),
5561
);
5662

5763
expect(onChangeMock).toBeCalledWith(
@@ -66,7 +72,10 @@ describe("numeric-input-editor", () => {
6672
render(<NumericInputEditor onChange={onChangeMock} />);
6773

6874
await userEvent.click(
69-
screen.getByRole("checkbox", {name: "Right alignment"}),
75+
within(screen.getByRole("group", {name: /^Alignment/})).getByRole(
76+
"radio",
77+
{name: "Right"},
78+
),
7079
);
7180

7281
expect(onChangeMock).toBeCalledWith({rightAlign: true});
@@ -78,7 +87,9 @@ describe("numeric-input-editor", () => {
7887
render(<NumericInputEditor onChange={onChangeMock} />);
7988

8089
await userEvent.click(
81-
screen.getByRole("checkbox", {name: "Coefficient"}),
90+
within(
91+
screen.getByRole("group", {name: /^Number style/}),
92+
).getByRole("radio", {name: "Coefficient"}),
8293
);
8394

8495
expect(onChangeMock).toBeCalledWith({coefficient: true});
@@ -89,11 +100,10 @@ describe("numeric-input-editor", () => {
89100

90101
render(<NumericInputEditor onChange={onChangeMock} />);
91102

92-
await userEvent.click(screen.getByLabelText("Toggle options"));
93103
await userEvent.click(
94-
screen.getByRole("checkbox", {
95-
name: "Strictly match only these formats",
96-
}),
104+
within(
105+
screen.getByRole("group", {name: /^Answer formats are/}),
106+
).getByRole("radio", {name: "Required"}),
97107
);
98108

99109
expect(onChangeMock).toBeCalledWith({
@@ -117,7 +127,7 @@ describe("numeric-input-editor", () => {
117127
render(<NumericInputEditor onChange={onChangeMock} />);
118128

119129
const input = screen.getByRole("textbox", {
120-
name: "Aria label",
130+
name: "aria label",
121131
});
122132

123133
await userEvent.type(input, "a");
@@ -128,27 +138,16 @@ describe("numeric-input-editor", () => {
128138
);
129139
});
130140

131-
it("should be possible to toggle options", async () => {
132-
render(<NumericInputEditor onChange={() => {}} />);
133-
134-
await userEvent.click(
135-
screen.getByRole("link", {name: "Toggle options"}),
136-
);
137-
138-
expect(
139-
screen.getByText("Unsimplified answers are"),
140-
).toBeInTheDocument();
141-
});
142-
143141
it("should be possible to set unsimplified answers to ungraded", async () => {
144142
const onChangeMock = jest.fn();
145143

146144
render(<NumericInputEditor onChange={onChangeMock} />);
147145

148146
await userEvent.click(
149-
screen.getByRole("link", {name: "Toggle options"}),
147+
within(
148+
screen.getByRole("group", {name: /^Unsimplified answers are/}),
149+
).getByRole("radio", {name: "Ungraded"}),
150150
);
151-
await userEvent.click(screen.getByRole("button", {name: "ungraded"}));
152151

153152
expect(onChangeMock).toBeCalledWith(
154153
expect.objectContaining({
@@ -165,9 +164,10 @@ describe("numeric-input-editor", () => {
165164
render(<NumericInputEditor onChange={onChangeMock} />);
166165

167166
await userEvent.click(
168-
screen.getByRole("link", {name: "Toggle options"}),
167+
within(
168+
screen.getByRole("group", {name: /^Unsimplified answers are/}),
169+
).getByRole("radio", {name: "Accepted"}),
169170
);
170-
await userEvent.click(screen.getByRole("button", {name: "accepted"}));
171171

172172
expect(onChangeMock).toBeCalledWith(
173173
expect.objectContaining({
@@ -184,9 +184,10 @@ describe("numeric-input-editor", () => {
184184
render(<NumericInputEditor onChange={onChangeMock} />);
185185

186186
await userEvent.click(
187-
screen.getByRole("link", {name: "Toggle options"}),
187+
within(
188+
screen.getByRole("group", {name: /^Unsimplified answers are/}),
189+
).getByRole("radio", {name: "Wrong"}),
188190
);
189-
await userEvent.click(screen.getByRole("button", {name: "wrong"}));
190191

191192
expect(onChangeMock).toBeCalledWith(
192193
expect.objectContaining({
@@ -212,10 +213,7 @@ describe("numeric-input-editor", () => {
212213

213214
render(<NumericInputEditor onChange={onChangeMock} />);
214215

215-
await userEvent.click(
216-
screen.getByRole("link", {name: "Toggle options"}),
217-
);
218-
await userEvent.click(screen.getByTitle(name));
216+
await userEvent.click(screen.getByRole("checkbox", {name: name}));
219217

220218
expect(onChangeMock).toBeCalledWith(
221219
expect.objectContaining({

‎packages/perseus-editor/src/widgets/numeric-input-editor.tsx

+479-355
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.