From 7164c17a30fe5023289ee05e471b2fca427061c8 Mon Sep 17 00:00:00 2001 From: Joseph Louise Date: Tue, 6 Jan 2026 09:30:28 +0800 Subject: [PATCH 01/12] Task: add remote prop to SuggestInput. This prop should take a string url like http://example.com/suggest and this url should return an array of strings used to update the options. --- src/form/SuggestInput.tsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/form/SuggestInput.tsx b/src/form/SuggestInput.tsx index 600d04d1..dc49507c 100644 --- a/src/form/SuggestInput.tsx +++ b/src/form/SuggestInput.tsx @@ -15,6 +15,8 @@ import type { InputProps } from './Input.js'; import getSlotStyles from '../helpers/getSlotStyles.js'; import Dropdown from '../base/Dropdown.js'; import Input from './Input.js'; +//modules +import { useState } from 'react'; //--------------------------------------------------------------------// // Types @@ -43,6 +45,8 @@ export type SuggestInputProps = Omit, //serialized list of options as array or object options?: DropdownOptionProp + //remote url to fetch suggestions + remote?: string }, 'multiple' >; @@ -143,12 +147,16 @@ export function SuggestInput(props: SuggestInputProps) { left, //?: boolean //dropdown handler onDropdown, //?: (show: boolean) => void + //called whenever user types + onQuery, //?: (query: string) => void //update handler onUpdate, //?: (value: string) => void //slot: style to apply to the select control option, //: CallableSlotStyleProp //serialized list of options as array or object options, //: SelectOption[]|Record + //remote url to fetch suggestions + remote, //?: string //position of the dropdown right, //?: boolean //custom inline styles @@ -159,6 +167,11 @@ export function SuggestInput(props: SuggestInputProps) { value, //?: T ...inputProps } = props; + //hooks + const [ + remoteOptions, + setRemoteOptions + ] = useState(options); //variables // determine classes const classes = [ 'frui-form-suggest-input' ]; @@ -169,6 +182,21 @@ export function SuggestInput(props: SuggestInputProps) { // get slot styles const controlStyles = control ? getSlotStyles(control, {}) : {}; const dropdownStyles = dropdown ? getSlotStyles(dropdown, {}) : {}; + //handlers + const handleQuery = async (query: string) => { + if (typeof remote === 'string' && query) { + try { + const response = + await fetch(`${remote}?q=${encodeURIComponent(query)}`); + const data = await response.json(); + if (Array.isArray(data)) { + setRemoteOptions(data); + } + } catch (error) { + console.error('Failed to fetch remote suggestions:', error); + } + } + }; //render return ( {children} From 28a8022ddc21e8b37abae3aedd21654bc5768874 Mon Sep 17 00:00:00 2001 From: Joseph Louise Date: Tue, 6 Jan 2026 09:39:05 +0800 Subject: [PATCH 02/12] Task: add remote prop to SuggestInput. This prop should take a string url like http://example.com/suggest and this url should return an array of strings used to update the options. --- src/form/SuggestInput.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/form/SuggestInput.tsx b/src/form/SuggestInput.tsx index dc49507c..0ea0765a 100644 --- a/src/form/SuggestInput.tsx +++ b/src/form/SuggestInput.tsx @@ -196,6 +196,9 @@ export function SuggestInput(props: SuggestInputProps) { console.error('Failed to fetch remote suggestions:', error); } } + if (typeof onQuery === 'function') { + onQuery(query); + } }; //render return ( From fac6bef61db58419eef58f0d9742e18e864d16bd Mon Sep 17 00:00:00 2001 From: Joseph Louise Date: Tue, 6 Jan 2026 09:42:05 +0800 Subject: [PATCH 03/12] Task: add remote prop to SuggestInput. This prop should take a string url like http://example.com/suggest and this url should return an array of strings used to update the options. --- src/form/SuggestInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/form/SuggestInput.tsx b/src/form/SuggestInput.tsx index 0ea0765a..ef8e5e76 100644 --- a/src/form/SuggestInput.tsx +++ b/src/form/SuggestInput.tsx @@ -212,7 +212,7 @@ export function SuggestInput(props: SuggestInputProps) { onDropdown={onDropdown} onUpdate={onUpdate} option={option} - options={typeof remote === 'string' ? remoteOptions : options} + options={remoteOptions} right={right} top={top} value={value} @@ -222,7 +222,7 @@ export function SuggestInput(props: SuggestInputProps) { {...inputProps} className={controlStyles.className} style={controlStyles.style} - onQuery={typeof remote === 'string' ? handleQuery : onQuery} + onQuery={handleQuery} /> {children} From 099d1da2d5f5e624ee361f52ade10106a77b7e3c Mon Sep 17 00:00:00 2001 From: Joseph Louise Date: Tue, 6 Jan 2026 11:51:20 +0800 Subject: [PATCH 04/12] update requested changes on the pr --- src/form/SuggestInput.tsx | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/form/SuggestInput.tsx b/src/form/SuggestInput.tsx index ef8e5e76..87e65229 100644 --- a/src/form/SuggestInput.tsx +++ b/src/form/SuggestInput.tsx @@ -39,6 +39,8 @@ export type SuggestInputProps = Omit void, //slot: style to apply to the select control @@ -145,6 +147,8 @@ export function SuggestInput(props: SuggestInputProps) { error, //?: boolean //position of the dropdown left, //?: boolean + //custom fetch function for dependency injection (mainly for tests) + fetch: customFetch = fetch, //dropdown handler onDropdown, //?: (show: boolean) => void //called whenever user types @@ -182,23 +186,18 @@ export function SuggestInput(props: SuggestInputProps) { // get slot styles const controlStyles = control ? getSlotStyles(control, {}) : {}; const dropdownStyles = dropdown ? getSlotStyles(dropdown, {}) : {}; - //handlers const handleQuery = async (query: string) => { if (typeof remote === 'string' && query) { - try { - const response = - await fetch(`${remote}?q=${encodeURIComponent(query)}`); - const data = await response.json(); - if (Array.isArray(data)) { - setRemoteOptions(data); - } - } catch (error) { - console.error('Failed to fetch remote suggestions:', error); + const response = + await customFetch( + remote.replace('{{QUERY}}', encodeURIComponent(query)) + ); + const data = await response.json(); + if (Array.isArray(data)) { + setRemoteOptions(data); } } - if (typeof onQuery === 'function') { - onQuery(query); - } + onQuery && onQuery(query); }; //render return ( From 04b995fa9ac084ab580e9c4797a9c302d5975f3c Mon Sep 17 00:00:00 2001 From: Joseph Louise Date: Wed, 7 Jan 2026 15:46:41 +0800 Subject: [PATCH 05/12] Missing test and documentation update for this task. --- src/form/SuggestInput.tsx | 2 +- tests/form/SuggestInput.test.tsx | 141 +++++++++++--------------- web/docs/views/form/suggest-input.tsx | 41 +++++++- 3 files changed, 98 insertions(+), 86 deletions(-) diff --git a/src/form/SuggestInput.tsx b/src/form/SuggestInput.tsx index 87e65229..1de0b7e0 100644 --- a/src/form/SuggestInput.tsx +++ b/src/form/SuggestInput.tsx @@ -39,7 +39,7 @@ export type SuggestInputProps = Omit void, diff --git a/tests/form/SuggestInput.test.tsx b/tests/form/SuggestInput.test.tsx index 7731f051..2f3daa42 100644 --- a/tests/form/SuggestInput.test.tsx +++ b/tests/form/SuggestInput.test.tsx @@ -2,7 +2,7 @@ // Imports //frui -import Select, { SelectPlaceholder } from '../../src/form/Select.js'; +import SuggestInput from '../../src/form/SuggestInput.js'; //tests import '@testing-library/jest-dom'; import { @@ -40,49 +40,43 @@ vi.mock('src/helpers/getClassStyles.js', () => ({ //--------------------------------------------------------------------// // Tests -describe(' - Pick one - + ); - const wrapper = document.querySelector('.frui-form-select'); + const wrapper = document.querySelector('.frui-form-suggest-input'); expect(wrapper).toBeInTheDocument(); - expect(screen.getByText('Pick one')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Type to search')).toBeInTheDocument(); }); - it('applies error class when error prop set', () => { - const { container } = render( - Click me - + /> ); - const toggle = screen.getByText('Click me'); - fireEvent.click(toggle); - expect( - document.querySelector( - '.frui-form-select-control-actions-toggle' - ) - ).toBeInTheDocument(); + const input = screen.getByRole('textbox') as HTMLInputElement; + fireEvent.change(input, { target: { value: 'ap' } }); + expect(onQuery).not.toHaveBeenCalled(); + fireEvent.change(input, { target: { value: 'app' } }); + expect(onQuery).toHaveBeenCalledWith('app'); }); - it('calls onUpdate when external value changes', async () => { const onUpdate = vi.fn(); const { rerender } = render( - ', () => { expect(onUpdate).toHaveBeenCalledWith('no'); }); }); - it('adds hidden input after selection', async () => { const { rerender } = render( - ); await waitFor(() => { @@ -126,52 +119,39 @@ describe(' + it('fetches remote suggestions with custom fetch prop', async () => { + const mockFetch = vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve([ + { value: 'result1', label: 'Result 1' }, + { value: 'result2', label: 'Result 2' } + ]) + } as Response) ); - rerender( - ', () => { value="opt2" /> ); - const control = container.querySelector( - '.frui-form-select-control-selected' - ); - expect(control).toBeInTheDocument(); - expect(control?.textContent).toContain('Opt2'); + const input = screen.getByRole('textbox') as HTMLInputElement; + expect(input.value).toBe('opt2'); rerender( -