Skip to content

Add support for centered NumericInputs#3731

Open
handeyeco wants to merge 15 commits into
mainfrom
LEMS-4144/center-ni
Open

Add support for centered NumericInputs#3731
handeyeco wants to merge 15 commits into
mainfrom
LEMS-4144/center-ni

Conversation

@handeyeco
Copy link
Copy Markdown
Contributor

@handeyeco handeyeco commented Jun 5, 2026

Summary:

Go schema change: https://github.com/Khan/webapp/pull/39970

This PR adds support for centered NumericInputs. This required a change to the schema for NumericInput's widgetOptions:

- rightAlign?: Boolean;
+ textAlign: "start" | "end" | "center";

This meant that I needed to update the widget, editor, parser, stories, and tests. Ultimately it's a small change to the UI and a lot of changes to our safety nets.

I decided to go with start/end vs left/right to set us up for i18n in the future. However maybe we want to keep left/right and we can add start/end later? That would let content creators pick: "this needs to be on the left" or "this can be at the start of whatever the locale wants".

Issue: LEMS-4144

Test plan:

Screenshot 2026-06-05 at 1 14 24 PM Screenshot 2026-06-05 at 2 28 53 PM

Most of this PR is adding/updating tests including:

  • Adding a story here: ?path=/story/widgets-numeric-input-visual-regression-tests-initial-state--center-text-align
  • Making the control interactive here: ?path=/docs/widgets-numeric-input--docs

@handeyeco handeyeco self-assigned this Jun 5, 2026
@handeyeco handeyeco changed the title fix Storybook control Add support for centered NumericInputs Jun 5, 2026
@github-actions github-actions Bot added item-splitting-change schema-change Attached to PRs when we detect Perseus Schema changes in it labels Jun 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

Size Change: +400 B (+0.08%)

Total Size: 508 kB

📦 View Changed
Filename Size Change
packages/perseus-core/dist/es/index.item-splitting.js 12.2 kB +124 B (+1.03%)
packages/perseus-core/dist/es/index.js 26.4 kB +161 B (+0.61%)
packages/perseus-editor/dist/es/index.js 105 kB +41 B (+0.04%)
packages/perseus/dist/es/index.js 200 kB +74 B (+0.04%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.32 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-linter/dist/es/index.js 9.65 kB
packages/perseus-score/dist/es/index.js 10.2 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/strings.js 8.6 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

});
});

describe("textAlign", () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests to make sure:

  1. We can migrate from rightAlign to textAlign
  2. We can parse textAlign correctly

}),
);

function migrateV0ToV1(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing rightAlign with textAlign is a major change and requires a migration.

coefficient: false,
labelText: "",
size: "normal",
textAlign: "center",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test to make sure we can parse the new textAlign

]);
});

it("converts alignment when rightAlign is undefined", () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure IN-to-NI still works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greatly appreciate you adding these tests for us!

});

it("should be possible to select right alignment", async () => {
it("should be possible to change text alignment", async () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test to make sure the editor can select the new options

};

// Verifies the center-aligned text input variant with a pre-filled value — the value should appear in the center
export const CenterTextAlign: Story = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visual regression test for centered NIs.

"coefficient": false,
"labelText": "",
"size": "normal",
"textAlign": "center",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the important bit here.

"coefficient": false,
"labelText": "",
"size": "normal",
"textAlign": "end",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the expected transformation of the legacy schema: rightAlign: true -> textAlign: end.

@handeyeco handeyeco marked this pull request as ready for review June 5, 2026 19:06
@handeyeco handeyeco added schema-change-ack Acknowledges that this PR's data-schema change has been reviewed by Content Platform item-splitting-change-ack Acknowledges that this PR's item-splitting bundle change has been reviewed labels Jun 5, 2026
@handeyeco handeyeco requested review from a team June 5, 2026 19:07
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (d550a0d) and published it to npm. You
can install it using the tag PR3731.

Example:

pnpm add @khanacademy/perseus@PR3731

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3731

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3731

coefficient: false,
labelText: "",
size: "normal",
rightAlign: true,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test to make sure we can still parse rightAlign

* How to align the text in the input
* it's "start"/"end" vs "left"/"right" to support i18n in the future
*/
textAlign: "start" | "end" | "center";
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could have done textAlign? but I feel like it's easier to just let the parser provide a default so we always have a value.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of working to reduce optionality in the schema and have the parser fill in details. I think we found that depending on the React widget components do the defaulting is tricky now that we have server-side scoring, etc.

Copy link
Copy Markdown
Collaborator

@jeremywiebe jeremywiebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by comments. I'll leave it to project folks to approve. Thanks for this Matthew!

expect(result.value.options.textAlign).toBe("start");
});

it("migrates from v0 to v1 when textAlign is true", () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it("migrates from v0 to v1 when textAlign is true", () => {
it("migrates from v0 to v1 when rightAlign is true", () => {

* How to align the text in the input
* it's "start"/"end" vs "left"/"right" to support i18n in the future
*/
textAlign: "start" | "end" | "center";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of working to reduce optionality in the schema and have the parser fill in details. I think we found that depending on the React widget components do the defaulting is tricky now that we have server-side scoring, etc.

Comment on lines +460 to +474
const textAlign = (
<label>
Text alignment
<SingleSelect
selectedValue={this.props.textAlign}
onChange={(value) => {
this.props.onChange({textAlign: value});
}}
placeholder="Select text alignment"
>
Right
</Pill>
</fieldset>
<OptionItem value="start" label="Left" />
<OptionItem value="center" label="Center" />
<OptionItem value="end" label="Right" />
</SingleSelect>
</label>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly related, but I'd love to move away from creating variables with snippets of JSX which we then render at some place in favour of bespoke little components.

Although we don't use the React Component view much, modelling these as react components helps them show up much better in that component debugger.

I also think it's a bit more idiomatic as React can manage re-rendering at that level.

🤷‍♂️

Something like this:

function AlignmentSelector(props: {
    value: "left" | "right" | "centre",
    onChange: (alignment: string) => void,
}) {
    return (<label>
                Text alignment
                <SingleSelect
                    selectedValue={this.props.textAlign}
                    onChange={(value) => {
                        this.props.onChange({textAlign: value});
                    }}
                    placeholder="Select text alignment"
                >
                    <OptionItem value="start" label="Left" />
                    <OptionItem value="center" label="Center" />
                    <OptionItem value="end" label="Right" />
                </SingleSelect>
            </label>)
}

And then ...

<AlignmentSelector 
    value={this.props.textAlign} 
    onChange={(alignment) => {   
        this.props.onChange({textAlign: alignment}); 
    } 
/>

Comment on lines 50 to 58
#answercontent input[type="text"].perseus-input-right-align,
#answercontent input[type="number"].perseus-input-right-align,
.framework-perseus input[type="text"].perseus-input-right-align,
.framework-perseus input[type="number"].perseus-input-right-align {
text-align: right;
}
.framework-perseus.perseus-mobile .perseus-input-right-align .keypad-input {
text-align: right;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be updated? Like .perseus-input-end-align or something?

static defaultProps: DefaultProps = {
size: "normal",
rightAlign: false,
textAlign: "start",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the widget options don't make this optional, can this prop be made required here and avoid needing a default?

padding: 0.4rem;
}

.input-with-examples.right-align {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this .end-align now?

Copy link
Copy Markdown
Contributor

@SonicScrewdriver SonicScrewdriver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally looks great to me! I just had a comment for numeric-input.stories.tsx, and there's a couple things Jeremy pointed out that we should fix up with the styles. (I'm not sure if IN/NI widgets share any styles with the Expression widget, but hopefully not.)

coefficient: false,
userInput: {currentValue: ""},
rightAlign: false,
alignment: "start",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be textAlign?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

item-splitting-change item-splitting-change-ack Acknowledges that this PR's item-splitting bundle change has been reviewed olc-5.0.f429d schema-change Attached to PRs when we detect Perseus Schema changes in it schema-change-ack Acknowledges that this PR's data-schema change has been reviewed by Content Platform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants