Skip to content

Commit 2f22b29

Browse files
committed
Update
1 parent 8ff1aeb commit 2f22b29

File tree

2 files changed

+115
-18
lines changed

2 files changed

+115
-18
lines changed

packages/cells/src/cells/multi-select-cell.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -138,30 +138,34 @@ const CustomMenu: React.FC<CustomMenuProps> = p => {
138138
* - Clicking on the pill label text won't open the dropdown menu (click input area to open)
139139
* - Removing pills via the X button still works normally (separate component)
140140
* - Keyboard navigation still works normally
141+
*
142+
* Note on type assertions: react-select's MultiValueGenericProps.innerProps type is
143+
* { className?: string }, but the underlying div element accepts all standard div props.
144+
* The type assertion to React.ComponentPropsWithoutRef<"div"> is necessary to add
145+
* event handlers that the actual DOM element supports.
141146
*/
142147
const SelectableMultiValueLabel: React.FC<MultiValueGenericProps<SelectOption>> = props => {
143-
// Preserve any existing handlers from innerProps (for forward compatibility)
148+
// Cast innerProps to the full div props type since react-select's types are overly restrictive
149+
// (they only type { className?: string } but the div accepts all standard props)
144150
const existingInnerProps = props.innerProps as React.ComponentPropsWithoutRef<"div"> | undefined;
145151

152+
const enhancedInnerProps: React.ComponentPropsWithoutRef<"div"> = {
153+
...existingInnerProps,
154+
// Allow text selection by stopping propagation but not preventing default
155+
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => {
156+
e.stopPropagation(); // Prevents react-select from treating it as a control click
157+
existingInnerProps?.onMouseDown?.(e);
158+
},
159+
onTouchEnd: (e: React.TouchEvent<HTMLDivElement>) => {
160+
e.stopPropagation();
161+
existingInnerProps?.onTouchEnd?.(e);
162+
},
163+
};
164+
146165
return (
147166
<components.MultiValueLabel
148167
{...props}
149-
innerProps={
150-
{
151-
...existingInnerProps,
152-
// Allow text selection by not preventing default on mouse down
153-
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => {
154-
e.stopPropagation(); // Prevents react-select from treating it as a control click
155-
// Call any existing handler
156-
existingInnerProps?.onMouseDown?.(e);
157-
},
158-
onTouchEnd: (e: React.TouchEvent<HTMLDivElement>) => {
159-
e.stopPropagation();
160-
// Call any existing handler
161-
existingInnerProps?.onTouchEnd?.(e);
162-
},
163-
} as React.ComponentPropsWithoutRef<"div">
164-
}
168+
innerProps={enhancedInnerProps as typeof props.innerProps}
165169
/>
166170
);
167171
};

packages/cells/test/multi-select-cell.test.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe("Multi Select Editor", () => {
270270
const Editor = renderer.provideEditor?.({
271271
...getMockCell(),
272272
location: [0, 0],
273-
}).editor;
273+
}).editor;
274274
if (Editor === undefined) {
275275
throw new Error("Editor is invalid");
276276
}
@@ -377,4 +377,97 @@ describe("Multi Select Editor", () => {
377377
});
378378

379379
// TODO: Add test for creating new options
380+
381+
it("allows text selection in pill labels (onMouseDown does not prevent default)", async () => {
382+
const mockCell = getMockCell({
383+
data: {
384+
kind: "multi-select-cell",
385+
options: [
386+
{ value: "option1", label: "Option 1", color: "red" },
387+
{ value: "option2", label: "Option 2", color: "blue" },
388+
],
389+
values: ["option1", "option2"],
390+
},
391+
});
392+
// @ts-ignore
393+
const Editor = renderer.provideEditor?.({
394+
...mockCell,
395+
location: [0, 0],
396+
}).editor;
397+
if (Editor === undefined) {
398+
throw new Error("Editor is invalid");
399+
}
400+
401+
const mockCellOnChange = vi.fn();
402+
const result = render(<Editor isHighlighted={false} value={mockCell} onChange={mockCellOnChange} />);
403+
const cellEditor = result.getByTestId("multi-select-cell");
404+
405+
// Find the pill labels (MultiValueLabel components render with the label text)
406+
const pillLabel = getByText(cellEditor, "Option 1");
407+
expect(pillLabel).toBeDefined();
408+
409+
// Simulate mousedown on the pill label - it should not prevent default (allowing text selection)
410+
// We verify this by checking that the event's defaultPrevented is false after the handler runs
411+
const mouseDownEvent = new MouseEvent("mousedown", {
412+
bubbles: true,
413+
cancelable: true,
414+
});
415+
416+
// The event should not be prevented (allowing text selection)
417+
pillLabel.dispatchEvent(mouseDownEvent);
418+
expect(mouseDownEvent.defaultPrevented).toBe(false);
419+
420+
// The onChange should NOT have been called just from clicking the label
421+
// (stopPropagation prevents the control from receiving the click)
422+
expect(mockCellOnChange).not.toHaveBeenCalled();
423+
});
424+
425+
it("still allows removing pills via the remove button after text selection enhancement", async () => {
426+
const mockCell = getMockCell({
427+
data: {
428+
kind: "multi-select-cell",
429+
options: [
430+
{ value: "option1", label: "Option 1", color: "red" },
431+
{ value: "option2", label: "Option 2", color: "blue" },
432+
],
433+
values: ["option1"],
434+
},
435+
});
436+
// @ts-ignore
437+
const Editor = renderer.provideEditor?.({
438+
...mockCell,
439+
location: [0, 0],
440+
}).editor;
441+
if (Editor === undefined) {
442+
throw new Error("Editor is invalid");
443+
}
444+
445+
const mockCellOnChange = vi.fn();
446+
const result = render(<Editor isHighlighted={false} value={mockCell} onChange={mockCellOnChange} />);
447+
const cellEditor = result.getByTestId("multi-select-cell");
448+
449+
// Find the pill label first
450+
const pillLabel = getByText(cellEditor, "Option 1");
451+
expect(pillLabel).toBeDefined();
452+
453+
// The remove button is a sibling of the label within the multi-value container
454+
// react-select renders: <div class="...multi-value"><div class="...label">text</div><div class="...remove">X</div></div>
455+
const multiValueContainer = pillLabel.parentElement;
456+
expect(multiValueContainer).not.toBeNull();
457+
458+
// Find the remove button (it's the element with the SVG/X icon, typically the last child or has a specific role)
459+
// react-select's remove button contains an SVG with a path
460+
const removeButton = multiValueContainer?.querySelector("svg")?.parentElement;
461+
expect(removeButton).not.toBeNull();
462+
463+
// Click the remove button
464+
fireEvent.click(removeButton!);
465+
466+
// The onChange should have been called to remove the value
467+
expect(mockCellOnChange).toHaveBeenCalledTimes(1);
468+
expect(mockCellOnChange).toHaveBeenCalledWith({
469+
...mockCell,
470+
data: { ...mockCell.data, values: [] },
471+
});
472+
});
380473
});

0 commit comments

Comments
 (0)