@citizenplane/pimp 18.9.10 → 18.9.11
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/dist/components/CpMenu.vue.d.ts +5 -2
- package/dist/components/CpMenu.vue.d.ts.map +1 -1
- package/dist/components/CpTooltip.vue.d.ts +1 -1
- package/dist/components/CpTooltip.vue.d.ts.map +1 -1
- package/dist/pimp.es.js +596 -586
- package/dist/pimp.umd.js +14 -14
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CpMenu.vue +73 -30
- package/src/stories/CpMenu.stories.ts +48 -3
package/package.json
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cpMenu">
|
|
3
3
|
<div v-if="isDrawer" class="cpMenu__drawerOverlay" :class="drawerOverlayDynamicClass" />
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
|
|
5
|
+
<div class="cpMenu__anchor">
|
|
6
|
+
<div
|
|
7
|
+
ref="trigger"
|
|
8
|
+
:aria-controls="menuId"
|
|
9
|
+
:aria-expanded="isOpen"
|
|
10
|
+
:aria-haspopup="popupType"
|
|
11
|
+
class="cpMenu__trigger"
|
|
12
|
+
@click="toggle"
|
|
13
|
+
>
|
|
14
|
+
<slot name="trigger" />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<primevue-popover
|
|
18
|
+
v-if="!isDrawer"
|
|
19
|
+
ref="popover"
|
|
20
|
+
append-to="self"
|
|
21
|
+
close-on-escape
|
|
22
|
+
dismissable
|
|
23
|
+
:pt="popoverPt"
|
|
24
|
+
@hide="onHide"
|
|
25
|
+
@show="onShow"
|
|
26
|
+
>
|
|
27
|
+
<cp-menu-list v-if="hasItems" :id="menuId" :items="items" @async-complete="onItemClick" @click="onItemClick" />
|
|
28
|
+
<slot v-else />
|
|
29
|
+
</primevue-popover>
|
|
13
30
|
</div>
|
|
14
31
|
|
|
15
32
|
<primevue-drawer
|
|
@@ -43,19 +60,6 @@
|
|
|
43
60
|
<cp-menu-list v-if="hasItems" :id="menuId" :items="items" @async-complete="onItemClick" @click="onItemClick" />
|
|
44
61
|
<slot v-else />
|
|
45
62
|
</primevue-drawer>
|
|
46
|
-
|
|
47
|
-
<primevue-popover
|
|
48
|
-
v-else
|
|
49
|
-
ref="popover"
|
|
50
|
-
close-on-escape
|
|
51
|
-
dismissable
|
|
52
|
-
:pt="popoverPt"
|
|
53
|
-
@hide="onHide"
|
|
54
|
-
@show="isOpen = true"
|
|
55
|
-
>
|
|
56
|
-
<cp-menu-list v-if="hasItems" :id="menuId" :items="items" @async-complete="onItemClick" @click="onItemClick" />
|
|
57
|
-
<slot v-else />
|
|
58
|
-
</primevue-popover>
|
|
59
63
|
</div>
|
|
60
64
|
</template>
|
|
61
65
|
|
|
@@ -74,12 +78,15 @@ import CpMenuList from '@/components/CpMenuList.vue'
|
|
|
74
78
|
import { Breakpoints } from '@/constants/layout'
|
|
75
79
|
import { getKeyboardFocusableElements } from '@/helpers/dom'
|
|
76
80
|
|
|
81
|
+
export type CpMenuPlacement = 'start' | 'end'
|
|
82
|
+
|
|
77
83
|
export interface Props {
|
|
78
84
|
class?: string
|
|
79
85
|
forcePopover?: boolean
|
|
80
86
|
isFullHeightDrawer?: boolean
|
|
81
87
|
items?: MenuItem[]
|
|
82
88
|
keepOpenOnClick?: boolean
|
|
89
|
+
placement?: CpMenuPlacement
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -87,6 +94,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
87
94
|
isFullHeightDrawer: false,
|
|
88
95
|
items: undefined,
|
|
89
96
|
keepOpenOnClick: false,
|
|
97
|
+
placement: 'start',
|
|
90
98
|
})
|
|
91
99
|
|
|
92
100
|
const emits = defineEmits<{
|
|
@@ -106,11 +114,11 @@ const isDrawer = computed(() => isMobileViewport.value && !props.forcePopover)
|
|
|
106
114
|
const hasItems = computed(() => props.items?.length)
|
|
107
115
|
const popupType = computed(() => (hasItems.value ? 'menu' : 'dialog'))
|
|
108
116
|
|
|
109
|
-
const popoverPt = {
|
|
117
|
+
const popoverPt = computed(() => ({
|
|
110
118
|
root: { class: ['cpMenu__menu', props.class] },
|
|
111
119
|
content: { class: 'cpMenu__menuContent' },
|
|
112
120
|
transition: { name: 'scale-elastic', duration: 100 },
|
|
113
|
-
}
|
|
121
|
+
}))
|
|
114
122
|
|
|
115
123
|
const drawerOverlayDynamicClass = computed(() => ({
|
|
116
124
|
'cpMenu__drawerOverlay--isOpen': isOpen.value,
|
|
@@ -123,9 +131,14 @@ const drawerPt = {
|
|
|
123
131
|
transition: { name: 'drawer', duration: 300 },
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
const isPopoverPlacementEnd = computed(() => props.placement === 'end')
|
|
135
|
+
|
|
126
136
|
const show = (event: Event) => {
|
|
127
|
-
if (isDrawer.value)
|
|
128
|
-
|
|
137
|
+
if (isDrawer.value) {
|
|
138
|
+
isOpen.value = true
|
|
139
|
+
} else {
|
|
140
|
+
popover.value?.show(event, trigger.value)
|
|
141
|
+
}
|
|
129
142
|
emits('show')
|
|
130
143
|
}
|
|
131
144
|
|
|
@@ -140,8 +153,11 @@ const onItemClick = () => {
|
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
const toggle = (event: Event) => {
|
|
143
|
-
if (isDrawer.value)
|
|
144
|
-
|
|
156
|
+
if (isDrawer.value) {
|
|
157
|
+
isOpen.value = !isOpen.value
|
|
158
|
+
} else {
|
|
159
|
+
popover.value?.toggle(event, trigger.value)
|
|
160
|
+
}
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
const focusTrigger = () => {
|
|
@@ -159,6 +175,27 @@ const onHide = () => {
|
|
|
159
175
|
focusTrigger()
|
|
160
176
|
}
|
|
161
177
|
|
|
178
|
+
const alignPopoverEnd = () => {
|
|
179
|
+
if (!popover.value) return
|
|
180
|
+
|
|
181
|
+
const { container: popoverElement, target: triggerElement } = popover.value as unknown as {
|
|
182
|
+
container: HTMLElement | null
|
|
183
|
+
target: HTMLElement | null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!popoverElement || !triggerElement) return
|
|
187
|
+
|
|
188
|
+
const triggerWidth = triggerElement.offsetWidth
|
|
189
|
+
const popoverWidth = popoverElement.offsetWidth
|
|
190
|
+
|
|
191
|
+
popoverElement.style.marginLeft = `${(popoverWidth - triggerWidth) * -1}px`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const onShow = async () => {
|
|
195
|
+
isOpen.value = true
|
|
196
|
+
if (isPopoverPlacementEnd.value) alignPopoverEnd()
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
defineExpose({ show, hide, toggle })
|
|
163
200
|
</script>
|
|
164
201
|
|
|
@@ -166,20 +203,26 @@ defineExpose({ show, hide, toggle })
|
|
|
166
203
|
.cpMenu {
|
|
167
204
|
display: contents;
|
|
168
205
|
|
|
206
|
+
&__anchor {
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
}
|
|
209
|
+
|
|
169
210
|
&__trigger {
|
|
170
211
|
display: inline-flex;
|
|
171
212
|
cursor: pointer;
|
|
172
213
|
}
|
|
173
214
|
|
|
174
215
|
&__menu {
|
|
175
|
-
position: absolute;
|
|
176
216
|
min-width: calc(var(--cp-dimensions-1) * 62.5);
|
|
177
217
|
border-radius: var(--cp-radius-md);
|
|
178
218
|
margin-top: var(--cp-spacing-sm-md);
|
|
179
219
|
background-color: var(--cp-background-primary);
|
|
180
220
|
box-shadow: var(--cp-shadows-overlay);
|
|
181
|
-
transform-origin: top;
|
|
182
221
|
will-change: opacity, transform;
|
|
222
|
+
|
|
223
|
+
&[data-p-popover-flipped="true"] {
|
|
224
|
+
margin-top: calc(var(--cp-spacing-sm-md) * -1);
|
|
225
|
+
}
|
|
183
226
|
}
|
|
184
227
|
|
|
185
228
|
&__menuContent {
|
|
@@ -39,6 +39,13 @@ const meta = {
|
|
|
39
39
|
description: 'Keep the menu open when clicking an item.',
|
|
40
40
|
table: { defaultValue: { summary: 'false' } },
|
|
41
41
|
},
|
|
42
|
+
placement: {
|
|
43
|
+
control: 'select',
|
|
44
|
+
options: ['start', 'end'],
|
|
45
|
+
description:
|
|
46
|
+
'Horizontal alignment of the popover relative to the trigger. `start`/`end` align the popover edge to the matching trigger edge; the menu auto-flips horizontally when there is not enough room.',
|
|
47
|
+
table: { defaultValue: { summary: 'start' } },
|
|
48
|
+
},
|
|
42
49
|
},
|
|
43
50
|
decorators: [() => ({ template: '<div style="min-height: 60vh; padding: 80px;"><story /></div>' })],
|
|
44
51
|
} satisfies Meta<typeof CpMenu>
|
|
@@ -52,7 +59,11 @@ type Story = StoryObj<typeof meta>
|
|
|
52
59
|
* a separator and a critical "Delete" action.
|
|
53
60
|
*/
|
|
54
61
|
export const Default: Story = {
|
|
55
|
-
|
|
62
|
+
args: {
|
|
63
|
+
forcePopover: true,
|
|
64
|
+
placement: 'start',
|
|
65
|
+
},
|
|
66
|
+
render: (args) => ({
|
|
56
67
|
components: { CpButton, CpMenu },
|
|
57
68
|
setup() {
|
|
58
69
|
const isLoading = ref(false)
|
|
@@ -83,10 +94,10 @@ export const Default: Story = {
|
|
|
83
94
|
},
|
|
84
95
|
])
|
|
85
96
|
|
|
86
|
-
return { items }
|
|
97
|
+
return { args, items }
|
|
87
98
|
},
|
|
88
99
|
template: `
|
|
89
|
-
<CpMenu :items="items" class="defaultMenu">
|
|
100
|
+
<CpMenu v-bind="args" :items="items" class="defaultMenu">
|
|
90
101
|
<template #trigger>
|
|
91
102
|
<CpButton>Open menu</CpButton>
|
|
92
103
|
</template>
|
|
@@ -160,6 +171,40 @@ export const FullHeightDrawer: Story = {
|
|
|
160
171
|
}),
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Use `placement` to control the popover's horizontal alignment. Switch the
|
|
176
|
+
* `placement` control to compare `start` and `end`: each aligns the popover's
|
|
177
|
+
* matching edge to the trigger's. The popover auto-flips horizontally when it
|
|
178
|
+
* would overflow. The trigger is centered here so the difference is easy to see.
|
|
179
|
+
*/
|
|
180
|
+
export const Placement: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
forcePopover: true,
|
|
183
|
+
placement: 'end',
|
|
184
|
+
},
|
|
185
|
+
render: (args) => ({
|
|
186
|
+
components: { CpButton, CpMenu },
|
|
187
|
+
setup() {
|
|
188
|
+
const items = [
|
|
189
|
+
{ label: 'Edit', leadingIcon: 'edit', command: () => alert('Edit clicked') },
|
|
190
|
+
{ label: 'Duplicate', leadingIcon: 'copy', command: () => alert('Duplicate clicked') },
|
|
191
|
+
{ separator: true },
|
|
192
|
+
{ label: 'Delete', leadingIcon: 'trash-2', isCritical: true, command: () => alert('Delete clicked') },
|
|
193
|
+
]
|
|
194
|
+
return { args, items }
|
|
195
|
+
},
|
|
196
|
+
template: `
|
|
197
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 320px;">
|
|
198
|
+
<CpMenu v-bind="args" :items="items">
|
|
199
|
+
<template #trigger>
|
|
200
|
+
<CpButton>Open menu</CpButton>
|
|
201
|
+
</template>
|
|
202
|
+
</CpMenu>
|
|
203
|
+
</div>
|
|
204
|
+
`,
|
|
205
|
+
}),
|
|
206
|
+
}
|
|
207
|
+
|
|
163
208
|
/**
|
|
164
209
|
* Use the default slot to render arbitrary content inside the popover —
|
|
165
210
|
* useful for forms, share panels, filters, etc.
|