From 596476280dee11a2a8dfcdeea36fada4b4941171 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Sun, 3 May 2026 16:14:51 +0200 Subject: [PATCH] feat(ui): inline-edit dropdowns auto-open + auto-exit on dismiss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user clicks an inline-edit affordance for country / timezone / subdivision, the field flipped to its combobox trigger but the popover didn't open — they had to click again. And if they dismissed the popover without picking, the field stayed in edit mode showing a "Select country…" trigger they couldn't get out of. Combobox primitives (country / timezone / subdivision) now accept: - defaultOpen — open on first render - onOpenChange — fired on every open/close transition InlineCountryField / InlineTimezoneField / and the country + subdivision fields inside addresses-editor pass defaultOpen=true and use onOpenChange to auto-exit edit mode when the popover closes without a selection. A pickedRef gate prevents the close-handler from racing the commit() exit when the user does pick a value. Bonus: addresses-editor now renders a flag emoji next to the country name in the read-only state (regional-indicator pair from the ISO code). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/shared/addresses-editor.tsx | 34 +++++++++++++++++-- src/components/shared/country-combobox.tsx | 16 +++++++-- .../shared/inline-country-field.tsx | 24 +++++++++++-- .../shared/inline-timezone-field.tsx | 16 ++++++++- .../shared/subdivision-combobox.tsx | 15 ++++++-- src/components/shared/timezone-combobox.tsx | 15 ++++++-- 6 files changed, 109 insertions(+), 11 deletions(-) diff --git a/src/components/shared/addresses-editor.tsx b/src/components/shared/addresses-editor.tsx index e3c9302..48298e4 100644 --- a/src/components/shared/addresses-editor.tsx +++ b/src/components/shared/addresses-editor.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, MapPin, Plus, Star, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -225,6 +225,14 @@ function Field({ label, children }: { label: string; children: React.ReactNode } ); } +/** Regional-indicator emoji flag for an ISO alpha-2 code (e.g. 'FR' → 🇫🇷). */ +function flagEmoji(code: string | null | undefined): string { + if (!code || code.length !== 2) return ''; + const A = 0x1f1e6; + const a = 'A'.charCodeAt(0); + return String.fromCodePoint(A + code.charCodeAt(0) - a, A + code.charCodeAt(1) - a); +} + function CountryFieldInline({ value, onSave, @@ -233,20 +241,34 @@ function CountryFieldInline({ onSave: (iso: string | null) => Promise; }) { const [editing, setEditing] = useState(false); + // Tracks whether a value was picked this edit cycle so the open-change + // handler doesn't double-exit while commit is still in flight. + const pickedRef = useRef(false); + if (editing) { return ( { + pickedRef.current = true; setEditing(false); await onSave(iso ?? null); }} clearable className="w-full" + // Drop the user straight into the picker — no extra click on the + // trigger required. + defaultOpen + onOpenChange={(open) => { + // Auto-exit edit mode when the popover closes without a pick so + // the user isn't stuck staring at a "Select country…" trigger. + if (!open && !pickedRef.current) setEditing(false); + if (open) pickedRef.current = false; + }} /> ); } - const display = value ? getCountryName(value, 'en') : null; + const display = value ? `${flagEmoji(value)} ${getCountryName(value, 'en')}` : null; return (