Skip to content

Conversation

@janosh
Copy link
Owner

@janosh janosh commented Dec 31, 2025

Summary by CodeRabbit

  • New Features

    • Keyboard shortcuts for MultiSelect (select/clear/open/close, customizable).
    • Resizable UI elements via edge dragging with min/max constraints and callbacks.
    • Command palette now supports grouping of actions.
    • Prev/Next links include hover preload hints.
  • Breaking Changes

    • FileDetails no longer accepts a style prop.
    • Toggle input handlers/attributes moved to an input_props prop.
  • Tests

    • Expanded and added many component tests (shortcuts, resizing, grouping, file details, icons).

✏️ Tip: You can customize this high-level summary in your review settings.

…rops

- Replace direct file.node bindings with reactive node_refs array to avoid binding_property_non_reactive warning
- Add effect to sync node_refs back to files.node for external access
- Use derived any_open state for button text
- Spread rest props onto element for better composability
- Remove commented-out hljs code
- Rename test file to follow *.svelte.test.ts convention
- Extend Action generic to include optional group property
- Improve dialog_style comment for clarity
- Add comprehensive tests for CmdPalette functionality
- New resizable() attachment for making elements resizable by edge dragging
- Supports configurable edges (top, right, bottom, left)
- Min/max width and height constraints
- Customizable handle size
- Callbacks for resize start, resize, and resize end events
- Add comprehensive tests for resizable behavior
- New shortcuts prop for customizing keyboard actions
- Default shortcuts: ctrl+a (select all), ctrl+shift+a (clear all)
- Add KeyboardShortcuts interface to types.ts
- Support modifier keys: ctrl, shift, alt, meta/cmd
- Shortcuts can be disabled by setting to null
- Add comprehensive keyboard shortcut tests
- Add data-sveltekit-preload-data="hover" to prev/next links
- Improves page transition performance
- Update tests to verify preload attribute
- Add Icon.test.ts with tests for icon rendering and props
- Add Wiggle.test.ts with tests for wiggle animation behavior
- Refactor Toggle.test.ts with improved structure and assertions
- Expand index.test.ts with additional export and util tests
- Update MultiSelect playwright tests
@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds keyboard-shortcut support to MultiSelect, a new resizable attachment action, refactors FileDetails to manage DOM refs and spread HTML attributes, extends CmdPalette Action generic with optional grouping, adds SvelteKit preload attrs in PrevNext, and includes many test additions/updates.

Changes

Cohort / File(s) Summary
Keyboard Shortcuts
src/lib/types.ts, src/lib/MultiSelect.svelte
New KeyboardShortcuts interface and shortcuts prop; parse_shortcut/matches_shortcut; handle_keydown pre-checks configured shortcuts (select_all, clear_all, open, close) and triggers corresponding actions before existing key logic.
Resizable Attachment
src/lib/attachments.ts
Adds Dimensions, ResizeCallback, ResizableOptions and exported resizable action implementing edge-based drag resizing, min/max clamps, handle sizing, cursor updates, and lifecycle callbacks (on_resize_start, on_resize, on_resize_end).
FileDetails Refactor & API
src/lib/FileDetails.svelte
Replaces per-file node binding with reactive node_refs array and derived any_open; toggle_all and open/close logic use node_refs; removes style prop and spreads remaining HTML attributes via {...rest}.
CmdPalette Typing
src/lib/CmdPalette.svelte
Extends component generic so Action may include optional group?: string; clarifies dialog_style comment to "inline style for the dialog element".
PrevNext Enhancement
src/lib/PrevNext.svelte
Adds data-sveltekit-preload-data="hover" attribute to prev/next anchor elements.
Tests — Additions & Updates
tests/vitest/*.test.ts, tests/playwright/*.test.ts
Large test changes: new/updated suites for MultiSelect (keyboard shortcuts added, duplicated block present), CmdPalette grouping tests, FileDetails (new tests; older suite removed), Icon, Wiggle, attachments (resizable), Toggle (now uses input_props), PrevNext (data-driven), and Playwright sync/wait improvements.
Public API / Types
src/lib/types.ts, src/lib/MultiSelect.svelte, src/lib/attachments.ts, src/lib/CmdPalette.svelte
Public surface updates: KeyboardShortcuts + shortcuts prop, exported resizable and related types, and extended Action generic to include group?: string.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Element as ResizableElement
  participant Resizer as resizable action
  participant Document as document

  Note over User,Element: User presses and drags an edge handle
  User ->> Element: mousedown on edge
  Element ->> Resizer: init drag (record start dims & edge)
  Resizer ->> Document: add mousemove & mouseup listeners
  par dragging
    Document ->> Resizer: mousemove events
    Resizer ->> Resizer: compute clamped width/height, adjust left/top if needed
    Resizer ->> Element: apply style updates (width/height/left/top)
    Resizer ->> Resizer: call on_resize callback (if provided)
  and release
    User ->> Document: mouseup
    Document ->> Resizer: mouseup event
    Resizer ->> Resizer: emit on_resize_end, cleanup listeners, reset cursor
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • janosh/svelte-multiselect#376: Overlaps changes to CmdPalette generics, FileDetails refactor, MultiSelect keyboard-shortcut logic, and attachments exports.
  • janosh/svelte-multiselect#363: Related keyboard handling edits in src/lib/MultiSelect.svelte.
  • janosh/svelte-multiselect#325: Related to attachment utilities and resizable-like additions in src/lib/attachments.ts.

Poem

🐰 I nibble keys and hop on edges light,

Groups line up and refs stay tight.
I stretch the pane and press a key,
Tests clap paws — the code runs free.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the three main features added: resizable attachment, MultiSelect keyboard shortcuts, and expanded test coverage across multiple components.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9061c80 and 6e19615.

📒 Files selected for processing (5)
  • src/lib/types.ts
  • tests/vitest/Icon.test.ts
  • tests/vitest/MultiSelect.svelte.test.ts
  • tests/vitest/Toggle.test.ts
  • tests/vitest/Wiggle.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
tests/vitest/Wiggle.test.ts (1)

77-84: Verify timer behavior with duration=0.

The test advances fake timers by 0ms to verify immediate reset, but vi.advanceTimersByTime(0) may not actually trigger timer callbacks. If the implementation uses setTimeout(..., 0), the test should advance by at least 1ms or use vi.runAllTimers() to ensure the callback executes.

🔎 More robust test approach
  test(`resets wiggle immediately with duration=0`, () => {
    const { props, get_value } = create_bindable_wiggle(true, { duration: 0 })
    mount(Wiggle, { target, props })

    expect(get_value()).toBe(true)
-   vi.advanceTimersByTime(0)
+   vi.runAllTimers()
    expect(get_value()).toBe(false)
  })
tests/vitest/MultiSelect.svelte.test.ts (2)

2827-2847: Consider test coverage for edge cases.

The keyboard shortcuts test suite is comprehensive, but consider adding tests for:

  • Shortcuts when disabled={true} (should they be blocked?)
  • Invalid shortcut string formats (e.g., "ctrl+", "invalid+key")
  • Shortcuts when dropdown is closed vs. open (do all shortcuts require open={true}?)

These edge cases would strengthen the test coverage and prevent potential runtime issues.

Do you want me to generate additional test cases for these scenarios?


2873-2901: Consider extracting common test setup.

The keyboard shortcut tests follow a consistent pattern with repeated setup code. Consider extracting a helper function to reduce duplication:

🔎 Example helper function
function setup_shortcut_test(props: Partial<MultiSelectProps>) {
  const state = $state<MultiSelectProps>({
    options: [`a`, `b`, `c`],
    open: true,
    ...props,
  })
  mount(MultiSelect, { target: document.body, props: state })
  const input = doc_query<HTMLInputElement>(`input[autocomplete]`)
  input.focus()
  return { props: state, input }
}

// Usage:
test(`ctrl+a selects all options when selectAllOption is enabled`, async () => {
  const { props, input } = setup_shortcut_test({ 
    selectAllOption: true, 
    selected: [] 
  })
  
  input.dispatchEvent(
    new KeyboardEvent(`keydown`, { key: `a`, ctrlKey: true, bubbles: true }),
  )
  await tick()
  
  expect(props.selected).toEqual([`a`, `b`, `c`])
})
src/lib/attachments.ts (2)

23-37: Consider exporting Dimensions and ResizeCallback types.

The type definitions look clean and well-structured. However, Dimensions and ResizeCallback are used in the exported ResizableOptions interface but aren't themselves exported, which prevents users from easily referencing these types in their callback implementations.

🔎 Proposed enhancement
-type Dimensions = { width: number; height: number }
-type ResizeCallback = (event: MouseEvent, dimensions: Dimensions) => void
+export type Dimensions = { width: number; height: number }
+export type ResizeCallback = (event: MouseEvent, dimensions: Dimensions) => void

138-163: Good setup with reasonable defaults.

The function initialization is clean with sensible default values. The option destructuring makes the code readable and maintainable.

One note: Line 163 modifies the element's position style from static to relative to enable left/top positioning during resize. While necessary for the feature to work, this side effect persists after cleanup. Consider storing and restoring the original position value if you want full cleanup isolation.

🔎 Proposed enhancement for complete cleanup
 (options: ResizableOptions = {}): Attachment => (element: Element) => {
   if (options.disabled) return

   const node = element as HTMLElement
+  const original_position = getComputedStyle(node).position
   const {
     edges = [`right`, `bottom`],
     // ... rest of options
   } = options

   // ... resize logic ...

   return () => {
     node.removeEventListener(`mousedown`, on_mousedown)
     node.removeEventListener(`mousemove`, on_hover)
     globalThis.removeEventListener(`mousemove`, on_mousemove)
     globalThis.removeEventListener(`mouseup`, on_mouseup)
     node.style.cursor = ``
+    if (original_position === `static`) {
+      node.style.position = original_position
+    }
   }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee776a and aaa4b31.

📒 Files selected for processing (17)
  • src/lib/CmdPalette.svelte
  • src/lib/FileDetails.svelte
  • src/lib/MultiSelect.svelte
  • src/lib/PrevNext.svelte
  • src/lib/attachments.ts
  • src/lib/types.ts
  • tests/playwright/MultiSelect.test.ts
  • tests/vitest/CmdPalette.svelte.test.ts
  • tests/vitest/FileDetails.svelte.test.ts
  • tests/vitest/FileDetails.test.ts
  • tests/vitest/Icon.test.ts
  • tests/vitest/MultiSelect.svelte.test.ts
  • tests/vitest/PrevNext.test.ts
  • tests/vitest/Toggle.test.ts
  • tests/vitest/Wiggle.test.ts
  • tests/vitest/attachments.test.ts
  • tests/vitest/index.test.ts
💤 Files with no reviewable changes (1)
  • tests/vitest/FileDetails.test.ts
🧰 Additional context used
🧬 Code graph analysis (5)
tests/vitest/Icon.test.ts (1)
src/lib/icons.ts (2)
  • icon_data (1-65)
  • IconName (67-67)
tests/vitest/FileDetails.svelte.test.ts (1)
tests/vitest/index.ts (1)
  • doc_query (4-8)
tests/vitest/index.test.ts (1)
src/lib/index.ts (1)
  • scroll_into_view_if_needed_polyfill (22-40)
tests/vitest/attachments.test.ts (1)
src/lib/attachments.ts (2)
  • draggable (42-136)
  • resizable (139-249)
tests/vitest/MultiSelect.svelte.test.ts (2)
tests/vitest/index.ts (1)
  • doc_query (4-8)
src/lib/types.ts (1)
  • MultiSelectProps (117-219)
🔇 Additional comments (34)
tests/vitest/index.test.ts (5)

21-36: Well-structured mock state management.

The MockState type and shared mock variable provide clean organization for the IntersectionObserver mock. The structure correctly captures callback, instance, and spy functions needed for testing.


38-61: Clean mock observer implementation.

The create_mock_observer helper effectively creates a controllable IntersectionObserver mock. The approach of resetting spies on each call and capturing the callback for manual invocation is a good pattern for testing observer-based code.


63-66: Good test isolation with proper cleanup.

The afterEach correctly resets both the mock state and unstubs globals, ensuring tests don't leak state to each other.


68-107: Comprehensive parameterized test coverage.

The test cases correctly cover all branches of the scroll_into_view_if_needed_polyfill function:

  • ratio <= 0 && centerIfNeededcenter
  • ratio > 0 || !centerIfNeedednearest
  • ratio >= 1 → no scroll

The verification that disconnect is always called (line 105) correctly matches the unconditional obs.disconnect() in the implementation.


109-122: Good coverage of default parameter behavior.

This test correctly verifies that centerIfNeeded defaults to true when omitted, ensuring the function scrolls to center when the element is not visible (ratio=0).

tests/vitest/Toggle.test.ts (5)

11-14: Nice refactoring with test.each.

Consolidating the checked prop tests into a single parameterized test improves maintainability and reduces duplication.


25-36: Well-structured test for Enter key behavior.

Merging the Enter key toggle tests into a single test that verifies both the toggle behavior and change event emission improves test clarity.


38-45: Good use of parameterized testing for non-toggle keys.

Using test.each to verify that multiple keys don't trigger toggling reduces code duplication and makes it easy to add more keys in the future.


47-66: Solid test for Enter key event handling.

The test correctly verifies that:

  • preventDefault is called
  • onkeydown handler fires before click
  • Event order is preserved

The use of call_order array to track execution sequence is a clean approach.


68-80: Good coverage of class and style props.

The test correctly verifies that custom classes and styles are applied to both the label and input elements.

tests/vitest/Icon.test.ts (4)

11-23: Comprehensive icon rendering test.

The parameterized test effectively verifies all icons in icon_data, checking viewBox, fill, and path attributes. The type assertion on line 11 is appropriate since Object.keys returns string[] but we know they're IconName.


25-56: Excellent pattern for testing rest props.

The parameterized test with tuple structure [attr, value, getter, expected] is clean and extensible. Using as const ensures type safety, and the variety of getter functions appropriately handles different attribute types (classList vs getAttribute).


58-74: Robust fallback testing with proper mock cleanup.

The test covers important edge cases (invalid, empty, whitespace strings) and correctly mocks and restores console.error. The use of @ts-expect-error on line 63 is appropriate for testing runtime behavior with invalid icon names.


76-81: Good practice testing computed styles.

Using getComputedStyle correctly verifies the actual rendered styles rather than just inline attributes, ensuring the component's default visual behavior is correct.

src/lib/PrevNext.svelte (1)

103-105: LGTM! SvelteKit preload enhancement.

The addition of data-sveltekit-preload-data="hover" enables efficient hover-based preloading for prev/next navigation links, improving user experience without changing existing navigation behavior.

Also applies to: 118-120

tests/vitest/MultiSelect.svelte.test.ts (1)

2767-2767: LGTM! Improved selector consistency.

Changing to input[autocomplete] aligns with the selector pattern used throughout the test file, improving consistency and maintainability.

tests/vitest/CmdPalette.svelte.test.ts (1)

594-727: LGTM! Comprehensive grouping test coverage.

The grouping tests thoroughly validate:

  • Group header rendering and content
  • Backward compatibility with ungrouped actions
  • Collapsible functionality with aria-expanded state
  • Mixed grouped/ungrouped scenarios
  • Initial collapsed state control via props

The test structure is clear and the assertions properly verify both DOM structure and behavior.

src/lib/types.ts (1)

217-230: LGTM! Well-designed keyboard shortcuts API.

The KeyboardShortcuts interface is thoughtfully designed:

  • Clear documentation with format examples
  • Partial<> type allows selective customization
  • string | null enables disabling individual shortcuts
  • Consistent naming convention (snake_case for action names)

The integration into MultiSelectProps as an optional prop maintains backward compatibility while enabling the new feature.

tests/playwright/MultiSelect.test.ts (2)

233-237: LGTM! Improved test synchronization for remove-all button.

The changes properly handle the flip animation by waiting for the button to become visible before clicking, and then asserting it's hidden after the action. This reduces flakiness in tests that depend on animation timing.


1061-1095: LGTM! Enhanced keyboard navigation test with better synchronization.

The test now properly:

  • Waits for state to settle after toggling keyboardExpandsCollapsedGroups
  • Waits for the options list to become visible
  • Verifies aria-expanded states before navigating
  • Queries active option after UI settles
  • Includes descriptive comments for each navigation step

These changes improve reliability, especially in CI environments where timing can vary.

tests/vitest/attachments.test.ts (2)

640-695: LGTM! Comprehensive tests for draggable edge cases.

Good coverage for:

  • Non-fixed positioning using offsetLeft/offsetTop instead of getBoundingClientRect
  • Ignoring mousemove events when not actively dragging
  • Ignoring mouseup events when not actively dragging

These tests prevent regressions in the draggable behavior for different positioning contexts.


999-1500: LGTM! Thorough test coverage for the new resizable feature.

The test suite comprehensively covers:

  • Edge hover cursor updates (right, bottom, left, top edges)
  • Custom handle_size configuration
  • Min/max width/height constraints with proper clamping
  • Resize lifecycle callbacks (on_resize_start, on_resize, on_resize_end)
  • Position adjustments for left/top edge resizes
  • Disabled state behavior
  • Static vs non-static positioned elements
  • Cleanup and event listener removal
  • Non-drag interaction guards

One minor observation: the on_resize_end callback test at line 1283-1286 uses the mocked offsetWidth/offsetHeight values (200, 150) rather than the actual resized dimensions (255, 150). This is expected since the mock doesn't update dynamically, but worth noting for future maintainers.

tests/vitest/FileDetails.svelte.test.ts (2)

38-79: LGTM! Toggle-all button test covers key scenarios.

Good coverage of:

  • Initial closed state verification
  • Opening all details via button click
  • Closing all details via button click
  • Handling partial open state (some manually opened, then close all)

81-126: No action needed. The test code properly uses $state for reactive state management, and the Vitest/Svelte configuration already fully supports this pattern. The @sveltejs/kit/vite plugin automatically handles preprocessing of all Svelte files, including .svelte.test.ts, and $state is already in widespread use across multiple test files in the same test suite (MultiSelect.svelte.test.ts, CmdPalette.svelte.test.ts, FileDetails.svelte.test.ts).

Likely an incorrect or invalid review comment.

src/lib/CmdPalette.svelte (1)

3-3: LGTM! Generic type extended for optional grouping support.

The Action generic now includes an optional group?: string property, enabling grouped actions in the command palette. This is a backward-compatible change that aligns with the grouping capabilities in the underlying MultiSelect component.

src/lib/FileDetails.svelte (2)

31-43: Good reactive refs management approach.

The node_refs state properly addresses the binding warning and includes memory leak prevention by trimming stale refs when the files array shrinks. The sync back to files[idx].node maintains API compatibility for external consumers.

One consideration: The effect runs whenever node_refs or files changes. The trimming logic (node_refs.splice(files.length)) mutates node_refs in place, which could trigger the effect again. However, since the condition node_refs.length > files.length will be false after the splice, it should stabilize without infinite loops.


48-54: Correct approach to read fresh DOM state in toggle_all.

The comment explains why $derived any_open isn't used here - reading the DOM state fresh via node_refs.some((node) => node?.open) ensures the toggle logic uses current state rather than potentially stale derived values. This is the right pattern for imperative DOM mutations.

tests/vitest/PrevNext.test.ts (2)

32-47: LGTM! Data-driven test pattern for prev/next link scenarios.

Using test.each with descriptive test cases (middle item, first item wraps, last item wraps) improves test readability and reduces duplication. The test clearly covers wrap-around behavior for navigation.


156-172: LGTM! Updated link_props test with preload attribute verification.

The test now verifies that data-sveltekit-preload-data="hover" is applied to navigation links, aligning with the component's updated behavior for SvelteKit preloading optimization.

src/lib/MultiSelect.svelte (3)

153-186: LGTM! Well-structured shortcut parsing utilities.

The parse_shortcut function correctly handles:

  • Case-insensitive parsing
  • Multiple modifiers (ctrl, shift, alt, meta/cmd)
  • The key as the last part after splitting on +

The matches_shortcut function properly checks all modifier keys match exactly (not just that required ones are present), which prevents false matches.

One edge case to consider: if a user specifies "a" (no modifiers), this will match any a keypress including with modifiers. This seems intentional given the exact matching of modifier booleans.


188-199: LGTM! Sensible default shortcuts with proper merging.

Default shortcuts:

  • select_all: ctrl+a - industry standard
  • clear_all: ctrl+shift+a - logical inverse with shift modifier
  • open/close: null - disabled by default

The $derived merge with user-provided shortcuts allows overriding defaults or disabling them by setting to null.


711-751: LGTM! Keyboard shortcuts checked early with proper conditions.

The shortcut handling:

  1. Checks shortcuts before other key logic (Escape, Tab, Enter, Arrow keys)
  2. Each shortcut has an appropriate condition:
    • select_all: Only when enabled and multiple options exist
    • clear_all: Only when selections exist
    • open/close: Based on current open state
  3. Properly prevents default and stops propagation to avoid browser shortcuts (e.g., Ctrl+A selecting all text)

The early return after handling a shortcut prevents the rest of handle_keydown from executing, which is the correct behavior.

src/lib/attachments.ts (2)

165-219: LGTM! Resize logic is mathematically correct.

The edge detection and resize calculations are implemented correctly:

  • Edge detection properly identifies which edge is under the cursor based on handle_size
  • Resize calculations correctly handle all four edges with proper position compensation for left/top edges
  • Clamping enforces min/max constraints as expected
  • Callbacks are invoked at appropriate lifecycle points (start, during, end)

The position adjustment math for left/top edges ensures the opposite edge stays fixed while resizing, which is the expected behavior.


221-249: LGTM! Event handling and cleanup are properly implemented.

The event handlers follow best practices:

  • Proper cleanup of global mouse listeners in on_mouseup to prevent memory leaks
  • Cursor feedback in on_hover provides clear visual indication of resizable edges
  • Comprehensive cleanup function removes all event listeners and resets cursor state
  • Preserves resized dimensions after cleanup (intentional - the resize should persist)

Also:
- Export Dimensions and ResizeCallback types from attachments.ts
- Refactor keyboard shortcut tests to use test.each for better coverage
- Add tests for disabled state, invalid shortcuts, and dropdown closed state
- Fix Toggle test onchange prop location
- Fix Wiggle test timer handling
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/vitest/Toggle.test.ts (1)

6-9: Test isolation issue remains unaddressed.

The shared document.body target without cleanup between tests causes DOM pollution. Each test mounts a new Toggle without removing previous ones, so get_input() may return stale elements.

🧹 Nitpick comments (4)
src/lib/attachments.ts (2)

26-37: Consider validating min/max constraint relationships.

The interface allows configurations where min_width > max_width or min_height > max_height, which would make resizing impossible. While the clamp function (line 160-161) handles this gracefully, it may be better to validate these constraints and warn users about invalid configurations during initialization.

🔎 Suggested validation
 export const resizable =
   (options: ResizableOptions = {}): Attachment => (element: Element) => {
     if (options.disabled) return
 
     const node = element as HTMLElement
     const {
       edges = [`right`, `bottom`],
       min_width = 50,
       min_height = 50,
       max_width = Infinity,
       max_height = Infinity,
       handle_size = 8,
       on_resize_start,
       on_resize,
       on_resize_end,
     } = options
 
+    if (min_width > max_width || min_height > max_height) {
+      console.warn(
+        `resizable: min dimensions exceed max dimensions (min_width=${min_width}, max_width=${max_width}, min_height=${min_height}, max_height=${max_height})`
+      )
+    }
+
     let active_edge: string | null = null

163-163: Document position override side effect.

The function automatically changes position: static to position: relative, which is necessary for resizing but could interfere with existing layouts. Consider documenting this behavior in JSDoc comments or the function's description to help users understand this side effect.

🔎 Suggested documentation
+// Svelte 5 attachment factory to make an element resizable by dragging its edges
+// @param options - Configuration options for resizing behavior
+// @returns Attachment function that sets up resizing on an element
+// @note Automatically sets position: relative on elements with position: static
 export const resizable =
   (options: ResizableOptions = {}): Attachment => (element: Element) => {
tests/vitest/MultiSelect.svelte.test.ts (1)

2827-4216: Excellent keyboard shortcuts test coverage!

The test suite comprehensively covers:

  • Basic shortcuts (select all, clear all)
  • Custom shortcut overrides
  • Disabled shortcuts (null)
  • Constraint respect (maxSelect, minSelect)
  • Mac keyboard support (meta/cmd)
  • Various modifier combinations
  • Disabled state blocking
  • Invalid formats handling
  • Dropdown open/closed states

The helper function test_shortcut (lines 4173-4199) is a nice touch that reduces boilerplate while maintaining clarity.

One optional enhancement to consider: add a test case for shortcuts that conflict with browser defaults (e.g., ctrl+s) to document whether the component prevents default browser behavior.

src/lib/MultiSelect.svelte (1)

153-170: Consider edge case handling in parse_shortcut.

The parse_shortcut function correctly handles common cases, but could behave unexpectedly with edge cases:

  1. Empty string: returns { key: '', ctrl: false, ... }
  2. Trailing +: "ctrl+" returns { key: '', ctrl: true, ... }
  3. Single key without modifiers: "a" returns { key: 'a', ... } (works correctly)

While these edge cases are caught by matches_shortcut returning false (line 176 checks !shortcut), the empty key will cause all events with matching modifiers to match. Consider validating the shortcut format or explicitly checking for non-empty keys.

🔎 Suggested validation
 function parse_shortcut(shortcut: string): {
   key: string
   ctrl: boolean
   shift: boolean
   alt: boolean
   meta: boolean
 } {
   const parts = shortcut.toLowerCase().split(`+`)
   const key = parts.pop() ?? ``
+  // Validate that key is non-empty after parsing
+  if (!key) {
+    console.warn(`Invalid shortcut format: "${shortcut}" (missing key)`)
+  }
   return {
     key,
     ctrl: parts.includes(`ctrl`),
     shift: parts.includes(`shift`),
     alt: parts.includes(`alt`),
     meta: parts.includes(`meta`) || parts.includes(`cmd`),
   }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aaa4b31 and 953c8f7.

📒 Files selected for processing (5)
  • src/lib/MultiSelect.svelte
  • src/lib/attachments.ts
  • tests/vitest/MultiSelect.svelte.test.ts
  • tests/vitest/Toggle.test.ts
  • tests/vitest/Wiggle.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
tests/vitest/MultiSelect.svelte.test.ts (2)
tests/vitest/index.ts (1)
  • doc_query (4-8)
src/lib/types.ts (1)
  • MultiSelectProps (117-219)
🔇 Additional comments (17)
tests/vitest/Wiggle.test.ts (3)

15-30: Excellent helper design.

The create_bindable_wiggle helper elegantly simulates Svelte's two-way binding with a getter/setter pattern, making it straightforward to test state changes triggered by the component. This is a clean, reusable approach.


32-39: LGTM!

Good baseline test verifying the component renders with expected transform properties.


41-84: Strong edge-case coverage.

The test suite thoroughly exercises the Wiggle component's auto-reset behavior across multiple scenarios (varying durations, starting states, and edge cases like duration=0). The parameterized test and comprehensive prop validation are well-structured.

tests/vitest/Toggle.test.ts (9)

11-14: LGTM!

Clean parameterized test covering both initial checked states.


16-23: LGTM!

Correctly verifies bidirectional toggle on click.


25-36: LGTM!

Correctly passes onchange via input_props and verifies Enter key toggles the checkbox while firing the change event.


47-66: LGTM!

Correctly verifies that Enter key calls preventDefault, invokes onkeydown handler, and maintains proper event ordering.


68-80: LGTM!

Good coverage of styling props, correctly distinguishing between label-level props and input_props.


82-95: LGTM!

Clean parameterized approach for event emission tests. All handlers correctly passed via input_props.


97-102: LGTM!

Simple structural smoke test verifying expected DOM elements.


104-123: LGTM!

Correctly tests event-based state synchronization. The manual change event dispatch is necessary in JSDOM since synthetic click() may not automatically fire change events.


38-45: Remove concern about Space key blocking.

The code does not prevent Space from toggling. The handle_keydown function only intercepts Enter (calling preventDefault() and simulating a click to ensure the change event fires). Space retains native checkbox behavior, as confirmed by the comment on line 18: "normally input type=checkbox toggles on space bar, this handler also responds to enter." The test uses programmatically dispatched events, which don't trigger native browser toggle behavior—a real user pressing Space would correctly toggle the input.

src/lib/attachments.ts (1)

139-249: LGTM! Well-structured resizable implementation.

The resizable attachment factory is well-implemented with:

  • Proper edge detection with configurable handle size
  • Dimension clamping to prevent invalid sizes
  • Correct position adjustment when resizing from left/top edges
  • Lifecycle callbacks for integration with parent components
  • Proper cleanup of event listeners

The implementation correctly handles the complexity of maintaining element position when resizing from edges that affect both size and position (left/top).

tests/vitest/MultiSelect.svelte.test.ts (1)

2767-2767: LGTM! Improved selector consistency.

Changing from input:not(.form-control) to input[autocomplete] is more explicit and aligns with the selector pattern used consistently throughout the test file. This makes the test more maintainable and less brittle.

src/lib/MultiSelect.svelte (3)

172-186: LGTM! Correct shortcut matching logic.

The matches_shortcut function correctly:

  • Handles null/undefined shortcuts (returns false)
  • Performs case-insensitive key matching
  • Requires exact modifier matches (prevents false positives)

The strict matching ensures that ctrl+a won't trigger on ctrl+shift+a, which is the correct behavior.


714-753: Well-implemented keyboard shortcut handling!

The shortcut handling is correctly integrated:

Proper placement: Shortcuts are checked before other key handling (line 714-753 before line 755), ensuring shortcuts take precedence over default behaviors.

Proper conditions: Each action has appropriate guards:

  • select_all: requires selectAllOption enabled, navigable options exist, and not single-select
  • clear_all: requires selected items exist
  • open: only when dropdown closed
  • close: only when dropdown open

Proper event handling: Calls preventDefault() and stopPropagation() to prevent browser defaults and bubbling.

Respects constraints: select_all and clear_all delegate to existing functions that already enforce maxSelect and minSelect constraints.


712-712: LGTM! Proper disabled state handling.

Moving the disabled check to the top of handle_keydown ensures all keyboard interactions, including shortcuts, are blocked when the component is disabled. This is the correct behavior and is properly covered by tests (test file lines 4095-4115).

Prevents malformed shortcuts like 'ctrl+' from matching any keypress
with the ctrl modifier. Now requires a non-empty key in the shortcut.

refactor: improve resizable docs and keyboard shortcut tests

- Add JSDoc documentation to resizable attachment factory
- Add validation warning when min dimensions exceed max
- Integrate preventDefault check into first keyboard shortcut test
- Remove redundant standalone preventDefault test
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 953c8f7 and 3942617.

📒 Files selected for processing (3)
  • src/lib/MultiSelect.svelte
  • src/lib/attachments.ts
  • tests/vitest/MultiSelect.svelte.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/MultiSelect.svelte
🔇 Additional comments (6)
tests/vitest/MultiSelect.svelte.test.ts (2)

2767-2767: LGTM!

The selector change from input:not(.form-control) to input[autocomplete] is consistent with other tests in this file and correctly targets the main input element.


3827-4223: Comprehensive keyboard shortcuts test coverage.

The test suite thoroughly covers:

  • Default shortcuts (ctrl+a, ctrl+shift+a)
  • Custom shortcut overrides
  • Disabling shortcuts via null
  • Constraint enforcement (maxSelect, minSelect)
  • Mac-specific modifiers (meta+a, cmd+a)
  • Complex modifier combinations
  • Disabled component state
  • Edge cases (invalid formats, no-op when already in target state)
  • Dropdown state independence

The test_shortcut helper at lines 4180-4206 is a nice pattern to reduce boilerplate.

src/lib/attachments.ts (4)

23-37: Clean type definitions for the resizable API.

The exported types provide a well-defined interface for the feature. The ResizableOptions interface is comprehensive with sensible defaults documented inline.


138-140: Side effect documented appropriately.

The auto-setting of position: relative on static elements is a reasonable default that enables the resize functionality. The doc comment on lines 138-139 correctly warns users about this behavior.

Also applies to: 170-170


202-226: Resize calculation logic is correct for single-edge resizing.

The implementation correctly:

  • Handles left/top edge resizing by adjusting position alongside dimension
  • Uses clamping to enforce min/max constraints
  • Updates styles in a performant manner

Note that corner resizing (e.g., bottom-right simultaneously) is not supported by design, which is fine given the edges interface only accepts single edges.


249-256: Cleanup function handles all listeners correctly.

The cleanup properly removes both node-level listeners (mousedown, mousemove for hover) and global listeners (mousemove, mouseup) that may have been attached during an active resize operation. This prevents memory leaks if the component unmounts mid-resize.

…x(MultiSelect): trim whitespace from shortcut parts

Prevents undefined behavior where clamp() would produce inconsistent
results when min dimensions exceed max dimensions.

Allows shortcuts with spaces like 'ctrl + a' to work correctly by
trimming whitespace from each part after splitting on '+'.
@janosh janosh changed the title feat: component improvements, resizable attachment, keyboard shortcuts, and test coverage resizable attachment + MultiSelect keyboard shortcuts + more test coverage Jan 1, 2026
@janosh janosh added enhancement New feature or request tests Test all the things types TypeScript labels Jan 1, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/lib/MultiSelect.svelte (1)

713-797: Document shortcut override behavior and add test coverage for edge cases.

Shortcuts are evaluated before built-in key handlers by design (comment on line 716 confirms "Check keyboard shortcuts first"). While default shortcut values (open=null, close=null) prevent conflicts, users can set shortcuts={{ open: "enter" }} to override Enter key behavior. This is technically possible and intentional for customization, but:

  1. The API documentation (types.ts:225-229) doesn't explicitly clarify this precedence behavior—comments hint at it ("Escape already works", "use existing behavior") but don't explicitly warn users about potential override scenarios.

  2. Test coverage includes various shortcut configurations but lacks test cases verifying shortcut overrides of built-in keys (e.g., custom open/close with Enter/Escape keys), which would validate this behavior and document expected outcomes.

Consider clarifying the KeyboardShortcuts documentation to explicitly state that custom shortcuts take precedence over built-in key handling, and add test cases demonstrating shortcut override behavior to prevent future surprises.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3942617 and 9061c80.

📒 Files selected for processing (2)
  • src/lib/MultiSelect.svelte
  • src/lib/attachments.ts
🔇 Additional comments (10)
src/lib/attachments.ts (5)

23-24: LGTM!

Type definitions are clean and appropriate for the resizable API.


26-37: LGTM!

The interface is well-designed with sensible optional parameters and follows the same callback pattern as draggable.


157-162: LGTM! Past review concern addressed.

The early return after validation prevents clamp() from producing inconsistent results when min exceeds max.


489-492: LGTM! Good security documentation.

The clarified comment explicitly warns about XSS risks when using user-provided content in tooltips, making it clear that sanitization is the caller's responsibility.


138-257: LGTM! Solid implementation with correct resize mathematics.

The resizable attachment is well-implemented:

  • Edge detection logic correctly identifies resize handles
  • Resize calculations for left/top edges properly adjust position while keeping opposite edges fixed
  • Dimension clamping prevents invalid sizes
  • Thorough cleanup and defense-in-depth listener removal
  • The automatic position: relative change for static elements is documented and necessary
  • Svelte 5.45.6 attachment API is stable and the factory pattern used here (options → Attachment function) is fully compatible with the official API
src/lib/MultiSelect.svelte (5)

10-10: LGTM: Import addition is correct.

The KeyboardShortcuts type import is properly added to support the new shortcuts feature.


148-149: LGTM: Prop initialization is appropriate.

The shortcuts prop with an empty object default allows users to optionally override specific shortcuts while inheriting defaults.


153-170: LGTM: Shortcut parsing logic is sound.

The function correctly parses modifier+key combinations and handles the cmd→meta alias for Mac compatibility. Empty or malformed shortcuts are safely handled by the matches_shortcut guard (line 179).


172-188: LGTM: Shortcut matching logic is robust.

The function correctly guards against null/undefined shortcuts and empty keys, preventing false positives like "ctrl+" matching any key with ctrl pressed. The exact modifier matching (line 185-187) is the correct behavior.


190-201: LGTM: Shortcut defaults are well-chosen.

The default shortcuts follow conventions (ctrl+a for select all), and open/close being null by default prevents unexpected behavior. The merge strategy correctly allows user overrides.

- Add explicit documentation in KeyboardShortcuts interface explaining
  that custom shortcuts are evaluated before built-in key handlers
- Add test.each for shortcut override edge cases (Enter, Escape, ArrowDown)
- Consolidate 4 individual tests into parameterized test.each for conciseness
@janosh janosh merged commit 3def2f6 into main Jan 1, 2026
6 of 7 checks passed
@janosh janosh deleted the component-improvements branch January 1, 2026 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request tests Test all the things types TypeScript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants