Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/geolibre-desktop/src/components/layout/MapGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ function PaneLayerToggle({ viewId, index }: PaneLayerToggleProps) {
return (
<DropdownMenuCheckboxItem
key={layer.id}
indicator="box"
checked={visible}
onCheckedChange={(checked: boolean) =>
setSecondaryLayerVisibility(viewId, layer.id, checked)
Expand Down
36 changes: 29 additions & 7 deletions packages/ui/src/components/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;

export const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
/** `"check"` (default): bare checkmark when checked. `"box"`: always-visible bordered square that fills on check. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (quality): The JSDoc describes visual behaviour but doesn't flag the controlled-only constraint. Adding a note here makes the limitation discoverable at the call site:

Suggested change
/** `"check"` (default): bare checkmark when checked. `"box"`: always-visible bordered square that fills on check. */
/** `"check"` (default): bare checkmark when checked. `"box"`: always-visible bordered square that fills on check. `indicator="box"` requires a controlled `checked` prop (not `defaultChecked`). */

indicator?: "check" | "box";
}
>(({ className, children, checked, indicator = "check", ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
Expand All @@ -125,11 +128,30 @@ export const DropdownMenuCheckboxItem = React.forwardRef<
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{indicator === "box" ? (
<span
className={cn(
"absolute left-2 flex h-4 w-4 items-center justify-center rounded-[4px] border transition-colors",
checked === true
? "border-primary bg-primary text-primary-foreground"
: checked === "indeterminate"
? "border-primary bg-primary/40 text-primary-foreground"
: "border-input",
)}
>
{checked === true ? (
<Check className="h-3 w-3" />
) : checked === "indeterminate" ? (
<span className="h-0.5 w-2 rounded-full bg-current" />
) : null}
</span>
Comment on lines +131 to +147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): indicator="box" silently breaks in uncontrolled mode

The "box" branch reads visual state directly from the checked prop, bypassing DropdownMenuPrimitive.ItemIndicator. When a caller uses uncontrolled mode (passing defaultChecked instead of checked), checked will be undefined here and the box will always render empty—Radix manages the internal checked state but the visual never updates.

The "check" path avoids this because ItemIndicator is wired to Radix's internal state and works in both modes.

Mitigations (pick one):

  1. Guard against uncontrolled usage in the JSDoc / TypeScript so callers know indicator="box" requires a controlled checked:
Suggested change
{indicator === "box" ? (
<span
className={cn(
"absolute left-2 flex h-4 w-4 items-center justify-center rounded-[4px] border transition-colors",
checked === true
? "border-primary bg-primary text-primary-foreground"
: checked === "indeterminate"
? "border-primary bg-primary/40 text-primary-foreground"
: "border-input",
)}
>
{checked === true ? (
<Check className="h-3 w-3" />
) : checked === "indeterminate" ? (
<span className="h-0.5 w-2 rounded-full bg-current" />
) : null}
</span>
{indicator === "box" ? (
// NOTE: "box" mode reads `checked` directly and only works when `checked`
// is explicitly controlled (not with `defaultChecked`).
<span
aria-hidden="true"
className={cn(
"absolute left-2 flex h-4 w-4 items-center justify-center rounded-[4px] border transition-colors",
checked === true
? "border-primary bg-primary text-primary-foreground"
: checked === "indeterminate"
? "border-primary bg-primary/40 text-primary-foreground"
: "border-input",
)}
>
{checked === true ? (
<Check className="h-3 w-3" />
) : checked === "indeterminate" ? (
<span className="h-0.5 w-2 rounded-full bg-current" />
) : null}
</span>
  1. Or add a checked-state ref/state and sync it with Radix's onCheckedChange to handle uncontrolled mode (more involved).

The aria-hidden="true" addition in the suggestion above is also recommended regardless — the outer <span> is a decorative indicator and the parent menuitemcheckbox already carries the semantic state via aria-checked; leaving the span un-hidden can cause some screen readers to double-announce the item.

) : (
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
)}
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
Expand Down
Loading