diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte index 7f5c55d..1149f78 100644 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -8,6 +8,11 @@ ref = $bindable(null), sideOffset = 4, align = 'start', + collisionPadding = 8, + avoidCollisions = true, + sticky = 'always', + hideWhenDetached = true, + dir = 'rtl', portalProps, class: className, ...restProps @@ -22,8 +27,13 @@ data-slot="dropdown-menu-content" {sideOffset} {align} + {collisionPadding} + {avoidCollisions} + {sticky} + {hideWhenDetached} + {dir} class={cn( - 'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 w-(--bits-dropdown-menu-anchor-width) min-w-32 overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden', + 'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 max-h-[min(20rem,var(--bits-dropdown-menu-content-available-height))] w-(--bits-dropdown-menu-anchor-width) min-w-32 overflow-x-hidden overflow-y-auto overscroll-contain rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden', className, )} {...restProps} diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte index 4e79986..a0273db 100644 --- a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -5,15 +5,25 @@ let { ref = $bindable(null), class: className, + collisionPadding = 8, + avoidCollisions = true, + sticky = 'always', + hideWhenDetached = true, + dir = 'rtl', ...restProps }: DropdownMenuPrimitive.SubContentProps = $props(); import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; - let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props(); + let { open = $bindable(false), dir = 'rtl', ...restProps }: DropdownMenuPrimitive.RootProps = $props(); - + diff --git a/src/lib/components/ui/floating-content.test.svelte b/src/lib/components/ui/floating-content.test.svelte new file mode 100644 index 0000000..ee33ba0 --- /dev/null +++ b/src/lib/components/ui/floating-content.test.svelte @@ -0,0 +1,26 @@ + + +
+ {#if kind === 'select'} + + انتخاب + + + {:else if kind === 'popover'} + + جزئیات + محتوای جزئیات + + {:else} + + عملیات + ویرایش + + {/if} +
diff --git a/src/lib/components/ui/floating-content.test.ts b/src/lib/components/ui/floating-content.test.ts new file mode 100644 index 0000000..88008f3 --- /dev/null +++ b/src/lib/components/ui/floating-content.test.ts @@ -0,0 +1,33 @@ +import { cleanup, render, screen } from '@testing-library/svelte'; +import { afterEach, describe, expect, it } from 'vitest'; + +import FloatingContentHarness from './floating-content.test.svelte'; + +afterEach(cleanup); + +describe('floating content primitives', () => { + it('bounds select menus to the available viewport with an internal scroll region', async () => { + render(FloatingContentHarness, { kind: 'select' }); + + const content = await screen.findByRole('listbox'); + const viewport = content.querySelector('[data-select-viewport]'); + expect(content.className).toContain('var(--bits-select-content-available-height)'); + expect(viewport?.className).toContain('overscroll-contain'); + expect(content.closest('[data-bits-floating-content-wrapper]')?.getAttribute('dir')).toBe('rtl'); + }); + + it('bounds popovers and menus to their collision-aware available height', async () => { + const popoverView = render(FloatingContentHarness, { kind: 'popover' }); + const popover = screen.getByText('محتوای جزئیات'); + expect(popover.className).toContain('var(--bits-popover-content-available-height)'); + expect(popover.closest('[data-bits-floating-content-wrapper]')?.getAttribute('dir')).toBe('rtl'); + + popoverView.unmount(); + + render(FloatingContentHarness, { kind: 'menu' }); + const menu = screen.getByRole('menu'); + expect(menu.className).toContain('var(--bits-dropdown-menu-content-available-height)'); + expect(menu.className).toContain('overscroll-contain'); + expect(menu.closest('[data-bits-floating-content-wrapper]')?.getAttribute('dir')).toBe('rtl'); + }); +}); diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte index aec952b..5d0cbad 100644 --- a/src/lib/components/ui/popover/popover-content.svelte +++ b/src/lib/components/ui/popover/popover-content.svelte @@ -9,6 +9,11 @@ class: className, sideOffset = 4, align = 'center', + collisionPadding = 8, + avoidCollisions = true, + sticky = 'always', + hideWhenDetached = true, + dir = 'rtl', portalProps, ...restProps }: PopoverPrimitive.ContentProps & { @@ -22,8 +27,13 @@ data-slot="popover-content" {sideOffset} {align} + {collisionPadding} + {avoidCollisions} + {sticky} + {hideWhenDetached} + {dir} class={cn( - 'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100', + 'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 z-50 flex max-h-[min(32rem,var(--bits-popover-content-available-height))] w-72 origin-(--transform-origin) flex-col gap-2.5 overflow-y-auto overscroll-contain rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100', className, )} {...restProps} diff --git a/src/lib/components/ui/select/select-content.svelte b/src/lib/components/ui/select/select-content.svelte index 659f7e6..672ea16 100644 --- a/src/lib/components/ui/select/select-content.svelte +++ b/src/lib/components/ui/select/select-content.svelte @@ -11,6 +11,11 @@ ref = $bindable(null), class: className, sideOffset = 4, + collisionPadding = 8, + avoidCollisions = true, + sticky = 'always', + hideWhenDetached = true, + dir = 'rtl', portalProps, children, preventScroll = true, @@ -24,17 +29,25 @@ {@render children?.()}