@alikhalilll/a-responsive-popover 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.cjs +279 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +276 -0
- package/dist/index.js.map +1 -0
- package/dist/nuxt/index.cjs +30 -0
- package/dist/nuxt/index.cjs.map +1 -0
- package/dist/nuxt/index.d.cts +15 -0
- package/dist/nuxt/index.d.ts +15 -0
- package/dist/nuxt/index.js +30 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/resolver/index.cjs +25 -0
- package/dist/resolver/index.cjs.map +1 -0
- package/dist/resolver/index.d.cts +14 -0
- package/dist/resolver/index.d.ts +14 -0
- package/dist/resolver/index.js +25 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +122 -0
- package/src/components/AResponsivePopover.vue +57 -0
- package/src/components/AResponsivePopoverContent.vue +80 -0
- package/src/components/AResponsivePopoverTrigger.vue +23 -0
- package/src/composables/useResponsivePopoverContext.ts +20 -0
- package/src/index.ts +10 -0
- package/src/nuxt/index.ts +37 -0
- package/src/resolver/index.ts +29 -0
- package/src/types.ts +39 -0
- package/web-types.json +74 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useMediaQuery } from '@vueuse/core';
|
|
4
|
+
import { APopover } from '@alikhalilll/a-popover';
|
|
5
|
+
import { ADrawer } from '@alikhalilll/a-drawer';
|
|
6
|
+
import { provideResponsivePopoverContext } from '../composables/useResponsivePopoverContext';
|
|
7
|
+
import type { AResponsivePopoverProps, AResponsivePopoverSlots } from '../types';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<AResponsivePopoverProps>(), {
|
|
10
|
+
breakpoint: '(min-width: 768px)',
|
|
11
|
+
modal: true,
|
|
12
|
+
scrollLock: 'events',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
defineSlots<AResponsivePopoverSlots>();
|
|
16
|
+
|
|
17
|
+
const open = defineModel<boolean>('open');
|
|
18
|
+
|
|
19
|
+
const isDesktop = useMediaQuery(() => props.breakpoint);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pre-imported on both branches — do NOT lazy-load. Switching the component identity at runtime
|
|
23
|
+
* means we still hydrate the right tree client-side.
|
|
24
|
+
*/
|
|
25
|
+
const Root = computed(() => (isDesktop.value ? APopover : ADrawer));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-branch `modal` resolution — the two roots interpret the prop differently:
|
|
29
|
+
*
|
|
30
|
+
* APopover (desktop, reka-ui): `modal=true` triggers `PopoverContentModal` + its
|
|
31
|
+
* `useBodyScrollLock`. We only want that when the caller explicitly opted into the
|
|
32
|
+
* body-level scroll lock; for `'events'`/`'none'` we install our own lock in
|
|
33
|
+
* `AResponsivePopoverContent`. Legacy `modal=false` still forces non-modal.
|
|
34
|
+
*
|
|
35
|
+
* ADrawer (mobile, vaul-vue): `modal=false` SUPPRESSES THE OVERLAY entirely. Drawers
|
|
36
|
+
* are modal by convention (a dimmed backdrop is the affordance), so default to modal
|
|
37
|
+
* unless the caller explicitly turned the whole thing off.
|
|
38
|
+
*/
|
|
39
|
+
const rekaModal = computed(() => {
|
|
40
|
+
if (props.modal === false) return false;
|
|
41
|
+
return props.scrollLock === 'body';
|
|
42
|
+
});
|
|
43
|
+
const drawerModal = computed(() => props.modal !== false);
|
|
44
|
+
const rootModal = computed(() => (isDesktop.value ? rekaModal.value : drawerModal.value));
|
|
45
|
+
|
|
46
|
+
provideResponsivePopoverContext({
|
|
47
|
+
open: computed(() => open.value ?? false),
|
|
48
|
+
isDesktop: computed(() => isDesktop.value),
|
|
49
|
+
scrollLock: computed(() => props.scrollLock),
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<component :is="Root" v-model:open="open" :modal="rootModal" data-slot="responsive-popover">
|
|
55
|
+
<slot :is-desktop="isDesktop" />
|
|
56
|
+
</component>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'vue';
|
|
3
|
+
import { computed } from 'vue';
|
|
4
|
+
import { useMediaQuery } from '@vueuse/core';
|
|
5
|
+
import { APopoverContent, useEventScrollLock } from '@alikhalilll/a-popover';
|
|
6
|
+
import { ADrawerContent } from '@alikhalilll/a-drawer';
|
|
7
|
+
import { useResponsivePopoverContext } from '../composables/useResponsivePopoverContext';
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
breakpoint?: string;
|
|
12
|
+
/** Classes applied on both branches. Avoid width / inset classes here. */
|
|
13
|
+
class?: HTMLAttributes['class'];
|
|
14
|
+
/** Classes applied only when the popover (desktop) branch is rendered. */
|
|
15
|
+
popoverClass?: HTMLAttributes['class'];
|
|
16
|
+
/** Classes applied only when the drawer (mobile) branch is rendered. */
|
|
17
|
+
drawerClass?: HTMLAttributes['class'];
|
|
18
|
+
/**
|
|
19
|
+
* Render the dimmed overlay on the desktop popover branch. Defaults to `false` — popovers
|
|
20
|
+
* on desktop are non-modal-looking by convention. The mobile drawer always has its own
|
|
21
|
+
* overlay (vaul-vue's `DrawerOverlay`) regardless of this prop.
|
|
22
|
+
*/
|
|
23
|
+
overlay?: boolean;
|
|
24
|
+
align?: 'start' | 'center' | 'end';
|
|
25
|
+
sideOffset?: number;
|
|
26
|
+
}>(),
|
|
27
|
+
{
|
|
28
|
+
breakpoint: '(min-width: 768px)',
|
|
29
|
+
align: 'start',
|
|
30
|
+
sideOffset: 4,
|
|
31
|
+
overlay: false,
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const ctx = useResponsivePopoverContext();
|
|
36
|
+
|
|
37
|
+
// Prefer the root's media query (so both layers agree). Fall back to a local one when this
|
|
38
|
+
// component is used outside `AResponsivePopover` (unusual but supported).
|
|
39
|
+
const fallbackIsDesktop = useMediaQuery(() => props.breakpoint);
|
|
40
|
+
const isDesktop = computed(() => ctx?.isDesktop.value ?? fallbackIsDesktop.value);
|
|
41
|
+
|
|
42
|
+
const scrollLockMode = computed(() => ctx?.scrollLock.value ?? 'events');
|
|
43
|
+
const overlayLockScroll = computed(() => scrollLockMode.value === 'body');
|
|
44
|
+
|
|
45
|
+
const mergedClass = computed(() => [
|
|
46
|
+
props.class,
|
|
47
|
+
isDesktop.value ? props.popoverClass : props.drawerClass,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Sticky-safe scroll lock — only active while the popover is open on desktop and the root
|
|
51
|
+
// asked for the event-based strategy. The getter resolves every responsive popover content
|
|
52
|
+
// element currently in the DOM, which lets stacked popovers share the lock cleanly.
|
|
53
|
+
useEventScrollLock({
|
|
54
|
+
allowedScrollContainer: () => {
|
|
55
|
+
if (typeof document === 'undefined') return [];
|
|
56
|
+
return Array.from(
|
|
57
|
+
document.querySelectorAll<HTMLElement>('[data-responsive-popover-scroll-container="true"]')
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
active: computed(() => !!ctx?.open.value && isDesktop.value && scrollLockMode.value === 'events'),
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<APopoverContent
|
|
66
|
+
v-if="isDesktop"
|
|
67
|
+
:overlay="props.overlay"
|
|
68
|
+
:overlay-lock-scroll="overlayLockScroll"
|
|
69
|
+
:align="props.align"
|
|
70
|
+
:side-offset="props.sideOffset"
|
|
71
|
+
:class="mergedClass"
|
|
72
|
+
data-slot="responsive-popover-content"
|
|
73
|
+
data-responsive-popover-scroll-container="true"
|
|
74
|
+
>
|
|
75
|
+
<slot />
|
|
76
|
+
</APopoverContent>
|
|
77
|
+
<ADrawerContent v-else :class="mergedClass" data-slot="responsive-popover-content">
|
|
78
|
+
<slot />
|
|
79
|
+
</ADrawerContent>
|
|
80
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useMediaQuery } from '@vueuse/core';
|
|
4
|
+
import { APopoverTrigger } from '@alikhalilll/a-popover';
|
|
5
|
+
import { ADrawerTrigger } from '@alikhalilll/a-drawer';
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
breakpoint?: string;
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
}>(),
|
|
12
|
+
{ breakpoint: '(min-width: 768px)' }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const isDesktop = useMediaQuery(() => props.breakpoint);
|
|
16
|
+
const Trigger = computed(() => (isDesktop.value ? APopoverTrigger : ADrawerTrigger));
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<component :is="Trigger" :as-child="props.asChild" data-slot="responsive-popover-trigger">
|
|
21
|
+
<slot />
|
|
22
|
+
</component>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { inject, provide, type ComputedRef, type InjectionKey } from 'vue';
|
|
2
|
+
import type { ScrollLockMode } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ResponsivePopoverContext {
|
|
5
|
+
open: ComputedRef<boolean>;
|
|
6
|
+
isDesktop: ComputedRef<boolean>;
|
|
7
|
+
scrollLock: ComputedRef<ScrollLockMode>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const RESPONSIVE_POPOVER_CONTEXT: InjectionKey<ResponsivePopoverContext> = Symbol(
|
|
11
|
+
'AResponsivePopoverContext'
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export function provideResponsivePopoverContext(ctx: ResponsivePopoverContext) {
|
|
15
|
+
provide(RESPONSIVE_POPOVER_CONTEXT, ctx);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useResponsivePopoverContext(): ResponsivePopoverContext | null {
|
|
19
|
+
return inject(RESPONSIVE_POPOVER_CONTEXT, null);
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as AResponsivePopover } from './components/AResponsivePopover.vue';
|
|
2
|
+
export { default as AResponsivePopoverTrigger } from './components/AResponsivePopoverTrigger.vue';
|
|
3
|
+
export { default as AResponsivePopoverContent } from './components/AResponsivePopoverContent.vue';
|
|
4
|
+
|
|
5
|
+
export type {
|
|
6
|
+
AResponsivePopoverProps,
|
|
7
|
+
AResponsivePopoverEmits,
|
|
8
|
+
AResponsivePopoverSlots,
|
|
9
|
+
ScrollLockMode,
|
|
10
|
+
} from './types';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineNuxtModule, addComponent } from '@nuxt/kit';
|
|
2
|
+
import type { NuxtModule } from '@nuxt/schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@alikhalilll/a-responsive-popover/nuxt` — registers the responsive-popover
|
|
6
|
+
* components for Nuxt auto-import. Also import the a-popover and a-drawer
|
|
7
|
+
* stylesheets (this component renders them), plus a-ui-base tokens.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ModuleOptions {
|
|
11
|
+
prefix?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const COMPONENTS: Record<string, string> = {
|
|
15
|
+
AResponsivePopover: '@alikhalilll/a-responsive-popover',
|
|
16
|
+
AResponsivePopoverContent: '@alikhalilll/a-responsive-popover',
|
|
17
|
+
AResponsivePopoverTrigger: '@alikhalilll/a-responsive-popover',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const module: NuxtModule<ModuleOptions> = defineNuxtModule<ModuleOptions>({
|
|
21
|
+
meta: {
|
|
22
|
+
name: '@alikhalilll/a-responsive-popover',
|
|
23
|
+
configKey: 'aResponsivePopover',
|
|
24
|
+
compatibility: { nuxt: '>=3.0.0' },
|
|
25
|
+
},
|
|
26
|
+
defaults: { prefix: '' },
|
|
27
|
+
setup(opts) {
|
|
28
|
+
const prefix = opts.prefix ?? '';
|
|
29
|
+
for (const [exportName, from] of Object.entries(COMPONENTS)) {
|
|
30
|
+
const baseName = exportName.startsWith('A') ? exportName.slice(1) : exportName;
|
|
31
|
+
const registeredName = `${prefix}${prefix ? baseName : exportName}`;
|
|
32
|
+
addComponent({ name: registeredName, export: exportName, filePath: from });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export default module;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ComponentResolver } from 'unplugin-vue-components';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@alikhalilll/a-responsive-popover/resolver` — auto-import resolver for
|
|
5
|
+
* non-Nuxt Vite/Webpack consumers via `unplugin-vue-components`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ResolverOptions {
|
|
9
|
+
prefix?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const COMPONENT_TO_ENTRY: Record<string, string> = {
|
|
13
|
+
AResponsivePopover: '@alikhalilll/a-responsive-popover',
|
|
14
|
+
AResponsivePopoverContent: '@alikhalilll/a-responsive-popover',
|
|
15
|
+
AResponsivePopoverTrigger: '@alikhalilll/a-responsive-popover',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function AResponsivePopoverResolver(opts: ResolverOptions = {}): ComponentResolver {
|
|
19
|
+
const prefix = opts.prefix ?? '';
|
|
20
|
+
return {
|
|
21
|
+
type: 'component',
|
|
22
|
+
resolve(name) {
|
|
23
|
+
const bare = prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
24
|
+
const from = COMPONENT_TO_ENTRY[bare];
|
|
25
|
+
if (!from) return;
|
|
26
|
+
return { name: bare, from };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type ScrollLockMode = 'events' | 'body' | 'none';
|
|
2
|
+
|
|
3
|
+
export interface AResponsivePopoverProps {
|
|
4
|
+
/** CSS media query for the desktop break. Below this width we render a vaul drawer. */
|
|
5
|
+
breakpoint?: string;
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated prefer `scrollLock`. Kept for back-compat: `modal=false` is a shorthand
|
|
8
|
+
* for `scrollLock="none"` (tooltip-style popover). `modal=true` (default) defers to
|
|
9
|
+
* `scrollLock`, which controls how page scroll is blocked.
|
|
10
|
+
*/
|
|
11
|
+
modal?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* How desktop page scroll is blocked while the popover is open:
|
|
14
|
+
* - `'events'` (default) — wheel/touch/keyboard intercepted at document level.
|
|
15
|
+
* Page scrollbar stays visible; `position: sticky` keeps working.
|
|
16
|
+
* - `'body'` — legacy `document.body.style.overflow='hidden'` lock. Use when the
|
|
17
|
+
* page must reflow as the scrollbar goes away.
|
|
18
|
+
* - `'none'` — no page-scroll lock at all.
|
|
19
|
+
*
|
|
20
|
+
* Drawer (mobile) branch is unaffected — vaul-vue owns its own lock.
|
|
21
|
+
*/
|
|
22
|
+
scrollLock?: ScrollLockMode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Emit map for {@link AResponsivePopover}. `update:open` is the `v-model:open`.
|
|
27
|
+
*/
|
|
28
|
+
export type AResponsivePopoverEmits = {
|
|
29
|
+
'update:open': [value: boolean | undefined];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Slot prop shape for {@link AResponsivePopover}. The default slot receives
|
|
34
|
+
* `isDesktop` so consumers can branch content on the active breakpoint without
|
|
35
|
+
* a separate `useMediaQuery` call.
|
|
36
|
+
*/
|
|
37
|
+
export interface AResponsivePopoverSlots {
|
|
38
|
+
default?: (props: { isDesktop: boolean }) => unknown;
|
|
39
|
+
}
|
package/web-types.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/web-types",
|
|
3
|
+
"name": "@alikhalilll/a-responsive-popover",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"js-types-syntax": "typescript",
|
|
6
|
+
"description-markup": "markdown",
|
|
7
|
+
"framework": "vue",
|
|
8
|
+
"framework-config": {
|
|
9
|
+
"enable-when": {
|
|
10
|
+
"node-packages": [
|
|
11
|
+
"vue"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"contributions": {
|
|
16
|
+
"html": {
|
|
17
|
+
"vue-components": [
|
|
18
|
+
{
|
|
19
|
+
"name": "AResponsivePopover",
|
|
20
|
+
"source": {
|
|
21
|
+
"module": "@alikhalilll/a-responsive-popover",
|
|
22
|
+
"symbol": "AResponsivePopover"
|
|
23
|
+
},
|
|
24
|
+
"props": [
|
|
25
|
+
{
|
|
26
|
+
"name": "breakpoint",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"required": false,
|
|
29
|
+
"description": "CSS media query for the desktop break. Below this width we render a vaul drawer."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "modal",
|
|
33
|
+
"type": "boolean",
|
|
34
|
+
"required": false,
|
|
35
|
+
"deprecated": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "scrollLock",
|
|
39
|
+
"type": "ScrollLockMode",
|
|
40
|
+
"required": false,
|
|
41
|
+
"description": "How desktop page scroll is blocked while the popover is open:\n- `'events'` (default) — wheel/touch/keyboard intercepted at document level.\n Page scrollbar stays visible; `position: sticky` keeps working.\n- `'body'` — legacy `document.body.style.overflow='hidden'` lock. Use when the\n page must reflow as the scrollbar goes away.\n- `'none'` — no page-scroll lock at all.\n\nDrawer (mobile) branch is unaffected — vaul-vue owns its own lock."
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"slots": [
|
|
45
|
+
{
|
|
46
|
+
"name": "default"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"js": {
|
|
50
|
+
"events": [
|
|
51
|
+
{
|
|
52
|
+
"name": "update:open"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "AResponsivePopoverTrigger",
|
|
59
|
+
"source": {
|
|
60
|
+
"module": "@alikhalilll/a-responsive-popover",
|
|
61
|
+
"symbol": "AResponsivePopoverTrigger"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "AResponsivePopoverContent",
|
|
66
|
+
"source": {
|
|
67
|
+
"module": "@alikhalilll/a-responsive-popover",
|
|
68
|
+
"symbol": "AResponsivePopoverContent"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|