From c9e7db20c403c9b3a5a7e817822d310f0e92eafd Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Tue, 4 Aug 2026 15:18:39 +0300 Subject: [PATCH 1/4] nosubmit button --- CHANGELOG.md | 1 + Input/NumberField.js | 8 ++--- Input/tests/Input-specs.js | 70 +++++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 330176d7b..526ccdc56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The following is a curated list of changes in the Enact limestone module, newest - `limestone/Alert` text container to fit the width of its text and center-align - `limestone/Chips.Chip` styling to match the latest GUI +- `limestone/Input` to show the submit button for separated number inputs by default ## [1.10.2] - 2026-07-09 diff --git a/Input/NumberField.js b/Input/NumberField.js index e62b834df..d744ff652 100644 --- a/Input/NumberField.js +++ b/Input/NumberField.js @@ -111,10 +111,10 @@ const NumberFieldBase = kind({ forwardCustomWithPrevent('onBeforeChange', ({value}) => ({value})), forwardCustom('onChange', (ev) => (ev)), // Check the length of the new value and return true (pass/proceed) if it is at or above max-length - ({value: updatedValue}, {maxLength, minLength, numberInputField}) => { + ({value: updatedValue}, {maxLength, minLength, noSubmitButton, numberInputField}) => { const updatedLength = normalizeValue(updatedValue, maxLength).length, - autoSubmit = getSeparated(numberInputField, maxLength) && minLength === maxLength; + autoSubmit = noSubmitButton && getSeparated(numberInputField, maxLength) && minLength === maxLength; return autoSubmit && updatedLength >= maxLength; }, forwardCustom('onComplete', (ev) => (ev)) @@ -155,10 +155,10 @@ const NumberFieldBase = kind({ ); } }, - submitButton: ({buttonSize, css, disabled, invalid, maxLength, minLength, noSubmitButton, onSubmit, value, numberInputField}) => { + submitButton: ({buttonSize, css, disabled, invalid, maxLength, minLength, noSubmitButton, onSubmit, value}) => { const isDisabled = disabled || invalid || (normalizeValue(value, maxLength).toString().length < minLength); - if (!noSubmitButton && (minLength !== maxLength || !getSeparated(numberInputField, maxLength))) { + if (!noSubmitButton) { return ; } else { return null; diff --git a/Input/tests/Input-specs.js b/Input/tests/Input-specs.js index 4d74d00f1..0fc35014e 100644 --- a/Input/tests/Input-specs.js +++ b/Input/tests/Input-specs.js @@ -320,28 +320,70 @@ describe('Input specs', () => { expect(buttonSubmit).not.toBeNull(); }); - test('should exclude a submit button when separated number input', () => { + test('should include a submit button when separated number input', () => { render( ); - const buttonSubmit = screen.queryByText('Submit'); + const buttonSubmit = screen.getByText('Submit'); - expect(buttonSubmit).toBeNull(); + expect(buttonSubmit).not.toBeNull(); }); - test('should exclude a submit button for explicit separated number input', () => { + test('should include a submit button for explicit separated number input', () => { render( ); + const buttonSubmit = screen.getByText('Submit'); + + expect(buttonSubmit).not.toBeNull(); + }); + + test('should exclude a submit button for separated number input when noSubmitButton is used', () => { + render( + + + + ); const buttonSubmit = screen.queryByText('Submit'); expect(buttonSubmit).toBeNull(); }); + test('should include a submit button for passwordnumber separated input', () => { + render( + + + + ); + const buttonSubmit = screen.getByText('Submit'); + + expect(buttonSubmit).not.toBeNull(); + }); + + test('should not call onComplete when max length is reached for separated number input with submit button', async () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); + render( + + + + ); + const numberButton = screen.getByText('2'); + + await user.click(numberButton); + + act(() => jest.advanceTimersByTime(300)); + + expect(spy).not.toHaveBeenCalled(); + + jest.useRealTimers(); + }); + test('should show an invalid tooltip if invalid and message supplied', () => { render( @@ -522,6 +564,26 @@ describe('Input specs', () => { expect(buttonSubmit).toBeNull(); }); + + test('should call onComplete when max length is reached for separated number input with noSubmitButton', async () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); + render( + + + + ); + const numberButton = screen.getByText('2'); + + await user.click(numberButton); + + act(() => jest.advanceTimersByTime(300)); + + expect(spy).toHaveBeenCalled(); + + jest.useRealTimers(); + }); }); describe('marqueeInputField', () => { From b8d80f988f553d62722d52c525a2ed1f57fe9e8f Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Wed, 5 Aug 2026 10:44:01 +0300 Subject: [PATCH 2/4] fix ui tests --- Input/NumberField.js | 4 ++-- Input/tests/Input-specs.js | 23 ++--------------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/Input/NumberField.js b/Input/NumberField.js index d744ff652..63a584383 100644 --- a/Input/NumberField.js +++ b/Input/NumberField.js @@ -111,10 +111,10 @@ const NumberFieldBase = kind({ forwardCustomWithPrevent('onBeforeChange', ({value}) => ({value})), forwardCustom('onChange', (ev) => (ev)), // Check the length of the new value and return true (pass/proceed) if it is at or above max-length - ({value: updatedValue}, {maxLength, minLength, noSubmitButton, numberInputField}) => { + ({value: updatedValue}, {maxLength, minLength, numberInputField}) => { const updatedLength = normalizeValue(updatedValue, maxLength).length, - autoSubmit = noSubmitButton && getSeparated(numberInputField, maxLength) && minLength === maxLength; + autoSubmit = getSeparated(numberInputField, maxLength) && minLength === maxLength; return autoSubmit && updatedLength >= maxLength; }, forwardCustom('onComplete', (ev) => (ev)) diff --git a/Input/tests/Input-specs.js b/Input/tests/Input-specs.js index 0fc35014e..79f67ede4 100644 --- a/Input/tests/Input-specs.js +++ b/Input/tests/Input-specs.js @@ -364,7 +364,7 @@ describe('Input specs', () => { expect(buttonSubmit).not.toBeNull(); }); - test('should not call onComplete when max length is reached for separated number input with submit button', async () => { + test('should call onComplete when max length is reached for separated number input', async () => { jest.useFakeTimers(); const spy = jest.fn(); const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); @@ -379,7 +379,7 @@ describe('Input specs', () => { act(() => jest.advanceTimersByTime(300)); - expect(spy).not.toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); jest.useRealTimers(); }); @@ -565,25 +565,6 @@ describe('Input specs', () => { expect(buttonSubmit).toBeNull(); }); - test('should call onComplete when max length is reached for separated number input with noSubmitButton', async () => { - jest.useFakeTimers(); - const spy = jest.fn(); - const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); - render( - - - - ); - const numberButton = screen.getByText('2'); - - await user.click(numberButton); - - act(() => jest.advanceTimersByTime(300)); - - expect(spy).toHaveBeenCalled(); - - jest.useRealTimers(); - }); }); describe('marqueeInputField', () => { From 944ed39ab564fa15642954fd8ecce14309ae9d66 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Wed, 5 Aug 2026 11:14:12 +0300 Subject: [PATCH 3/4] fix auto submit when button displayed --- Input/Input.js | 7 +++++-- Input/NumberField.js | 5 +++-- Input/tests/Input-specs.js | 22 +++++++++++++++++++++- tests/ui/specs/Input/Input-specs.js | 14 ++++++++++++++ tests/ui/specs/Input/InputPage.js | 4 ++++ 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/Input/Input.js b/Input/Input.js index c79cb4530..9d9523157 100644 --- a/Input/Input.js +++ b/Input/Input.js @@ -166,8 +166,8 @@ const InputPopupBase = kind({ * * Overridden by `length` value. * - * When smaller than `maxLength`, number type inputs will show a submit button and will not - * auto-submit when the length reaches `maxLength`. Defaults to the `maxLength` value. + * Defaults to the `maxLength` value. Values shorter than `minLength` keep the submit button + * disabled. * * @type {Number} * @public @@ -185,6 +185,9 @@ const InputPopupBase = kind({ /** * Omits the submit button. * + * When `true` for separated number inputs where `minLength` equals `maxLength`, the input + * auto-submits when the length reaches `maxLength`. + * * @type {Boolean} * @public */ diff --git a/Input/NumberField.js b/Input/NumberField.js index 63a584383..479cf3c5f 100644 --- a/Input/NumberField.js +++ b/Input/NumberField.js @@ -111,10 +111,11 @@ const NumberFieldBase = kind({ forwardCustomWithPrevent('onBeforeChange', ({value}) => ({value})), forwardCustom('onChange', (ev) => (ev)), // Check the length of the new value and return true (pass/proceed) if it is at or above max-length - ({value: updatedValue}, {maxLength, minLength, numberInputField}) => { + ({value: updatedValue}, {maxLength, minLength, noSubmitButton, numberInputField}) => { const updatedLength = normalizeValue(updatedValue, maxLength).length, - autoSubmit = getSeparated(numberInputField, maxLength) && minLength === maxLength; + // Auto-submit only when the submit button is omitted for separated equal-length fields + autoSubmit = noSubmitButton && getSeparated(numberInputField, maxLength) && minLength === maxLength; return autoSubmit && updatedLength >= maxLength; }, forwardCustom('onComplete', (ev) => (ev)) diff --git a/Input/tests/Input-specs.js b/Input/tests/Input-specs.js index 79f67ede4..90744420b 100644 --- a/Input/tests/Input-specs.js +++ b/Input/tests/Input-specs.js @@ -364,7 +364,7 @@ describe('Input specs', () => { expect(buttonSubmit).not.toBeNull(); }); - test('should call onComplete when max length is reached for separated number input', async () => { + test('should not call onComplete when max length is reached for separated number input with submit button', async () => { jest.useFakeTimers(); const spy = jest.fn(); const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); @@ -379,6 +379,26 @@ describe('Input specs', () => { act(() => jest.advanceTimersByTime(300)); + expect(spy).not.toHaveBeenCalled(); + + jest.useRealTimers(); + }); + + test('should call onComplete when max length is reached for separated number input with noSubmitButton', async () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); + render( + + + + ); + const numberButton = screen.getByText('2'); + + await user.click(numberButton); + + act(() => jest.advanceTimersByTime(300)); + expect(spy).toHaveBeenCalled(); jest.useRealTimers(); diff --git a/tests/ui/specs/Input/Input-specs.js b/tests/ui/specs/Input/Input-specs.js index fdd2d6fba..a8dbb6216 100644 --- a/tests/ui/specs/Input/Input-specs.js +++ b/tests/ui/specs/Input/Input-specs.js @@ -142,6 +142,13 @@ describe('Input test', () => { await Page.spotlightSelect(); await Page.spotlightSelect(); await Page.spotlightSelect(); + + // Submit is required when the button is shown; move focus onto it + for (let i = 0; i < 5 && !(await components.input3.submitButton.isFocused()); i++) { + await Page.spotlightDown(); + } + expect(await components.input3.submitButton.isFocused()).toBe(true); + await Page.spotlightSelect(); await browser.pause(1000); expect(await components.input3.self.isFocused()).toBe(true); @@ -198,6 +205,13 @@ describe('Input test', () => { await Page.spotlightSelect(); await Page.spotlightSelect(); await Page.spotlightSelect(); + + // Submit is required when the button is shown; move focus onto it + for (let i = 0; i < 5 && !(await components.input4.submitButton.isFocused()); i++) { + await Page.spotlightDown(); + } + expect(await components.input4.submitButton.isFocused()).toBe(true); + await Page.spotlightSelect(); await browser.pause(1000); expect(await components.input4.self.isFocused()).toBe(true); diff --git a/tests/ui/specs/Input/InputPage.js b/tests/ui/specs/Input/InputPage.js index 61269ebb1..002e1f619 100644 --- a/tests/ui/specs/Input/InputPage.js +++ b/tests/ui/specs/Input/InputPage.js @@ -27,6 +27,10 @@ class InputInterface { return element('.Input_Input_numberCell', browser); } + get submitButton () { + return element('.Input_Input_submitButton', browser); + } + get title () { return element('.Input_Input_titles', browser); } From 603e30793805483260f0ebf1a034c86ee07fb0a8 Mon Sep 17 00:00:00 2001 From: Dan Ichim Date: Tue, 11 Aug 2026 10:17:05 +0300 Subject: [PATCH 4/4] review changes --- Input/Input.js | 8 ++++++-- Input/tests/Input-specs.js | 23 ++++++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Input/Input.js b/Input/Input.js index 9d9523157..d7d8c0331 100644 --- a/Input/Input.js +++ b/Input/Input.js @@ -166,8 +166,8 @@ const InputPopupBase = kind({ * * Overridden by `length` value. * - * Defaults to the `maxLength` value. Values shorter than `minLength` keep the submit button - * disabled. + * Defaults to the `maxLength` value. The submit button is disabled while the value is + * shorter than `minLength`. * * @type {Number} * @public @@ -188,6 +188,10 @@ const InputPopupBase = kind({ * When `true` for separated number inputs where `minLength` equals `maxLength`, the input * auto-submits when the length reaches `maxLength`. * + * Only use this when auto-submit applies (a separated number input whose `minLength` equals + * `maxLength`); otherwise the input cannot be submitted, as there is neither a submit button + * nor auto-submit. + * * @type {Boolean} * @public */ diff --git a/Input/tests/Input-specs.js b/Input/tests/Input-specs.js index 90744420b..b541c4942 100644 --- a/Input/tests/Input-specs.js +++ b/Input/tests/Input-specs.js @@ -384,6 +384,28 @@ describe('Input specs', () => { jest.useRealTimers(); }); + test('should call onComplete when submit button clicked for separated number input with equal min and max length', async () => { + jest.useFakeTimers(); + const spy = jest.fn(); + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); + render( + + + + ); + const numberButton = screen.getByText('2'); + const submitButton = screen.getByText('Submit'); + + await user.click(numberButton); + await user.click(submitButton); + + act(() => jest.advanceTimersByTime(300)); + + expect(spy).toHaveBeenCalled(); + + jest.useRealTimers(); + }); + test('should call onComplete when max length is reached for separated number input with noSubmitButton', async () => { jest.useFakeTimers(); const spy = jest.fn(); @@ -584,7 +606,6 @@ describe('Input specs', () => { expect(buttonSubmit).toBeNull(); }); - }); describe('marqueeInputField', () => {