feat(checkout): fix searchable select API search and add billing destination

Fixes:
1. SearchableSelect now supports onSearch prop for API-based search
   - Added onSearch and isSearching props
   - shouldFilter disabled when onSearch provided
2. DynamicCheckoutField connects handleApiSearch to SearchableSelect
3. RAJAONGKIR_INTEGRATION.md adds both billing and shipping destination_id

This enables the destination search field to actually call the API
when user types, instead of just filtering local (empty) options.
This commit is contained in:
Dwindi Ramadhana
2026-01-08 14:47:54 +07:00
parent f6b778c7fc
commit f518d7e589
3 changed files with 40 additions and 9 deletions

View File

@@ -88,7 +88,7 @@ function woonoow_rajaongkir_search_destinations($request) {
} }
// ============================================================ // ============================================================
// 2. Add destination field to checkout fields // 2. Add destination field to checkout fields (both billing and shipping)
// Always add for Indonesia zone (no premature country check) // Always add for Indonesia zone (no premature country check)
// ============================================================ // ============================================================
add_filter('woocommerce_checkout_fields', function($fields) { add_filter('woocommerce_checkout_fields', function($fields) {
@@ -103,12 +103,11 @@ add_filter('woocommerce_checkout_fields', function($fields) {
return $fields; return $fields;
} }
// Add searchable destination field // Destination field definition (reused for billing and shipping)
// The frontend will show/hide based on selected country $destination_field = [
$fields['shipping']['shipping_destination_id'] = [
'type' => 'searchable_select', 'type' => 'searchable_select',
'label' => __('Destination (Province, City, Subdistrict)', 'woonoow'), 'label' => __('Destination (Province, City, Subdistrict)', 'woonoow'),
'required' => false, // Not required initially, SPA will manage this 'required' => false, // Frontend will manage this based on country
'priority' => 85, 'priority' => 85,
'class' => ['form-row-wide'], 'class' => ['form-row-wide'],
'placeholder' => __('Search destination...', 'woonoow'), 'placeholder' => __('Search destination...', 'woonoow'),
@@ -122,6 +121,12 @@ add_filter('woocommerce_checkout_fields', function($fields) {
], ],
]; ];
// Add to billing (used when "Ship to different address" is NOT checked)
$fields['billing']['billing_destination_id'] = $destination_field;
// Add to shipping (used when "Ship to different address" IS checked)
$fields['shipping']['shipping_destination_id'] = $destination_field;
return $fields; return $fields;
}, 20); // Priority 20 to run after Rajaongkir's own filter }, 20); // Priority 20 to run after Rajaongkir's own filter

View File

@@ -162,7 +162,9 @@ export function DynamicCheckoutField({
document.dispatchEvent(event); document.dispatchEvent(event);
} }
}} }}
placeholder={isSearching ? 'Searching...' : (field.placeholder || `Search ${field.label}...`)} onSearch={handleApiSearch}
isSearching={isSearching}
placeholder={field.placeholder || `Search ${field.label}...`}
emptyLabel={ emptyLabel={
isSearching isSearching
? 'Searching...' ? 'Searching...'

View File

@@ -28,6 +28,9 @@ interface Props {
emptyLabel?: string; emptyLabel?: string;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
// For API-based search
onSearch?: (searchTerm: string) => void;
isSearching?: boolean;
} }
export function SearchableSelect({ export function SearchableSelect({
@@ -38,12 +41,26 @@ export function SearchableSelect({
emptyLabel = "No results found.", emptyLabel = "No results found.",
className, className,
disabled = false, disabled = false,
onSearch,
isSearching = false,
}: Props) { }: Props) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("");
const selected = options.find((o) => o.value === value); const selected = options.find((o) => o.value === value);
React.useEffect(() => { if (disabled && open) setOpen(false); }, [disabled, open]); React.useEffect(() => { if (disabled && open) setOpen(false); }, [disabled, open]);
// Handle search input changes
const handleSearchChange = (value: string) => {
setSearchValue(value);
if (onSearch) {
onSearch(value);
}
};
// Determine if we should use local filtering or API-based search
const shouldFilter = !onSearch;
return ( return (
<Popover open={disabled ? false : open} onOpenChange={(o) => !disabled && setOpen(o)}> <Popover open={disabled ? false : open} onOpenChange={(o) => !disabled && setOpen(o)}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -69,10 +86,16 @@ export function SearchableSelect({
align="start" align="start"
sideOffset={4} sideOffset={4}
> >
<Command shouldFilter> <Command shouldFilter={shouldFilter}>
<CommandInput placeholder="Search..." /> <CommandInput
placeholder="Search..."
value={searchValue}
onValueChange={handleSearchChange}
/>
<CommandList> <CommandList>
<CommandEmpty>{emptyLabel}</CommandEmpty> <CommandEmpty>
{isSearching ? "Searching..." : emptyLabel}
</CommandEmpty>
{options.map((opt) => ( {options.map((opt) => (
<CommandItem <CommandItem
key={opt.value} key={opt.value}
@@ -80,6 +103,7 @@ export function SearchableSelect({
onSelect={() => { onSelect={() => {
onChange?.(opt.value); onChange?.(opt.value);
setOpen(false); setOpen(false);
setSearchValue("");
}} }}
> >
<Check <Check