@citizenplane/pimp 18.25.0 → 18.25.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/package.json
CHANGED
|
@@ -63,10 +63,11 @@
|
|
|
63
63
|
</template>
|
|
64
64
|
|
|
65
65
|
<script setup lang="ts">
|
|
66
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
66
67
|
import PrimevueDrawer from 'primevue/drawer'
|
|
67
68
|
import type { MenuItem } from 'primevue/menuitem'
|
|
68
69
|
import PrimevuePopover from 'primevue/popover'
|
|
69
|
-
import { computed, nextTick, ref, useId } from 'vue'
|
|
70
|
+
import { computed, nextTick, ref, shallowRef, useId } from 'vue'
|
|
70
71
|
|
|
71
72
|
import { useIsBreakpoint } from '@/composables/useIsBreakpoint'
|
|
72
73
|
|
|
@@ -107,6 +108,7 @@ const trigger = ref<HTMLElement | null>(null)
|
|
|
107
108
|
const popover = ref<InstanceType<typeof PrimevuePopover>>()
|
|
108
109
|
const closeButton = ref<InstanceType<typeof CpButton> | null>(null)
|
|
109
110
|
const isOpen = ref(false)
|
|
111
|
+
const observedPopoverElement = shallowRef<HTMLElement | null>(null)
|
|
110
112
|
const menuId = useId()
|
|
111
113
|
|
|
112
114
|
const isMobileViewport = useIsBreakpoint(Breakpoints.TABLET)
|
|
@@ -186,30 +188,56 @@ const focusCloseButton = () => {
|
|
|
186
188
|
|
|
187
189
|
const onHide = () => {
|
|
188
190
|
isOpen.value = false
|
|
191
|
+
observedPopoverElement.value = null
|
|
189
192
|
focusTrigger()
|
|
190
193
|
emits('hide')
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
const
|
|
194
|
-
if (!popover.value) return
|
|
196
|
+
const getPopoverElements = () => {
|
|
197
|
+
if (!popover.value) return null
|
|
195
198
|
|
|
196
199
|
const { container: popoverElement, target: triggerElement } = popover.value as unknown as {
|
|
197
200
|
container: HTMLElement | null
|
|
198
201
|
target: HTMLElement | null
|
|
199
202
|
}
|
|
200
203
|
|
|
201
|
-
if (!popoverElement || !triggerElement) return
|
|
204
|
+
if (!popoverElement || !triggerElement) return null
|
|
205
|
+
|
|
206
|
+
return { popoverElement, triggerElement }
|
|
207
|
+
}
|
|
202
208
|
|
|
203
|
-
|
|
204
|
-
const
|
|
209
|
+
const getPopoverRightEdge = (popoverElement: HTMLElement) => {
|
|
210
|
+
const offsetParent = popoverElement.offsetParent as HTMLElement | null
|
|
211
|
+
const offsetParentLeft = offsetParent?.getBoundingClientRect().left ?? 0
|
|
205
212
|
|
|
206
|
-
|
|
213
|
+
return offsetParentLeft + popoverElement.offsetLeft + popoverElement.offsetWidth
|
|
207
214
|
}
|
|
208
215
|
|
|
216
|
+
const alignPopoverEnd = () => {
|
|
217
|
+
const elements = getPopoverElements()
|
|
218
|
+
if (!elements) return
|
|
219
|
+
|
|
220
|
+
const { popoverElement, triggerElement } = elements
|
|
221
|
+
popoverElement.style.marginLeft = ''
|
|
222
|
+
|
|
223
|
+
const gapAfterTriggerEnd = getPopoverRightEdge(popoverElement) - triggerElement.getBoundingClientRect().right
|
|
224
|
+
if (!gapAfterTriggerEnd) return
|
|
225
|
+
|
|
226
|
+
popoverElement.style.marginLeft = `${gapAfterTriggerEnd * -1}px`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
useResizeObserver(observedPopoverElement, alignPopoverEnd)
|
|
230
|
+
|
|
209
231
|
const onShow = async () => {
|
|
210
232
|
isOpen.value = true
|
|
211
|
-
if (isPopoverPlacementEnd.value) alignPopoverEnd()
|
|
212
233
|
emits('show')
|
|
234
|
+
|
|
235
|
+
if (!isPopoverPlacementEnd.value) return
|
|
236
|
+
|
|
237
|
+
await nextTick()
|
|
238
|
+
|
|
239
|
+
alignPopoverEnd()
|
|
240
|
+
observedPopoverElement.value = getPopoverElements()?.popoverElement ?? null
|
|
213
241
|
}
|
|
214
242
|
|
|
215
243
|
defineExpose({ show, hide, toggle })
|
|
@@ -7,6 +7,9 @@ import CpLanguageSwitcher from '@/components/CpLanguageSwitcher.vue'
|
|
|
7
7
|
const STORY_TRIGGER_LAYOUT =
|
|
8
8
|
'display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; width: 100%;'
|
|
9
9
|
|
|
10
|
+
const STORY_EDGE_TRIGGER_LAYOUT =
|
|
11
|
+
'display: flex; align-items: center; justify-content: flex-end; min-height: 60vh; width: 100%; padding: 0 16px;'
|
|
12
|
+
|
|
10
13
|
const LANGS_WITH_FLAGS = [
|
|
11
14
|
{ code: 'fr', name: 'Français', flag: 'fr' },
|
|
12
15
|
{ code: 'en', name: 'English', flag: 'gb' },
|
|
@@ -113,6 +116,31 @@ export const WithoutFlags: Story = {
|
|
|
113
116
|
}),
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
/**
|
|
120
|
+
* `placement="end"` with the trigger pinned to the right edge of the canvas, the navbar
|
|
121
|
+
* case: PrimeVue's collision handling already right-aligns the popover there, so the
|
|
122
|
+
* popover's right edge must sit flush with the trigger's, not shifted left.
|
|
123
|
+
*/
|
|
124
|
+
export const PlacementEndAtViewportEdge: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
langs: LANGS_WITH_FLAGS,
|
|
127
|
+
modelValue: 'fr',
|
|
128
|
+
placement: 'end',
|
|
129
|
+
},
|
|
130
|
+
render: (args) => ({
|
|
131
|
+
components: { CpLanguageSwitcher },
|
|
132
|
+
setup() {
|
|
133
|
+
const currentCode = ref(args.modelValue)
|
|
134
|
+
return { args, currentCode }
|
|
135
|
+
},
|
|
136
|
+
template: `
|
|
137
|
+
<div style="${STORY_EDGE_TRIGGER_LAYOUT}">
|
|
138
|
+
<CpLanguageSwitcher v-bind="args" v-model="currentCode" />
|
|
139
|
+
</div>
|
|
140
|
+
`,
|
|
141
|
+
}),
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
/**
|
|
117
145
|
* Mobile viewport — the list moves into a bottom sheet. No `sheetTitle` is passed,
|
|
118
146
|
* so the header collapses to its drag handle and the languages start right under it.
|
|
@@ -5,7 +5,7 @@ import CpButton from '@/components/CpButton.vue'
|
|
|
5
5
|
import CpMenu from '@/components/CpMenu.vue'
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
|
-
title: '
|
|
8
|
+
title: 'Organisms/CpMenu',
|
|
9
9
|
component: CpMenu,
|
|
10
10
|
parameters: {
|
|
11
11
|
docs: {
|
|
@@ -208,6 +208,42 @@ export const Placement: Story = {
|
|
|
208
208
|
}),
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The same `placement="end"` menu with its trigger pinned to the right edge of the
|
|
213
|
+
* canvas, where PrimeVue's collision handling already right-aligns the popover on the
|
|
214
|
+
* trigger. The popover's right edge must sit flush with the trigger's, not shifted left.
|
|
215
|
+
*/
|
|
216
|
+
export const PlacementEndAtViewportEdge: Story = {
|
|
217
|
+
args: {
|
|
218
|
+
forcePopover: true,
|
|
219
|
+
placement: 'end',
|
|
220
|
+
},
|
|
221
|
+
parameters: {
|
|
222
|
+
layout: 'fullscreen',
|
|
223
|
+
},
|
|
224
|
+
render: (args) => ({
|
|
225
|
+
components: { CpButton, CpMenu },
|
|
226
|
+
setup() {
|
|
227
|
+
const items = [
|
|
228
|
+
{ label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') },
|
|
229
|
+
{ label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') },
|
|
230
|
+
{ separator: true },
|
|
231
|
+
{ label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') },
|
|
232
|
+
]
|
|
233
|
+
return { args, items }
|
|
234
|
+
},
|
|
235
|
+
template: `
|
|
236
|
+
<div style="display: flex; align-items: center; justify-content: flex-end; min-height: 320px; width: 100%;">
|
|
237
|
+
<CpMenu v-bind="args" :items="items">
|
|
238
|
+
<template #trigger>
|
|
239
|
+
<CpButton>Open menu</CpButton>
|
|
240
|
+
</template>
|
|
241
|
+
</CpMenu>
|
|
242
|
+
</div>
|
|
243
|
+
`,
|
|
244
|
+
}),
|
|
245
|
+
}
|
|
246
|
+
|
|
211
247
|
/**
|
|
212
248
|
* Use the default slot to render arbitrary content inside the popover —
|
|
213
249
|
* useful for forms, share panels, filters, etc.
|