7
7
8
8
### General description
9
9
10
- This component is just a wrapper for the floating-vue plugin by Akryum,
11
- please refer to this documentation for customization:
12
- https://github.com/Akryum/floating-vue
13
-
14
10
This components has two slots:
15
11
* 'trigger' which can be any html element and it will trigger the popover
16
12
this slot is optional since you can toggle the popover also by updating the
@@ -51,12 +47,12 @@ open prop on this component;
51
47
52
48
The [`focus-trap`](https://github.com/focus-trap/focus-trap) emits an error when used in a non-focusable element tree.
53
49
54
- The prop `: focus-trap="false" ` help to prevent it when the default behavior is not relevant.
50
+ The prop `no- focus-trap` help to prevent it when the default behavior is not relevant.
55
51
56
52
```vue
57
53
<template>
58
54
<div style="display: flex">
59
- <NcPopover : focus-trap="false" >
55
+ <NcPopover no- focus-trap>
60
56
<template #trigger>
61
57
<NcButton>Click me!</NcButton>
62
58
</template>
@@ -68,21 +64,60 @@ The prop `:focus-trap="false"` help to prevent it when the default behavior is n
68
64
</template>
69
65
```
70
66
71
- #### With passing props to `floating-vue`'s `Dropdown`:
67
+ #### With logical placement
68
+
69
+ If the text flow is language specific (e.g. UI is shown for right-to-left language),
70
+ also the popover often needs to be adjusted when not rendered on top or bottom (default).
72
71
73
72
```vue
74
73
<template>
75
- <div style="display: flex">
76
- <NcPopover container="body" :popper-hide-triggers="(triggers) => [...triggers, 'click']" popup-role="dialog">
77
- <template #trigger>
78
- <NcButton>I am the trigger</NcButton>
79
- </template>
80
- <template #default>
81
- <NcButton>Click on the button will close NcPopover</NcButton>
82
- </template>
83
- </NcPopover>
74
+ <div class="wrapper">
75
+ <fieldset>
76
+ <NcCheckboxRadioSwitch v-model="dir" type="radio" value="ltr">
77
+ LTR
78
+ </NcCheckboxRadioSwitch>
79
+ <NcCheckboxRadioSwitch v-model="dir" type="radio" value="rtl">
80
+ RTL
81
+ </NcCheckboxRadioSwitch>
82
+ </fieldset>
83
+ <div class="content" :dir>
84
+ <NcPopover :key="dir"
85
+ placement="end"
86
+ :triggers="['hover']">
87
+ <template #trigger>
88
+ <NcButton>
89
+ Hover me
90
+ </NcButton>
91
+ </template>
92
+ <template #default>
93
+ This will be shown on the logical end of the button.
94
+ </template>
95
+ </NcPopover>
96
+ </div>
84
97
</div>
85
98
</template>
99
+ <script>
100
+ export default {
101
+ data() {
102
+ return {
103
+ dir: 'ltr',
104
+ }
105
+ },
106
+ }
107
+ </script>
108
+ <style scoped>
109
+ .content {
110
+ display: flex;
111
+ flex-direction: row;
112
+ justify-content: space-around;
113
+ }
114
+
115
+ fieldset {
116
+ display: flex;
117
+ flex-direction: row;
118
+ gap: 12px;
119
+ }
120
+ </style>
86
121
```
87
122
88
123
#### With a custom button in as a trigger:
@@ -141,11 +176,22 @@ See: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/
141
176
142
177
<template>
143
178
<Dropdown ref="popover"
144
- :distance="10"
145
179
:arrow-padding="10"
180
+ :auto-hide="closeOnClickOutside"
181
+ :boundary="boundary || undefined"
182
+ :container
183
+ :delay
184
+ :distance="10"
146
185
:no-auto-focus="true /* Handled by the focus trap */"
186
+ :placement="internalPlacement"
147
187
:popper-class="popoverBaseClass"
188
+ :popper-triggers
189
+ :popper-hide-triggers
190
+ :popper-show-triggers
148
191
:shown="internalShown"
192
+ :triggers="internalTriggers"
193
+ :hide-triggers
194
+ :show-triggers
149
195
@update:shown="internalShown = $event"
150
196
@apply-show="afterShow"
151
197
@apply-hide="afterHide">
@@ -162,9 +208,10 @@ See: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/
162
208
</template>
163
209
164
210
<script>
165
- import { warn } from 'vue '
211
+ import { isRTL } from '@nextcloud/l10n '
166
212
import { Dropdown } from 'floating-vue'
167
213
import { createFocusTrap } from 'focus-trap'
214
+ import { warn } from 'vue'
168
215
import { getTrapStack } from '../../utils/focusTrap.ts'
169
216
import NcPopoverTriggerProvider from './NcPopoverTriggerProvider.vue'
170
217
@@ -182,27 +229,38 @@ export default {
182
229
183
230
props: {
184
231
/**
185
- * Show or hide the popper
186
- * @see https://floating-vue.starpad.dev/api/#shown
232
+ * Element to use for calculating the popper boundary (size and position).
187
233
*/
188
- shown: {
234
+ boundary: {
235
+ type: String,
236
+ default: '',
237
+ },
238
+
239
+ /**
240
+ * Automatically hide the popover on click outside.
241
+ */
242
+ closeOnClickOutside: {
189
243
type: Boolean,
190
244
default: false,
191
245
},
192
246
193
247
/**
194
- * Popup role
195
- * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup#values
248
+ * Container where to mount the popover.
249
+ * Either a select query or `false` to mount to the parent node.
196
250
*/
197
- popupRole: {
198
- type: String,
199
- default: undefined,
200
- validator: (value) => ['menu', 'listbox', 'tree', 'grid', 'dialog', 'true'].includes(value),
251
+ container: {
252
+ type: [String, Boolean],
253
+ default: 'body',
201
254
},
202
255
203
- popoverBaseClass: {
204
- type: String,
205
- default: '',
256
+ /**
257
+ * Delay for showing or hiding the popover.
258
+ *
259
+ * Can either be a number or an object to configure different delays (`{ show: number, hide: number }`).
260
+ */
261
+ delay: {
262
+ type: [Number, Object],
263
+ default: 0,
206
264
},
207
265
208
266
/**
@@ -213,6 +271,47 @@ export default {
213
271
default: false,
214
272
},
215
273
274
+ /**
275
+ * Where to place the popover.
276
+ *
277
+ * This consists of the vertical placment and the horizontal placement.
278
+ * E.g. `bottom` will place the popover on the bottom of the trigger (horizontally centered),
279
+ * while `buttom-start` will horizontally align the popover on the logical start (e.g. for LTR layout on the left.).
280
+ * The `start` or `end` placement will align the popover on the left or right side or the trigger element.
281
+ *
282
+ * @type {'auto'|'auto-start'|'auto-end'|'top'|'top-start'|'top-end'|'bottom'|'bottom-start'|'bottom-end'|'start'|'end'}
283
+ */
284
+ placement: {
285
+ type: String,
286
+ default: 'bottom',
287
+ },
288
+
289
+ popoverBaseClass: {
290
+ type: String,
291
+ default: '',
292
+ },
293
+
294
+ /**
295
+ * Events that trigger the popover on the popover container itself.
296
+ * This is useful if you set `triggers` to `hover` and also want the popover to stay open while hovering the popover itself.
297
+ *
298
+ * It is possible to also pass an object to define different triggers for hide and show `{ show: ['hover'], hide: ['click'] }`.
299
+ */
300
+ popoverTriggers: {
301
+ type: [Array, Object],
302
+ default: null,
303
+ },
304
+
305
+ /**
306
+ * Popup role
307
+ * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-haspopup#values
308
+ */
309
+ popupRole: {
310
+ type: String,
311
+ default: undefined,
312
+ validator: (value) => ['menu', 'listbox', 'tree', 'grid', 'dialog', 'true'].includes(value),
313
+ },
314
+
216
315
/**
217
316
* Set element to return focus to after focus trap deactivation
218
317
*
@@ -222,23 +321,101 @@ export default {
222
321
default: undefined,
223
322
type: [HTMLElement, SVGElement, String, Boolean, Function],
224
323
},
225
- },
226
324
227
- emits: [
228
- 'after-show',
229
- 'after-hide',
230
325
/**
231
- * @see https://floating-vue.starpad.dev/api/#update-shown
326
+ * Show or hide the popper
232
327
*/
328
+ shown: {
329
+ type: Boolean,
330
+ default: false,
331
+ },
332
+
333
+ /**
334
+ * Events that trigger the popover.
335
+ *
336
+ * If you pass an empty array then only the `shown` prop can control the popover state.
337
+ * Following events are available:
338
+ * - `'hover'`
339
+ * - `'click'`
340
+ * - `'focus'`
341
+ * - `'touch'`
342
+ *
343
+ * It is also possible to pass an object to have different events for show and hide:
344
+ * `{ hide: ['click'], show: ['click', 'hover'] }`
345
+ */
346
+ triggers: {
347
+ type: [Array, Object],
348
+ default: () => ['click'],
349
+ },
350
+ },
351
+
352
+ emits: [
353
+ 'afterShow',
354
+ 'afterHide',
233
355
'update:shown',
234
356
],
235
357
358
+ setup() {
359
+ return {
360
+ isRtl: isRTL(),
361
+ }
362
+ },
363
+
236
364
data() {
237
365
return {
238
366
internalShown: this.shown,
239
367
}
240
368
},
241
369
370
+ computed: {
371
+ popperTriggers() {
372
+ if (this.popoverTriggers && Array.isArray(this.popoverTriggers)) {
373
+ return this.popoverTriggers
374
+ }
375
+ return undefined
376
+ },
377
+ popperHideTriggers() {
378
+ if (this.popoverTriggers && typeof this.popoverTriggers === 'object') {
379
+ return this.popoverTriggers.hide
380
+ }
381
+ return undefined
382
+ },
383
+ popperShowTriggers() {
384
+ if (this.popoverTriggers && typeof this.popoverTriggers === 'object') {
385
+ return this.popoverTriggers.show
386
+ }
387
+ return undefined
388
+ },
389
+
390
+ internalTriggers() {
391
+ if (this.triggers && Array.isArray(this.triggers)) {
392
+ return this.triggers
393
+ }
394
+ return undefined
395
+ },
396
+ hideTriggers() {
397
+ if (this.triggers && typeof this.triggers === 'object') {
398
+ return this.triggers.hide
399
+ }
400
+ return undefined
401
+ },
402
+ showTriggers() {
403
+ if (this.triggers && typeof this.triggers === 'object') {
404
+ return this.triggers.show
405
+ }
406
+ return undefined
407
+ },
408
+
409
+ internalPlacement() {
410
+ if (this.placement === 'start') {
411
+ return this.isRtl ? 'right' : 'left'
412
+ } else if (this.placement === 'end') {
413
+ return this.isRtl ? 'left' : 'right'
414
+ }
415
+ return this.placement
416
+ },
417
+ },
418
+
242
419
watch: {
243
420
shown(value) {
244
421
this.internalShown = value
@@ -297,7 +474,7 @@ export default {
297
474
* @return {HTMLElement|undefined}
298
475
*/
299
476
getPopoverTriggerContainerElement() {
300
- return this.$refs.popover.$refs.popper.$refs.reference
477
+ return this.$refs.popover? .$refs.popper? .$refs.reference
301
478
},
302
479
303
480
/**
@@ -380,7 +557,7 @@ export default {
380
557
* run earlier than this where there is no guarantee that the
381
558
* tooltip is already visible and in the DOM.
382
559
*/
383
- this.$emit('after-show ')
560
+ this.$emit('afterShow ')
384
561
}, { once: true, passive: true })
385
562
386
563
this.removeFloatingVueAriaDescribedBy()
@@ -398,7 +575,7 @@ export default {
398
575
* run earlier than this where there is no guarantee that the
399
576
* tooltip is already visible and in the DOM.
400
577
*/
401
- this.$emit('after-hide ')
578
+ this.$emit('afterHide ')
402
579
}, { once: true, passive: true })
403
580
404
581
this.clearFocusTrap()
0 commit comments