Skip to content

Commit 533c6dc

Browse files
committed
fix(webapp): fix billing email chip background, add undo
The chip container used ComboboxChips' default bg-surface-canvas / border-border-muted, which reads as a much darker fill than every other Input on the form — override to Input's own tokens. Removing a chip (backspace or its own remove button) is a state update, not a native text edit, so the browser's built-in Cmd/Ctrl+Z had nothing to undo. Track removals locally and restore the last one on undo when the draft input is empty.
1 parent 225b0e3 commit 533c6dc

1 file changed

Lines changed: 29 additions & 1 deletion

File tree

packages/webapp/src/pages/Team/Billing/components/InvoicingEmailsField.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState } from 'react';
12
import { useFormContext, useWatch } from 'react-hook-form';
23
import { z } from 'zod';
34

@@ -19,10 +20,26 @@ export const InvoicingEmailsField: React.FC = () => {
1920
const inputValue = useWatch({ control, name: 'emailsDraft' }) ?? '';
2021
const setInputValue = (value: string) => setValue('emailsDraft', value);
2122

23+
// Removing a chip (backspace or the chip's own × button) is a state update, not a native
24+
// text edit, so the browser's built-in Cmd/Ctrl+Z has nothing to undo. Track removals here
25+
// so we can restore the last one ourselves — see handleKeyDown.
26+
const [removedStack, setRemovedStack] = useState<string[]>([]);
27+
2228
const commit = (next: string[]) => {
29+
const removed = emails.filter((e) => !next.includes(e));
30+
if (removed.length > 0) {
31+
setRemovedStack((prev) => [...prev, ...removed]);
32+
}
2333
setValue('emails', next, { shouldDirty: true, shouldValidate: true });
2434
};
2535

36+
const undoLastRemoval = () => {
37+
if (removedStack.length === 0) return;
38+
const last = removedStack[removedStack.length - 1]!;
39+
setRemovedStack((prev) => prev.slice(0, -1));
40+
setValue('emails', [...emails, last], { shouldDirty: true, shouldValidate: true });
41+
};
42+
2643
// Splits on commas so a paste of "a@x.com, b@x.com" (or Figma's comma-separated
2744
// display format) adds every address at once instead of one long invalid chip.
2845
const addEmailsFromText = (text: string) => {
@@ -62,6 +79,15 @@ export const InvoicingEmailsField: React.FC = () => {
6279
const value = (e.target as HTMLInputElement).value.replace(/,$/, '').trim();
6380
if (!value) return;
6481
addEmailsFromText(value);
82+
return;
83+
}
84+
85+
// Only take over Cmd/Ctrl+Z when the input is empty — otherwise let the browser's
86+
// native undo handle an in-progress text edit in the draft input as usual.
87+
const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
88+
if (isUndo && !(e.target as HTMLInputElement).value) {
89+
e.preventDefault();
90+
undoLastRemoval();
6591
}
6692
};
6793

@@ -89,7 +115,9 @@ export const InvoicingEmailsField: React.FC = () => {
89115
</FormLabel>
90116
<FormControl>
91117
<Combobox items={[]} multiple value={emails} inputValue={inputValue} onValueChange={commit} open={false}>
92-
<ComboboxChips className="min-h-9">
118+
{/* ComboboxChips defaults to bg-surface-canvas/border-border-muted, which reads as a
119+
different (much darker) fill than a plain Input — match Input's own tokens instead. */}
120+
<ComboboxChips className="min-h-9 bg-surface-input border-border-interactive">
93121
{emails.length > 0 && (
94122
<ComboboxValue>
95123
{emails.map((email) => (

0 commit comments

Comments
 (0)