@citizenplane/pimp 18.9.10 → 18.9.12

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "18.9.10",
3
+ "version": "18.9.12",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8081",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -1,15 +1,32 @@
1
1
  <template>
2
2
  <div class="cpMenu">
3
3
  <div v-if="isDrawer" class="cpMenu__drawerOverlay" :class="drawerOverlayDynamicClass" />
4
- <div
5
- ref="trigger"
6
- :aria-controls="menuId"
7
- :aria-expanded="isOpen"
8
- :aria-haspopup="popupType"
9
- class="cpMenu__trigger"
10
- @click="toggle"
11
- >
12
- <slot name="trigger" />
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) isOpen.value = true
128
- else popover.value?.show(event, trigger.value)
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) isOpen.value = !isOpen.value
144
- else popover.value?.toggle(event, trigger.value)
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 {
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="cpTable" :class="mainClasses">
3
- <div
3
+ <section
4
4
  ref="cpTableContainer"
5
5
  :aria-labelledby="uniqueId"
6
6
  class="cpTable__container"
@@ -91,7 +91,7 @@
91
91
  </tbody>
92
92
  </table>
93
93
  <cp-table-empty-state v-if="hasNoResult" class="cpTable__emptyState" :placeholder="noResultPlaceholder" />
94
- </div>
94
+ </section>
95
95
  <cp-table-footer
96
96
  v-if="hasPagination"
97
97
  :is-loading="isLoading"
@@ -574,7 +574,7 @@ defineExpose({ hideContextualMenu, resetPagination, currentRowData })
574
574
  }
575
575
 
576
576
  /* Scrolling Visual Cue */
577
- &__container[role='region'][aria-labelledby][tabindex] {
577
+ &__container[aria-labelledby][tabindex] {
578
578
  @extend %u-scrolling-visual-cue;
579
579
 
580
580
  overflow: auto;
@@ -616,12 +616,12 @@ defineExpose({ hideContextualMenu, resetPagination, currentRowData })
616
616
  var(--cp-dimensions-3_5) 100%;
617
617
  }
618
618
 
619
- &__container[role='region'][aria-labelledby][tabindex]:focus {
619
+ &__container[aria-labelledby][tabindex]:focus {
620
620
  border-radius: var(--cp-radius-md-lg);
621
621
  box-shadow: 0 0 0 calc(var(--cp-dimensions-0_5) * 1.5) var(--cp-focus);
622
622
  }
623
623
 
624
- &__container[role='region'][aria-labelledby][tabindex] &__table {
624
+ &__container[aria-labelledby][tabindex] &__table {
625
625
  border: none;
626
626
  margin: 0;
627
627
  }
@@ -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
- render: () => ({
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.