Skip to content

Commit 80add86

Browse files
committed
feat(popup): opens popup menu in a drawer on mobile
Fixes #2736
1 parent 99abd63 commit 80add86

19 files changed

Lines changed: 155 additions & 48 deletions

File tree

projects/client/src/lib/components/buttons/popup/PopupMenu.svelte

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,74 @@
11
<script lang="ts">
2+
import Drawer from "$lib/components/drawer/Drawer.svelte";
23
import MoreIcon from "$lib/components/icons/MoreIcon.svelte";
3-
import { usePortal } from "$lib/features/portal/usePortal";
4+
import { useMedia, WellKnownMediaQuery } from "$lib/stores/css/useMedia";
45
import { disableTransitionOn } from "$lib/utils/actions/disableTransitionOn";
56
import { slide } from "svelte/transition";
67
import type { PopupMenuProps } from "./PopupMenuProps";
8+
import { usePopupMenu } from "./_internal/usePopupMenu";
79
810
const {
911
items,
1012
mode = "overlay",
1113
size = "small",
1214
icon,
1315
label,
16+
title,
1417
...props
1518
}: PopupMenuProps = $props();
1619
17-
const { portalTrigger, portal, isOpened } = usePortal();
18-
</script>
20+
const isMobile = useMedia(WellKnownMediaQuery.mobile);
21+
const variant = $derived($isMobile ? "drawer" : "portal");
1922
20-
<button
21-
use:disableTransitionOn={"touch"}
22-
use:portalTrigger
23-
aria-haspopup="true"
24-
aria-label={label}
25-
aria-expanded={$isOpened}
26-
class="trakt-popup-menu-button"
27-
data-mode={mode}
28-
data-size={size}
29-
class:has-custom-icon={!!icon}
30-
{...props}
31-
>
32-
{#if icon}
33-
{@render icon()}
34-
{:else}
35-
<MoreIcon {size} />
36-
{/if}
37-
</button>
23+
const { menuTrigger, portal, isOpened, close } = $derived(
24+
usePopupMenu({ variant }),
25+
);
26+
</script>
3827

39-
{#if $isOpened}
40-
<div
41-
use:portal
42-
class="trakt-popup-menu-container"
43-
transition:slide={{ duration: 150 }}
28+
{#key variant}
29+
<button
30+
use:disableTransitionOn={"touch"}
31+
use:menuTrigger
32+
aria-haspopup="true"
33+
aria-label={label}
34+
aria-expanded={$isOpened}
35+
class="trakt-popup-menu-button"
36+
data-mode={mode}
37+
data-size={size}
38+
data-variant={variant}
39+
class:has-custom-icon={!!icon}
40+
class:has-drawer-open={$isOpened && variant === "drawer"}
41+
{...props}
4442
>
45-
<div class="spacer"></div>
46-
<ul>
47-
{@render items()}
48-
</ul>
49-
</div>
43+
{#if icon}
44+
{@render icon()}
45+
{:else}
46+
<MoreIcon {size} />
47+
{/if}
48+
</button>
49+
{/key}
50+
51+
{#if variant === "drawer"}
52+
{#if $isOpened}
53+
<Drawer onClose={close} {title} size="auto">
54+
<ul class="popup-menu-drawer-item">
55+
{@render items()}
56+
</ul>
57+
</Drawer>
58+
{/if}
59+
{:else}
60+
{#if $isOpened}
61+
<div
62+
use:portal
63+
class="trakt-popup-menu-container"
64+
transition:slide={{ duration: 150 }}
65+
>
66+
<div class="spacer"></div>
67+
<ul>
68+
{@render items()}
69+
</ul>
70+
</div>
71+
{/if}
5072
{/if}
5173

5274
<style lang="scss">
@@ -143,6 +165,7 @@
143165
}
144166
}
145167
168+
&.has-drawer-open,
146169
&[data-popup-state="opened"] {
147170
:global(svg) {
148171
transform: rotate(90deg);
@@ -187,4 +210,17 @@
187210
height: calc($button-size + $button-padding * 2 + var(--list-padding));
188211
}
189212
}
213+
214+
.popup-menu-drawer-item {
215+
all: unset;
216+
217+
display: grid;
218+
grid-template-columns: 100%;
219+
gap: var(--gap-xxs);
220+
221+
:global(li) {
222+
width: 100%;
223+
box-sizing: border-box;
224+
}
225+
}
190226
</style>

projects/client/src/lib/components/buttons/popup/PopupMenuProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Snippet } from 'svelte';
33
export type PopupMenuProps =
44
& {
55
items: Snippet;
6+
title: string;
67
icon?: Snippet;
78
mode?: 'overlay' | 'standalone';
89
size?: 'small' | 'normal';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { usePortal } from '$lib/features/portal/usePortal.ts';
2+
import { NOOP_FN } from '$lib/utils/constants.ts';
3+
import { BehaviorSubject } from 'rxjs';
4+
5+
type UsePopupMenuProps = {
6+
variant: 'portal' | 'drawer';
7+
};
8+
9+
export function usePopupMenu({ variant }: UsePopupMenuProps) {
10+
if (variant === 'portal') {
11+
const { portalTrigger, ...rest } = usePortal();
12+
13+
return {
14+
menuTrigger: portalTrigger,
15+
...rest,
16+
};
17+
}
18+
19+
const isOpened = new BehaviorSubject(false);
20+
21+
const openMenu = () => {
22+
isOpened.next(true);
23+
};
24+
25+
const menuTrigger = (targetNode: HTMLElement) => {
26+
targetNode.addEventListener('click', openMenu);
27+
28+
return {
29+
destroy() {
30+
targetNode.removeEventListener('click', openMenu);
31+
},
32+
};
33+
};
34+
35+
return {
36+
menuTrigger,
37+
portal: NOOP_FN,
38+
isOpened: isOpened.asObservable(),
39+
close: () => {
40+
isOpened.next(false);
41+
},
42+
};
43+
}

projects/client/src/lib/features/calendar/CalendarMediaCard.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@
6969
<RenderFor audience="authenticated">
7070
<CardActionBar>
7171
{#snippet actions()}
72-
<PopupMenu label={m.button_label_popup_menu({ title: media.title })}>
72+
<PopupMenu
73+
label={m.button_label_popup_menu({ title: media.title })}
74+
title={media.title}
75+
>
7376
{#snippet items()}
7477
{@render popupActions()}
7578
{/snippet}

projects/client/src/lib/features/portal/usePortal.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { usePopupHelpers } from '$lib/features/portal/_internal/usePopupHelpers.
55
import { clickOutside } from '$lib/utils/actions/clickOutside.ts';
66
import { NOOP_FN } from '$lib/utils/constants.ts';
77
import { BehaviorSubject } from 'rxjs';
8-
import { onMount } from 'svelte';
98
import { isTargetContained } from './_internal/isTargetContained.ts';
109
import type { PopupPlacement } from './_internal/models/PopupPlacement.ts';
1110

@@ -74,15 +73,14 @@ export function usePortal(props?: PortalProps) {
7473
closeHandler();
7574
};
7675
const toggleAroundTarget = () => toggleHandler(targetNode);
76+
const { destroy: destroyClickOutside } = clickOutside(targetNode);
7777

78-
onMount(() => {
79-
clickOutside(targetNode);
80-
targetNode.addEventListener('clickoutside', closeAroundTarget);
81-
targetNode.addEventListener('click', toggleAroundTarget);
82-
});
78+
targetNode.addEventListener('clickoutside', closeAroundTarget);
79+
targetNode.addEventListener('click', toggleAroundTarget);
8380

8481
return {
8582
destroy() {
83+
destroyClickOutside();
8684
targetNode.removeEventListener('clickoutside', closeAroundTarget);
8785
targetNode.removeEventListener('click', toggleAroundTarget);
8886
removeHelpers(null);

projects/client/src/lib/sections/lists/components/EpisodeCard.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@
4646
{#if externalPopupActions}
4747
<CardActionBar>
4848
{#snippet actions()}
49-
<PopupMenu label={m.button_label_popup_menu({ title: episode.title })}>
49+
<PopupMenu
50+
label={m.button_label_popup_menu({ title: episode.title })}
51+
title={episode.title}
52+
>
5053
{#snippet items()}
5154
{@render externalPopupActions()}
5255
{/snippet}

projects/client/src/lib/sections/lists/components/MediaCard.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
{#if popupActions}
4343
<CardActionBar>
4444
{#snippet actions()}
45-
<PopupMenu label={m.button_label_popup_menu({ title: media.title })}>
45+
<PopupMenu
46+
label={m.button_label_popup_menu({ title: media.title })}
47+
title={media.title}
48+
>
4649
{#snippet items()}
4750
{@render popupActions()}
4851
{/snippet}

projects/client/src/lib/sections/lists/components/MediaSummaryCard.svelte

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838
} & SeasonCardProps;
3939
4040
type ItemCardProps =
41-
| MediaCardProps
42-
| EpisodeSummaryProps
43-
| SeasonSummaryProps;
41+
MediaCardProps | EpisodeSummaryProps | SeasonSummaryProps;
4442
4543
type SummaryCardProps = {
4644
contextualTag?: Snippet;
@@ -159,6 +157,7 @@
159157
<PopupMenu
160158
label={m.button_label_popup_menu({ title: media.title })}
161159
mode="standalone"
160+
title={media.title}
162161
>
163162
{#snippet items()}
164163
{@render popupActions()}

projects/client/src/lib/sections/lists/components/SeasonItem.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
label={m.button_label_popup_menu({
7878
title: seasonLabel(season.number),
7979
})}
80+
title={seasonLabel(season.number)}
8081
>
8182
{#snippet items()}
8283
{@render popupActions()}

projects/client/src/lib/sections/lists/season/_internal/SeasonPopupMenu.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<PopupMenu
2424
label={m.button_label_popup_menu({ title })}
2525
mode="standalone"
26+
{title}
2627
{disabled}
2728
>
2829
{#snippet items()}

0 commit comments

Comments
 (0)