1<script lang="ts">
2 import { onDestroy, onMount } from 'svelte';
3 import { Select as SelectPrimitive } from 'bits-ui';
4 import SelectScrollUpButton from './select-scroll-up-button.svelte';
5 import SelectScrollDownButton from './select-scroll-down-button.svelte';
6 import { cn, type WithoutChild } from '$lib/components/ui/utils.js';
7
8 let {
9 ref = $bindable(null),
10 class: className,
11 sideOffset = 4,
12 portalProps,
13 children,
14 ...restProps
15 }: WithoutChild<SelectPrimitive.ContentProps> & {
16 portalProps?: SelectPrimitive.PortalProps;
17 } = $props();
18
19 let cleanupInternalListeners: (() => void) | undefined;
20
21 onMount(() => {
22 const listenerOptions: AddEventListenerOptions = { passive: false };
23
24 const blockOutsideWheel = (event: WheelEvent) => {
25 if (!ref) {
26 return;
27 }
28
29 const target = event.target as Node | null;
30
31 if (!target || !ref.contains(target)) {
32 event.preventDefault();
33 event.stopPropagation();
34 }
35 };
36
37 const blockOutsideTouchMove = (event: TouchEvent) => {
38 if (!ref) {
39 return;
40 }
41
42 const target = event.target as Node | null;
43
44 if (!target || !ref.contains(target)) {
45 event.preventDefault();
46 event.stopPropagation();
47 }
48 };
49
50 document.addEventListener('wheel', blockOutsideWheel, listenerOptions);
51 document.addEventListener('touchmove', blockOutsideTouchMove, listenerOptions);
52
53 return () => {
54 document.removeEventListener('wheel', blockOutsideWheel, listenerOptions);
55 document.removeEventListener('touchmove', blockOutsideTouchMove, listenerOptions);
56 };
57 });
58
59 $effect(() => {
60 const element = ref;
61
62 cleanupInternalListeners?.();
63
64 if (!element) {
65 return;
66 }
67
68 const stopWheelPropagation = (event: WheelEvent) => {
69 event.stopPropagation();
70 };
71
72 const stopTouchPropagation = (event: TouchEvent) => {
73 event.stopPropagation();
74 };
75
76 element.addEventListener('wheel', stopWheelPropagation);
77 element.addEventListener('touchmove', stopTouchPropagation);
78
79 cleanupInternalListeners = () => {
80 element.removeEventListener('wheel', stopWheelPropagation);
81 element.removeEventListener('touchmove', stopTouchPropagation);
82 };
83 });
84
85 onDestroy(() => {
86 cleanupInternalListeners?.();
87 });
88</script>
89
90<SelectPrimitive.Portal {...portalProps}>
91 <SelectPrimitive.Content
92 bind:ref
93 {sideOffset}
94 data-slot="select-content"
95 class={cn(
96 'relative z-[var(--layer-popover,1000000)] max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:-translate-x-1 data-[side=left]:slide-in-from-right-2 data-[side=right]:translate-x-1 data-[side=right]:slide-in-from-left-2 data-[side=top]:-translate-y-1 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
97 className
98 )}
99 {...restProps}
100 >
101 <SelectScrollUpButton />
102 <SelectPrimitive.Viewport
103 class={cn(
104 'h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1'
105 )}
106 >
107 {@render children?.()}
108 </SelectPrimitive.Viewport>
109 <SelectScrollDownButton />
110 </SelectPrimitive.Content>
111</SelectPrimitive.Portal>