Skip to content

Drawer POC #1145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Drawer POC #1145

wants to merge 2 commits into from

Conversation

kotAPI
Copy link
Collaborator

@kotAPI kotAPI commented Jun 7, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a flexible Drawer component with support for customizable placement, transitions, and drag-to-open/close gestures.
    • Added subcomponents for Drawer structure, including Root, Trigger, Portal, Overlay, Content, Title, Description, and Close.
    • Enabled background scaling and visual effects when drawers are open, including support for nested and multi-directional drawers.
    • Provided comprehensive Storybook examples demonstrating various Drawer configurations, animations, and interactive behaviors.
  • Style

    • Improved code formatting in an internal hook (no user-facing changes).

Copy link
Contributor

coderabbitai bot commented Jun 7, 2025

Walkthrough

A comprehensive Drawer UI component system has been introduced, including context, root, trigger, overlay, portal, content, title, description, and close subcomponents. The Drawer supports advanced features such as nested drawers, drag-to-close, background scaling, and customizable animations. Extensive Storybook stories demonstrate its various configurations and interactive behaviors.

Changes

File(s) Change Summary
src/components/ui/Drawer/Drawer.tsx Introduced a new Drawer component as a wrapper, exposing subcomponents (Root, Trigger, Portal, Overlay, Content, Title, Description, Close) as static properties; logs a warning if used directly.
src/components/ui/Drawer/context/DrawerContext.tsx Added DrawerContext for managing drawer state and behavior; implemented useDrawerNesting hook for handling nested drawers and z-index calculation.
src/components/ui/Drawer/fragments/DrawerClose.tsx Added DrawerClose component for rendering a close button, consuming DrawerContext for styling.
src/components/ui/Drawer/fragments/DrawerContent.tsx Added DrawerContent component with animation, drag-to-close logic, and context integration for transitions and gestures.
src/components/ui/Drawer/fragments/DrawerDescription.tsx Added DrawerDescription component for rendering descriptive text with context-based styling and optional asChild support.
src/components/ui/Drawer/fragments/DrawerOverlay.tsx Added DrawerOverlay component for rendering and animating the drawer overlay, handling click events and fade transitions.
src/components/ui/Drawer/fragments/DrawerPortal.tsx Added DrawerPortal component to render children inside a dialog portal.
src/components/ui/Drawer/fragments/DrawerRoot.tsx Added DrawerRoot component managing open/close state, drag progress, context, and optional background scaling effects; supports nesting and side placement.
src/components/ui/Drawer/fragments/DrawerTitle.tsx Added DrawerTitle component for rendering a title with context-based styling and optional asChild support.
src/components/ui/Drawer/fragments/DrawerTrigger.tsx Added DrawerTrigger component for opening the nearest drawer, supporting asChild and custom click handling.
src/components/ui/Drawer/stories/Drawer.stories.tsx Added extensive Storybook stories showcasing Drawer features: different sides, animations, background scaling, nested drawers, draggable drawers, and progressive scaling.
src/core/hooks/useIsInsideFrom/index.ts Only indentation changes; no logic or export modifications.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DrawerTrigger
    participant DrawerRoot
    participant DrawerContext
    participant DrawerOverlay
    participant DrawerContent
    participant DrawerClose

    User->>DrawerTrigger: Clicks trigger
    DrawerTrigger->>DrawerRoot: Finds nearest DrawerRoot and sets open=true
    DrawerRoot->>DrawerContext: Provides context (open state, handlers)
    DrawerRoot->>DrawerOverlay: Renders overlay (if open)
    DrawerRoot->>DrawerContent: Renders content (if open)
    DrawerContent->>User: Handles drag gestures (optional)
    DrawerOverlay->>User: Handles overlay clicks (to close)
    DrawerContent->>DrawerClose: Renders close button
    DrawerClose->>DrawerRoot: Triggers close on click
    DrawerRoot->>DrawerContext: Updates context (open=false)
    DrawerOverlay->>DrawerRoot: Unmounts overlay
    DrawerContent->>DrawerRoot: Unmounts content
Loading

Poem

🐇
In the warren, drawers now slide,
With overlays, triggers, and context inside.
From left, right, top, or down below,
They nest and scale, put on a show!
Drag to close, or click away—
This rabbit’s drawers are here to stay!
🗄️✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error Exit handler never called!
npm error This is an error with npm itself. Please report this error at:
npm error https://github.com/npm/cli/issues
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-07T05_43_27_457Z-debug-0.log

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 9

🧹 Nitpick comments (10)
src/components/ui/Drawer/Drawer.tsx (1)

12-15: Consider alternative approaches for compound component pattern.

The warning-based approach works but is somewhat unusual. Consider these alternatives:

  1. Non-callable object: Export an object instead of a function
  2. Throw error: Throw an error instead of just warning

Alternative 1: Non-callable object

-const Drawer = () => {
-    console.warn('Direct usage of Drawer is not supported. Please use Drawer.Root, Drawer.Content, etc. instead.');
-    return null;
-};
+const Drawer = {} as {
+    Root: typeof DrawerRoot;
+    Trigger: typeof DrawerTrigger;
+    Portal: typeof DrawerPortal;
+    Overlay: typeof DrawerOverlay;
+    Content: typeof DrawerContent;
+    Title: typeof DrawerTitle;
+    Description: typeof DrawerDescription;
+    Close: typeof DrawerClose;
+};

Alternative 2: Throw error

-    console.warn('Direct usage of Drawer is not supported. Please use Drawer.Root, Drawer.Content, etc. instead.');
-    return null;
+    throw new Error('Direct usage of Drawer is not supported. Please use Drawer.Root, Drawer.Content, etc. instead.');
src/components/ui/Drawer/fragments/DrawerTitle.tsx (1)

5-5: Remove unused import.

DialogPrimitive is imported but never used in this component.

-import DialogPrimitive from '~/core/primitives/Dialog';
src/components/ui/Drawer/context/DrawerContext.tsx (1)

36-36: Simplify conditional check with optional chaining.

The chained property checks can be simplified using optional chaining as suggested by static analysis.

-        if (parentContext && parentContext.rootClass && parentContext.rootClass !== '') {
+        if (parentContext?.rootClass !== '') {
🧰 Tools
🪛 Biome (1.9.4)

[error] 36-36: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/ui/Drawer/fragments/DrawerRoot.tsx (1)

167-171: Consider alternative to DOM property attachment.

Directly attaching properties to DOM elements can be fragile and is generally discouraged. Consider using a ref callback or data attributes instead.

-                ref={(el) => {
-                    if (el) {
-                        (el as any).__drawerOpenChange = handleOpenChange;
-                    }
-                }}
+                ref={(el) => {
+                    if (el) {
+                        el.setAttribute('data-drawer-open-change', 'true');
+                    }
+                }}

Or better yet, consider if this property is actually needed - it may be an artifact from development.

src/components/ui/Drawer/fragments/DrawerOverlay.tsx (1)

6-6: Remove unused import.

DialogPrimitive is imported but never used in this component.

-import DialogPrimitive from '~/core/primitives/Dialog';
🧰 Tools
🪛 GitHub Check: lint

[warning] 6-6:
'DialogPrimitive' is defined but never used

src/components/ui/Drawer/fragments/DrawerTrigger.tsx (1)

19-38: Consider using React patterns instead of DOM traversal.

The current implementation relies on DOM traversal and attaching properties to DOM elements, which is fragile and goes against React's declarative paradigm. This approach could break with React updates or when the DOM structure changes.

Consider these alternatives:

  1. Use a dedicated context for each drawer instance
  2. Pass a ref callback through props
  3. Use a state management solution

Example using ref callback:

// In parent component
const triggerRef = useRef<{ open: () => void }>(null);

// Pass to trigger
<DrawerTrigger ref={triggerRef} />
src/components/ui/Drawer/fragments/DrawerContent.tsx (1)

222-231: Remove redundant case clause.

The case 'bottom': is redundant since it has the same implementation as the default case.

-case 'bottom':
 default:
     return {
         ...baseStyles,
         bottom: 0,
         left: 0,
         right: 0,
         transform: `translate3d(0, ${translatePercent}%, 0)`
     };
🧰 Tools
🪛 Biome (1.9.4)

[error] 222-222: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

src/components/ui/Drawer/stories/Drawer.stories.tsx (3)

269-275: Remove or uncomment the feature list.

This commented code appears to be a feature list that should either be displayed or removed entirely.

-{/* <ul style={{ paddingLeft: '20px', lineHeight: '1.8' }}>
-    <li>Background content scales to 95% size</li>
-    <li>Rounded corners applied to the entire page</li>
-    <li>Smooth transitions synchronized with drawer animation</li>
-    <li>Configurable scale amount and border radius</li>
-    <li>Automatic cleanup on drawer close</li>
-</ul> */}
+<ul style={{ paddingLeft: '20px', lineHeight: '1.8' }}>
+    <li>Background content scales to 95% size</li>
+    <li>Rounded corners applied to the entire page</li>
+    <li>Smooth transitions synchronized with drawer animation</li>
+    <li>Configurable scale amount and border radius</li>
+    <li>Automatic cleanup on drawer close</li>
+</ul>

318-320: Remove empty Drawer.Description component.

The empty Description component adds unnecessary DOM elements.

 </Drawer.Title>
-<Drawer.Description>
-
-</Drawer.Description>
 <div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>

1-725: Consider extracting common styles for better maintainability.

The stories use extensive inline styles which makes them hard to maintain and creates repetition. Consider extracting common styles into constants or using CSS modules/styled-components.

Example refactor:

const drawerStyles = {
    content: {
        padding: '20px',
        backgroundColor: 'white',
        borderRadius: '12px',
    },
    title: {
        margin: '0 0 8px 0',
        fontSize: '18px',
        fontWeight: 'bold',
    },
    description: {
        margin: '0 0 16px 0',
        color: '#666',
    },
} as const;

// Usage
<div style={drawerStyles.content}>
    <Drawer.Title>
        <h2 style={drawerStyles.title}>Title</h2>
    </Drawer.Title>
</div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3479ef3 and 66d3f63.

📒 Files selected for processing (12)
  • src/components/ui/Drawer/Drawer.tsx (1 hunks)
  • src/components/ui/Drawer/context/DrawerContext.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerClose.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerContent.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerDescription.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerOverlay.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerPortal.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerRoot.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerTitle.tsx (1 hunks)
  • src/components/ui/Drawer/fragments/DrawerTrigger.tsx (1 hunks)
  • src/components/ui/Drawer/stories/Drawer.stories.tsx (1 hunks)
  • src/core/hooks/useIsInsideFrom/index.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/ui/Drawer/fragments/DrawerDescription.tsx (1)
src/components/ui/Drawer/context/DrawerContext.tsx (1)
  • DrawerContext (17-29)
🪛 GitHub Check: lint
src/components/ui/Drawer/fragments/DrawerOverlay.tsx

[warning] 66-66:
Visible, non-interactive elements with click handlers must have at least one keyboard listener


[warning] 6-6:
'DialogPrimitive' is defined but never used

src/components/ui/Drawer/fragments/DrawerContent.tsx

[warning] 240-240:
Non-interactive elements should not be assigned mouse or keyboard event listeners


[warning] 240-240:
Visible, non-interactive elements with click handlers must have at least one keyboard listener

🪛 Biome (1.9.4)
src/components/ui/Drawer/context/DrawerContext.tsx

[error] 36-36: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/ui/Drawer/fragments/DrawerContent.tsx

[error] 222-222: Useless case clause.

because the default clause is present:

Unsafe fix: Remove the useless case.

(lint/complexity/noUselessSwitchCase)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (15)
src/core/hooks/useIsInsideFrom/index.ts (5)

9-9: No-op formatting change
Pure indentation adjustment on the useState initialization; logic remains unchanged.


11-15: No-op formatting change
Indentation tweaks within the useEffect early-return block; behavior is identical.


17-24: No-op formatting change
Adjusted indentation in the ancestor-traversal loop; no functional impact.


26-27: No-op formatting change
Indentation refinement for the final state update and dependency array; logic unchanged.


29-29: No-op formatting change
Indentation normalized on the hook’s return statement; functionality intact.

src/components/ui/Drawer/fragments/DrawerClose.tsx (1)

13-25: LGTM! Clean implementation with proper context usage.

The component correctly consumes the DrawerContext, handles className composition with clsx, and properly forwards props to the underlying DialogPrimitive.Action.

src/components/ui/Drawer/Drawer.tsx (1)

17-24: LGTM! Clean compound component API.

The static property assignments create a clean, discoverable API for the drawer system. This pattern enables intuitive usage like Drawer.Root, Drawer.Content, etc.

src/components/ui/Drawer/fragments/DrawerTitle.tsx (1)

13-31: Component implementation looks solid.

The asChild pattern is correctly implemented with proper fallbacks and prop merging. The use of React.cloneElement with id fallback ensures accessibility while maintaining flexibility.

src/components/ui/Drawer/context/DrawerContext.tsx (2)

17-29: Well-structured context with sensible defaults.

The context provides a comprehensive API for drawer state management with appropriate default values for all properties.


32-52: Robust nesting hook with proper error handling.

The useDrawerNesting hook correctly handles parent context detection and provides sensible fallbacks with error handling.

🧰 Tools
🪛 Biome (1.9.4)

[error] 36-36: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/ui/Drawer/fragments/DrawerRoot.tsx (2)

27-52: Excellent state management implementation.

The component properly handles both controlled and uncontrolled modes with appropriate synchronization between open state and drag progress.


60-126: Sophisticated background scaling with proper cleanup.

The background scaling effect is well-implemented with proper nesting support, smooth transitions, and thorough cleanup on unmount.

src/components/ui/Drawer/fragments/DrawerOverlay.tsx (2)

20-58: Excellent animation timing implementation.

The use of double requestAnimationFrame for smooth animation start and proper cleanup of timeouts/RAF is well-implemented.


74-84: Smart performance optimizations.

The inline styles with willChange, backfaceVisibility, and translateZ(0) provide good performance optimizations for animations.

src/components/ui/Drawer/fragments/DrawerContent.tsx (1)

19-50: Well-implemented animation state management!

The animation logic with double RAF for entering animations and proper cleanup is well thought out. The state transitions are clear and the timing synchronization with the overlay is a nice touch.

Comment on lines +5 to +7
export type DrawerPortalProps = {
children: React.ReactNode;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix type definition to include additional props.

The DrawerPortalProps type only defines children, but the component implementation accepts additional props via rest parameters (...props). This creates a type safety gap.

-export type DrawerPortalProps = {
-    children: React.ReactNode;
-}
+export type DrawerPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal> & {
+    children: React.ReactNode;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type DrawerPortalProps = {
children: React.ReactNode;
}
export type DrawerPortalProps = React.ComponentProps<typeof DialogPrimitive.Portal> & {
children: React.ReactNode;
}
🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerPortal.tsx around lines 5 to 7, the
DrawerPortalProps type only includes the children prop but the component also
accepts additional props via rest parameters. Update the DrawerPortalProps type
to extend React.HTMLAttributes or a suitable interface to include these
additional props, ensuring type safety for all passed properties.

Comment on lines +25 to +29
return (
<Comp {...childProps} id="drawer-description">
{children}
</Comp>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify non-asChild rendering logic.

Since the asChild case is now handled separately, the remaining logic can be simplified.

-    return (
-        <Comp {...childProps} id="drawer-description">
-            {children}
-        </Comp>
-    );
+    return (
+        <p className={clsx(`${rootClass}-description`, className)} {...props} id="drawer-description">
+            {children}
+        </p>
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Comp {...childProps} id="drawer-description">
{children}
</Comp>
);
return (
<p
className={clsx(`${rootClass}-description`, className)}
{...props}
id="drawer-description"
>
{children}
</p>
);
🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerDescription.tsx around lines 25 to
29, simplify the rendering logic by removing any conditional checks for the
asChild prop, since that case is handled separately. Directly return the
component with the passed childProps and children without extra branching or
conditions.

Comment on lines +14 to +23
const Comp = asChild ? React.Fragment : 'p';
const childProps = asChild ? {} : { className: clsx(`${rootClass}-description`, className), ...props };

if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
...childProps,
...children.props,
id: children.props.id || 'drawer-description'
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix asChild implementation logic.

The current asChild implementation has several issues:

  1. When asChild is true, childProps is set to an empty object (line 15), so the context-based className is never applied to cloned elements.
  2. The cloned element doesn't receive the properly composed className from the context.
-    const Comp = asChild ? React.Fragment : 'p';
-    const childProps = asChild ? {} : { className: clsx(`${rootClass}-description`, className), ...props };
-
-    if (asChild && React.isValidElement(children)) {
-        return React.cloneElement(children, {
-            ...childProps,
-            ...children.props,
-            id: children.props.id || 'drawer-description'
-        });
-    }
+    if (asChild && React.isValidElement(children)) {
+        return React.cloneElement(children, {
+            ...props,
+            ...children.props,
+            className: clsx(`${rootClass}-description`, className, children.props.className),
+            id: children.props.id || 'drawer-description'
+        });
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerDescription.tsx around lines 14 to
23, the asChild logic sets childProps to an empty object when asChild is true,
causing the context-based className to be omitted from cloned elements. To fix
this, ensure childProps always includes the composed className regardless of
asChild's value, so when cloning children, the className from context and any
additional props are properly merged and applied.

Comment on lines +66 to +73
<div
className={clsx(`${rootClass}-overlay`, className)}
onClick={(event) => {
// Only close if clicking directly on the overlay, not on nested content
if (event.target === event.currentTarget) {
handleOverlayClick();
}
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add keyboard accessibility for overlay dismissal.

The overlay should be dismissible via keyboard (ESC key) to meet accessibility standards.

-            onClick={(event) => {
+            onClick={(event) => {
                 // Only close if clicking directly on the overlay, not on nested content
                 if (event.target === event.currentTarget) {
                     handleOverlayClick();
                 }
             }}
+            onKeyDown={(event) => {
+                if (event.key === 'Escape') {
+                    handleOverlayClick();
+                }
+            }}
+            tabIndex={-1}
🧰 Tools
🪛 GitHub Check: lint

[warning] 66-66:
Visible, non-interactive elements with click handlers must have at least one keyboard listener

🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerOverlay.tsx around lines 66 to 73,
the overlay div currently only supports dismissal via mouse click. To improve
accessibility, add a keyboard event listener that listens for the ESC key press
and triggers the handleOverlayClick function to dismiss the overlay. Ensure the
div is focusable by adding a tabIndex attribute if necessary, and handle the
keydown event to detect the ESC key.

Comment on lines +42 to +46
return React.cloneElement(children, {
...children.props,
className: clsx(`${rootClass}-trigger`, className, children.props.className),
onClick: handleClick
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix props spreading order to prevent unintended overrides.

The current props spreading order allows passed props to override the component's onClick handler and className, which could break functionality.

-return React.cloneElement(children, {
-    ...children.props,
-    className: clsx(`${rootClass}-trigger`, className, children.props.className),
-    onClick: handleClick
-});
+return React.cloneElement(children, {
+    ...children.props,
+    className: clsx(`${rootClass}-trigger`, className, children.props.className),
+    onClick: (e: React.MouseEvent) => {
+        children.props.onClick?.(e);
+        handleClick(e);
+    }
+});

Similarly for ButtonPrimitive:

 <ButtonPrimitive
+    {...props}
     className={clsx(`${rootClass}-trigger`, className)}
     onClick={handleClick}
-    {...props}
 >

Also applies to: 50-53

🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerTrigger.tsx around lines 42 to 46
and 50 to 53, the order of props spreading causes passed props to override the
component's onClick handler and className, potentially breaking functionality.
To fix this, reorder the spread so that children.props and passed props come
before the explicitly set className and onClick, ensuring the component's
handlers and className take precedence and are not unintentionally overridden.

let element = event.currentTarget as HTMLElement;
while (element) {
if (element.hasAttribute('data-drawer-root')) {
const openChangeHandler = (element as any).__drawerOpenChange;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace unsafe type casting with proper type definition.

Using as any defeats TypeScript's type safety. Consider defining a proper interface for the extended HTMLElement.

-const openChangeHandler = (element as any).__drawerOpenChange;
+interface DrawerElement extends HTMLElement {
+  __drawerOpenChange?: (open: boolean) => void;
+}
+const openChangeHandler = (element as DrawerElement).__drawerOpenChange;

Additionally, consider using a WeakMap or data attribute approach instead of directly attaching properties to DOM elements for better encapsulation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const openChangeHandler = (element as any).__drawerOpenChange;
interface DrawerElement extends HTMLElement {
__drawerOpenChange?: (open: boolean) => void;
}
const openChangeHandler = (element as DrawerElement).__drawerOpenChange;
🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerTrigger.tsx at line 30, replace the
unsafe type casting using 'as any' by defining a proper TypeScript interface
that extends HTMLElement to include the __drawerOpenChange property. Then use
this interface for the element's type. Additionally, refactor the code to avoid
attaching properties directly to DOM elements by using a WeakMap or data
attributes to store and access the __drawerOpenChange handler, improving
encapsulation and type safety.

Comment on lines +52 to +171
dragThresholdRef.current = true;
setIsDragging(true);
}

// Only process drag if we've exceeded threshold
if (!dragThresholdRef.current) return;

let progress = initialProgressRef.current; // Start from initial progress

switch (side) {
case 'bottom': {
// Dragging up closes, dragging down opens
const bottomMovement = -deltaY / window.innerHeight;
progress = Math.max(0, Math.min(1, initialProgressRef.current + bottomMovement));
break;
}
case 'top': {
// Dragging down closes, dragging up opens
const topMovement = deltaY / window.innerHeight;
progress = Math.max(0, Math.min(1, initialProgressRef.current + topMovement));
break;
}
case 'right': {
// Dragging left closes, dragging right opens
const rightMovement = -deltaX / window.innerWidth;
progress = Math.max(0, Math.min(1, initialProgressRef.current + rightMovement));
break;
}
case 'left': {
// Dragging right closes, dragging left opens
const leftMovement = deltaX / window.innerWidth;
progress = Math.max(0, Math.min(1, initialProgressRef.current + leftMovement));
break;
}
}

handleDragProgress(progress);
};

const handleLocalDragEnd = () => {
// Only handle drag end if we actually started dragging
if (dragThresholdRef.current) {
setIsDragging(false);
// Snap to open or closed based on current progress
const finalProgress = dragProgress > 0.5 ? 1 : 0;
handleDragEnd(finalProgress);
}
dragThresholdRef.current = false;
};

// Mouse events
const handleMouseDown = (e: React.MouseEvent) => {
// Don't prevent default immediately - let normal clicks work
const startY = e.clientY;
const startX = e.clientX;
handleDragStart(startY, startX);

const handleMouseMove = (e: MouseEvent) => {
// Prevent default once we start dragging
if (dragThresholdRef.current) {
e.preventDefault();
}
handleDragMove(e.clientY, e.clientX, startY, startX);
};

const handleMouseUp = () => {
handleLocalDragEnd();
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};

document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};

// Touch events
const handleTouchStart = (e: React.TouchEvent) => {
const touch = e.touches[0];
const startY = touch.clientY;
const startX = touch.clientX;
handleDragStart(startY, startX);

const handleTouchMove = (e: TouchEvent) => {
// Only prevent default once we start dragging
if (dragThresholdRef.current) {
e.preventDefault();
}
const touch = e.touches[0];
handleDragMove(touch.clientY, touch.clientX, startY, startX);
};

const handleTouchEnd = () => {
handleLocalDragEnd();
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd);
};

document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract drag logic into a custom hook for better maintainability.

The drag functionality spans over 100 lines and makes the component complex. Consider extracting it into a reusable hook.

Create a custom hook:

// useDraggableDrawer.ts
export const useDraggableDrawer = ({
    side,
    dragProgress,
    handleDragProgress,
    handleDragEnd,
}: UseDraggableDrawerProps) => {
    const [isDragging, setIsDragging] = useState(false);
    const initialProgressRef = useRef(0);
    const dragThresholdRef = useRef(false);
    
    // ... move all drag logic here ...
    
    return {
        isDragging,
        handleMouseDown,
        handleTouchStart,
    };
};

Then simplify the component:

const { isDragging, handleMouseDown, handleTouchStart } = useDraggableDrawer({
    side,
    dragProgress,
    handleDragProgress,
    handleDragEnd,
});
🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerContent.tsx around lines 52 to 171,
the drag functionality code is lengthy and complex within the component. Extract
all drag-related state, refs, and handlers (handleDragStart, handleDragMove,
handleLocalDragEnd, handleMouseDown, handleTouchStart, and their inner event
handlers) into a new custom hook named useDraggableDrawer. This hook should
accept side, dragProgress, handleDragProgress, and handleDragEnd as parameters,
manage isDragging state internally, and return isDragging along with
handleMouseDown and handleTouchStart functions. Then, replace the existing drag
logic in the component with calls to this hook to simplify and improve
maintainability.

Comment on lines +240 to +253
<div
ref={containerRef}
style={getDrawerStyles()}
className={clsx(`${rootClass}-content`, className)}
role="dialog"
aria-modal="true"
data-state={isOpen ? 'open' : 'closed'}
data-side={side}
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
onClick={(event) => {
// Prevent clicks on content from propagating to overlay
event.stopPropagation();
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Add keyboard support and improve accessibility.

The drawer content has mouse/touch handlers but lacks keyboard support. Additionally, the non-interactive div with event handlers raises accessibility concerns.

Consider these improvements:

  1. Add keyboard event handlers for arrow keys to enable keyboard-based dragging
  2. Add appropriate ARIA attributes for draggable content
  3. Make the element focusable when draggable
 <div
     ref={containerRef}
     style={getDrawerStyles()}
     className={clsx(`${rootClass}-content`, className)}
     role="dialog"
     aria-modal="true"
+    aria-grabbed={isDragging}
+    tabIndex={0}
     data-state={isOpen ? 'open' : 'closed'}
     data-side={side}
     onMouseDown={handleMouseDown}
     onTouchStart={handleTouchStart}
+    onKeyDown={handleKeyDown}
     onClick={(event) => {
         // Prevent clicks on content from propagating to overlay
         event.stopPropagation();
     }}
 >

Add keyboard handler:

const handleKeyDown = (e: React.KeyboardEvent) => {
    if (!isOpen) return;
    
    switch (e.key) {
        case 'ArrowUp':
        case 'ArrowDown':
        case 'ArrowLeft':
        case 'ArrowRight':
            e.preventDefault();
            // Implement keyboard-based drag logic
            break;
        case 'Escape':
            handleDragEnd(0);
            break;
    }
};
🧰 Tools
🪛 GitHub Check: lint

[warning] 240-240:
Non-interactive elements should not be assigned mouse or keyboard event listeners


[warning] 240-240:
Visible, non-interactive elements with click handlers must have at least one keyboard listener

🤖 Prompt for AI Agents
In src/components/ui/Drawer/fragments/DrawerContent.tsx around lines 240 to 253,
the drawer content div has mouse and touch event handlers but lacks keyboard
support and accessibility features. To fix this, add a keyboard event handler
for arrow keys and Escape to enable keyboard-based dragging and closing. Make
the div focusable by adding a tabIndex attribute when draggable, and include
appropriate ARIA attributes such as aria-grabbed or aria-dropeffect to indicate
draggable content. Attach the new handleKeyDown function to the div's onKeyDown
event to handle keyboard interactions.

import Button from '~/components/ui/Button/Button';

const CloseIcon = () => {
return <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.8536 2.85355C13.0488 2.65829 13.0488 2.34171 12.8536 2.14645C12.6583 1.95118 12.3417 1.95118 12.1464 2.14645L7.5 6.79289L2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.79289 7.5L2.14645 12.1464C1.95118 12.3417 1.95118 12.6583 2.14645 12.8536C2.34171 13.0488 2.65829 13.0488 2.85355 12.8536L7.5 8.20711L12.1464 12.8536C12.3417 13.0488 12.6583 13.0488 12.8536 12.8536C13.0488 12.6583 13.0488 12.3417 12.8536 12.1464L8.20711 7.5L12.8536 2.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use React's camelCase naming for SVG attributes.

React requires camelCase for SVG attributes.

-<path d="..." fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
+<path d="..." fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path>
🤖 Prompt for AI Agents
In src/components/ui/Drawer/stories/Drawer.stories.tsx at line 8, the SVG
attributes use hyphenated names like fill-rule and clip-rule, which are not
compliant with React's camelCase naming convention. Update these attribute names
to camelCase equivalents: change fill-rule to fillRule and clip-rule to clipRule
to ensure proper rendering in React.

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.

1 participant