Skip to content

Fix project button overflow with dropdown menu#1207

Open
skyfallwastaken wants to merge 1 commit into
mainfrom
project-button-overflow
Open

Fix project button overflow with dropdown menu#1207
skyfallwastaken wants to merge 1 commit into
mainfrom
project-button-overflow

Conversation

@skyfallwastaken
Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings April 21, 2026 22:01
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR replaces per-project inline icon buttons with a single three-dot DropdownMenu (bits-ui) to fix overflow on narrow cards. The structural refactor is clean, but the two link items ("View project website" and "View repository") wrap an <a> tag inside DropdownMenu.Item without using asChild, which means keyboard users who navigate to those items and press Enter will trigger a no-op click on the outer div instead of navigating to the URL.

Confidence Score: 4/5

Safe to merge after fixing the keyboard-navigation regression for link items.

One P1 accessibility defect: keyboard users cannot activate the 'View project website' or 'View repository' items. The remaining findings are P2 style suggestions and do not block merge, but the P1 should be addressed first.

app/javascript/pages/Projects/Index.svelte — specifically the two DropdownMenu.Item wrappers around anchor tags (lines 314–360)

Important Files Changed

Filename Overview
app/javascript/pages/Projects/Index.svelte Replaced per-project inline action buttons with a bits-ui DropdownMenu; keyboard accessibility for link items is broken due to <a> nested inside DropdownMenu.Item without asChild.

Sequence Diagram

sequenceDiagram
    actor User
    participant Trigger as DropdownMenu.Trigger (3-dot button)
    participant Content as DropdownMenu.Content
    participant Item as DropdownMenu.Item (div[role=menuitem])
    participant AnchorTag as <a> (nested child)

    User->>Trigger: Click / Enter
    Trigger->>Content: Opens dropdown portal

    Note over User,Item: Mouse path (works)
    User->>AnchorTag: Mouse click
    AnchorTag-->>User: Navigate to URL ✓

    Note over User,Item: Keyboard path (broken)
    User->>Item: Arrow key highlight + Enter
    Item->>Item: bits-ui fires synthetic click on div
    Item--xAnchorTag: Navigation NOT triggered ✗
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 314-336

Comment:
**Keyboard navigation won't activate link items**

`DropdownMenu.Item` renders as a `div[role="menuitem"]`; when a keyboard user presses Enter/Space on the highlighted item, bits-ui fires a synthetic click on the `div`, *not* on the inner `<a>`. Navigation therefore silently fails for keyboard-only users. The same pattern is repeated for the "View repository" item (line 339–360).

The idiomatic fix is to use `asChild` so the item delegates its role/props onto the `<a>` itself:

```svelte
<DropdownMenu.Item asChild>
  {#snippet child({ props })}
    <a
      {...props}
      href={project.repository.homepage}
      target="_blank"
      rel="noopener noreferrer"
      class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
    >
      <!-- icon + label -->
    </a>
  {/snippet}
</DropdownMenu.Item>
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 292-313

Comment:
**Dropdown trigger visible even when no actions exist**

If a project has no `repository?.homepage`, no `repo_url`, `manage_enabled` is falsy, and neither `archive_path` nor `unarchive_path` is set, the three-dot trigger button still renders but opens an empty menu. Consider conditionally rendering the trigger only when at least one action is available.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 314-451

Comment:
**Duplicated item class string — extract to a variable**

The class `"flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"` is copy-pasted five times. The existing `cardActionClass` variable shows the pattern; a similar `dropdownItemClass` const would keep changes consistent and reduce noise.

```svelte
const dropdownItemClass = "flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none";
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Replace project card action buttons with..." | Re-trigger Greptile

Comment on lines +314 to +336
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
>
<a
href={project.repository.homepage}
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
<svg
class="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2s.06-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.92 7.92 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8 8 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.7 15.7 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
/>
</svg>
View project website
</a>
</DropdownMenu.Item>
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.

P1 Keyboard navigation won't activate link items

DropdownMenu.Item renders as a div[role="menuitem"]; when a keyboard user presses Enter/Space on the highlighted item, bits-ui fires a synthetic click on the div, not on the inner <a>. Navigation therefore silently fails for keyboard-only users. The same pattern is repeated for the "View repository" item (line 339–360).

The idiomatic fix is to use asChild so the item delegates its role/props onto the <a> itself:

<DropdownMenu.Item asChild>
  {#snippet child({ props })}
    <a
      {...props}
      href={project.repository.homepage}
      target="_blank"
      rel="noopener noreferrer"
      class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
    >
      <!-- icon + label -->
    </a>
  {/snippet}
</DropdownMenu.Item>
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 314-336

Comment:
**Keyboard navigation won't activate link items**

`DropdownMenu.Item` renders as a `div[role="menuitem"]`; when a keyboard user presses Enter/Space on the highlighted item, bits-ui fires a synthetic click on the `div`, *not* on the inner `<a>`. Navigation therefore silently fails for keyboard-only users. The same pattern is repeated for the "View repository" item (line 339–360).

The idiomatic fix is to use `asChild` so the item delegates its role/props onto the `<a>` itself:

```svelte
<DropdownMenu.Item asChild>
  {#snippet child({ props })}
    <a
      {...props}
      href={project.repository.homepage}
      target="_blank"
      rel="noopener noreferrer"
      class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
    >
      <!-- icon + label -->
    </a>
  {/snippet}
</DropdownMenu.Item>
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +292 to +313
<DropdownMenu.Root>
<DropdownMenu.Trigger
class={cardActionClass}
aria-label="Project actions"
>
<svg
class="h-5 w-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<svg
class="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l-3-3 3-3"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 11h9a4 4 0 0 1 0 8h-2"
/>
</svg>
</Button>
{:else if !show_archived && project.archive_path}
<Button
type="button"
unstyled
class={cardActionClass}
title="Archive project"
onclick={() => openStatusChangeModal(project, false)}
<circle cx="12" cy="6" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="12" cy="18" r="2" />
</svg>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
class="z-50 min-w-[180px] rounded-xl border border-surface-200 bg-dark py-1.5 px-1 shadow-lg"
sideOffset={8}
>
<svg
class="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7h18l-2 11H5z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V4h8v3"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 11h4"
/>
</svg>
</Button>
{/if}
</div>
{#if project.repository?.homepage}
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.

P2 Dropdown trigger visible even when no actions exist

If a project has no repository?.homepage, no repo_url, manage_enabled is falsy, and neither archive_path nor unarchive_path is set, the three-dot trigger button still renders but opens an empty menu. Consider conditionally rendering the trigger only when at least one action is available.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 292-313

Comment:
**Dropdown trigger visible even when no actions exist**

If a project has no `repository?.homepage`, no `repo_url`, `manage_enabled` is falsy, and neither `archive_path` nor `unarchive_path` is set, the three-dot trigger button still renders but opens an empty menu. Consider conditionally rendering the trigger only when at least one action is available.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +314 to +451
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
>
<a
href={project.repository.homepage}
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
<svg
class="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2m-5.15 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56M14.34 14H9.66c-.1-.66-.16-1.32-.16-2s.06-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2M12 19.96c-.83-1.2-1.5-2.53-1.91-3.96h3.82c-.41 1.43-1.08 2.76-1.91 3.96M8 8H5.08A7.92 7.92 0 0 1 9.4 4.44C8.8 5.55 8.35 6.75 8 8m-2.92 8H8c.35 1.25.8 2.45 1.4 3.56A8 8 0 0 1 5.08 16m-.82-2C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2M12 4.03c.83 1.2 1.5 2.54 1.91 3.97h-3.82c.41-1.43 1.08-2.77 1.91-3.97M18.92 8h-2.95a15.7 15.7 0 0 0-1.38-3.56c1.84.63 3.37 1.9 4.33 3.56M12 2C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2"
/>
</svg>
View project website
</a>
</DropdownMenu.Item>
{/if}
{#if project.repo_url}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
>
<a
href={project.repo_url}
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
<svg
class="h-4 w-4 shrink-0"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2a2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82"
/>
</svg>
View repository
</a>
</DropdownMenu.Item>
{/if}
{#if project.manage_enabled}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
onclick={() => openMappingEditor(project)}
>
<svg
class="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M16.862 3.487a2.1 2.1 0 0 1 2.97 2.97L9.75 16.54 6 17.25l.71-3.75z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M14.5 5.85l3.65 3.65"
/>
</svg>
{project.repo_url
? "Edit mapping"
: "Link repository"}
</DropdownMenu.Item>
{/if}
{#if show_archived && project.unarchive_path}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
onclick={() => openStatusChangeModal(project, true)}
>
<svg
class="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l-3-3 3-3"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 11h9a4 4 0 0 1 0 8h-2"
/>
</svg>
Restore project
</DropdownMenu.Item>
{:else if !show_archived && project.archive_path}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
onclick={() => openStatusChangeModal(project, false)}
>
<svg
class="h-4 w-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7h18l-2 11H5z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V4h8v3"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 11h4"
/>
</svg>
Archive project
</DropdownMenu.Item>
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.

P2 Duplicated item class string — extract to a variable

The class "flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none" is copy-pasted five times. The existing cardActionClass variable shows the pattern; a similar dropdownItemClass const would keep changes consistent and reduce noise.

const dropdownItemClass = "flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none";
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/javascript/pages/Projects/Index.svelte
Line: 314-451

Comment:
**Duplicated item class string — extract to a variable**

The class `"flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"` is copy-pasted five times. The existing `cardActionClass` variable shows the pattern; a similar `dropdownItemClass` const would keep changes consistent and reduce noise.

```svelte
const dropdownItemClass = "flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none";
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the Projects index card actions UI to prevent button overflow by consolidating per-project actions into a single dropdown menu.

Changes:

  • Replaces the inline action icon group (website/repo/edit/archive) with a bits-ui DropdownMenu.
  • Adds a “more actions” trigger (3-dot icon) and renders project actions as menu items.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<svg
class="h-4 w-4 shrink-0"
fill="currentColor"
viewBox="0 0 24 24"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The SVG icons inside the menu items that already have visible text (“View project website” / “View repository”) should be marked decorative (e.g., aria-hidden="true") to avoid redundant/verbose screen reader output.

Suggested change
viewBox="0 0 24 24"
viewBox="0 0 24 24"
aria-hidden="true"

Copilot uses AI. Check for mistakes.
Comment on lines +339 to +366
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
>
<a
href={project.repo_url}
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
<svg
class="h-4 w-4 shrink-0"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2a2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2a2 2 0 0 0 2-2a2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82"
/>
</svg>
View repository
</a>
</DropdownMenu.Item>
{/if}
{#if project.manage_enabled}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
onclick={() => openMappingEditor(project)}
>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The DropdownMenu.Item styling class string is duplicated across each item. Consider extracting it into a constant (similar to cardActionClass) to reduce repetition and make future styling changes safer.

Copilot uses AI. Check for mistakes.
Comment on lines +314 to +322
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-surface-content data-highlighted:bg-surface-content/10 focus-visible:outline-none"
>
<a
href={project.repository.homepage}
target="_blank"
rel="noopener noreferrer"
class="flex w-full items-center gap-2"
>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

DropdownMenu.Item wraps an <a> element. This creates nested/focusable interactive elements and can break dropdown keyboard behavior (roving focus/Enter activation may not trigger the link). Prefer rendering the link as the menu item itself (e.g., use the component’s asChild/link API so the <a> is the root element) instead of nesting an anchor inside the item.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants