fix(web): read the enlarge split from the control, and keep the caret
Two faults in the Enter path, both found in review. The split read `values[fieldName] as string`, a cast over a Record<string, unknown> fed by whatever a server's schema declares. A string field holding a non-string default renders as text and then threw on `.slice`, so one keystroke crashed the panel. Read the control's own value instead: always a string, and the exact string the selection offsets index into. The caret was recorded only when the newline fit, so pressing Enter mid value in a field at its maxLength enlarged without one and threw the caret to the end — a second surprise on top of the newline that did not arrive. Record it on both paths, `start + 1` after an insertion and `start` otherwise. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
c9054ec058
commit
1d3b973742
2 changed files with 53 additions and 10 deletions
|
|
@ -2002,9 +2002,15 @@ describe("SchemaForm enlarge keyboard access (#2138)", () => {
|
|||
// Seeds a value and holds it in real state. A stub `onChange` would leave the
|
||||
// field's value frozen, which makes "the value did not change" assertions
|
||||
// pass whatever the component does.
|
||||
function SeededHarness({ maxLength }: { maxLength?: number }) {
|
||||
function SeededHarness({
|
||||
maxLength,
|
||||
initial = "abc",
|
||||
}: {
|
||||
maxLength?: number;
|
||||
initial?: unknown;
|
||||
}) {
|
||||
const [values, setValues] = useState<Record<string, unknown>>({
|
||||
note: "abc",
|
||||
note: initial,
|
||||
});
|
||||
return (
|
||||
<SchemaForm
|
||||
|
|
@ -2252,6 +2258,34 @@ describe("SchemaForm enlarge keyboard access (#2138)", () => {
|
|||
expect(noteField().value).toBe("a\n");
|
||||
});
|
||||
|
||||
// `values` is a Record<string, unknown> fed by whatever a server declares, so
|
||||
// a string field can arrive holding a number. It renders as text, and slicing
|
||||
// it as a string would throw — turning one keystroke into a crashed panel
|
||||
// (#2139 review). Reading the control's own value keeps it a string.
|
||||
it("survives a non-string value on a string field", () => {
|
||||
renderWithMantine(<SeededHarness initial={123} />);
|
||||
|
||||
expect(noteField().value).toBe("123");
|
||||
noteField().setSelectionRange(3, 3);
|
||||
fireEvent.keyDown(noteField(), { key: "Enter" });
|
||||
|
||||
expect(noteField().tagName).toBe("TEXTAREA");
|
||||
expect(noteField().value).toBe("123\n");
|
||||
});
|
||||
|
||||
// Even when the newline does not fit, the keyboard had a real caret position
|
||||
// and it is kept — otherwise the user editing the middle of a full field is
|
||||
// thrown to the end on top of not getting their newline.
|
||||
it("keeps the caret where it was when the newline does not fit", () => {
|
||||
renderWithMantine(<SeededHarness maxLength={3} />);
|
||||
|
||||
noteField().setSelectionRange(1, 1);
|
||||
fireEvent.keyDown(noteField(), { key: "Enter" });
|
||||
|
||||
expect(noteField().value).toBe("abc");
|
||||
expect(noteField().selectionStart).toBe(1);
|
||||
});
|
||||
|
||||
// The control: the same seeded field with no maxLength does take the newline,
|
||||
// so the test above is showing the constraint at work rather than a field
|
||||
// that never accepts one.
|
||||
|
|
|
|||
|
|
@ -762,7 +762,14 @@ export function SchemaForm({
|
|||
// A field with no room left for the newline is enlarged without one,
|
||||
// since the alternative is breaching a maxLength the schema states.
|
||||
const enlargeWithNewline = (input: HTMLInputElement) => {
|
||||
const current = (rawValue as string) ?? "";
|
||||
// The control's own value, not the form's. Two reasons, and the second
|
||||
// is the one that bites: it is the exact string the selection offsets
|
||||
// below index into, and it is always a string. `values` is a
|
||||
// `Record<string, unknown>` fed by whatever a server's schema declares,
|
||||
// so a non-string default on a string field (`default: 123`) arrives
|
||||
// here as a number — it renders as text, and would then throw on
|
||||
// `.slice`, turning a keystroke into a crashed panel.
|
||||
const current = input.value;
|
||||
// A control that cannot report a selection (`selectionStart` is null
|
||||
// for some input types) is treated as a caret at the end.
|
||||
const start = input.selectionStart ?? current.length;
|
||||
|
|
@ -771,13 +778,15 @@ export function SchemaForm({
|
|||
const fits =
|
||||
fieldSchema.maxLength === undefined ||
|
||||
next.length <= fieldSchema.maxLength;
|
||||
if (fits) {
|
||||
handleFieldChange(fieldName, next);
|
||||
setEnlargeCarets((previous) => ({
|
||||
...previous,
|
||||
[fieldName]: start + 1,
|
||||
}));
|
||||
}
|
||||
if (fits) handleFieldChange(fieldName, next);
|
||||
// The caret is recorded either way. The keyboard route always has a
|
||||
// real position, so dropping it when the newline does not fit would
|
||||
// send the caret to the end of a field the user was editing the middle
|
||||
// of — a second surprise on top of the newline they did not get.
|
||||
setEnlargeCarets((previous) => ({
|
||||
...previous,
|
||||
[fieldName]: fits ? start + 1 : start,
|
||||
}));
|
||||
enlarge();
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue