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/Input.js b/Input/Input.js index c79cb4530..d7d8c0331 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. The submit button is disabled while the value is + * shorter than `minLength`. * * @type {Number} * @public @@ -185,6 +185,13 @@ 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`. + * + * 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/NumberField.js b/Input/NumberField.js index e62b834df..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)) @@ -155,10 +156,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..b541c4942 100644 --- a/Input/tests/Input-specs.js +++ b/Input/tests/Input-specs.js @@ -320,28 +320,112 @@ 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 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(); + 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(); + }); + test('should show an invalid tooltip if invalid and message supplied', () => { render( 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); }