Skip to content

Commit 4cb9f6c

Browse files
committed
feat(NcPopover): add properties instead of linking to wrapped library
- Also support RTL design Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 4914c1a commit 4cb9f6c

File tree

3 files changed

+264
-39
lines changed

3 files changed

+264
-39
lines changed

CHANGELOG.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ The `richEditing` mixin can be replaced by just using the `NcRichText` component
143143
- The `range` property was removed in favor of `type="datetime-range"` (datetime ranges), `type="date-range"` (date only ranges), and `type="time-range"` (time only ranges).
144144
- The `lang` property was replaced with the `locale` property.
145145
- The `formatter` property was removed.
146+
- `NcPopover` no longer is a transparent wrapper over the `floating-vue` package.
147+
Instead only use the documented properties and events.
148+
If you find some use cases not covered by the documented interface, please open a feature request.
146149
- `NcSelect`
147150
- `userSelect` property was removed, instead just use the `NcSelectUsers` component
148151
- `closeOnSelect` property was removed in favor of `keepOpen`.
@@ -257,7 +260,12 @@ The `richEditing` mixin can be replaced by just using the `NcRichText` component
257260

258261
### 📝 Notes
259262
#### NcPopover
260-
The `focusTrap` property is now deprecated and will be replaced with `noFocusTrap`,
263+
`NcPopover` not has its own properties and no longer directly exposes the internal library used (`floating-vue`).
264+
It is still possible to use its properties, but this ability might be removed in the next version.
265+
This we encourage you to only use the documented properties.
266+
267+
Also this component now supports a logical placement (`start`, `end`) which works with RTL design.
268+
Moreover the `focusTrap` property is now deprecated and will be replaced with `noFocusTrap`,
261269
the reason behind this is to only have boolean properties with default value of `false` allowing shortcut props.
262270

263271
## [v8.25.0](https://github.com/nextcloud-libraries/nextcloud-vue/tree/v8.25.0) (UNRELEASED)

src/components/NcPopover/NcPopover.vue

+215-38
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77

88
### General description
99

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-
1410
This components has two slots:
1511
* 'trigger' which can be any html element and it will trigger the popover
1612
this slot is optional since you can toggle the popover also by updating the
@@ -51,12 +47,12 @@ open prop on this component;
5147

5248
The [`focus-trap`](https://github.com/focus-trap/focus-trap) emits an error when used in a non-focusable element tree.
5349

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.
5551

5652
```vue
5753
<template>
5854
<div style="display: flex">
59-
<NcPopover :focus-trap="false">
55+
<NcPopover no-focus-trap>
6056
<template #trigger>
6157
<NcButton>Click me!</NcButton>
6258
</template>
@@ -68,21 +64,60 @@ The prop `:focus-trap="false"` help to prevent it when the default behavior is n
6864
</template>
6965
```
7066

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).
7271

7372
```vue
7473
<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>
8497
</div>
8598
</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>
86121
```
87122

88123
#### With a custom button in as a trigger:
@@ -141,11 +176,22 @@ See: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/
141176

142177
<template>
143178
<Dropdown ref="popover"
144-
:distance="10"
145179
:arrow-padding="10"
180+
:auto-hide="closeOnClickOutside"
181+
:boundary="boundary || undefined"
182+
:container
183+
:delay
184+
:distance="10"
146185
:no-auto-focus="true /* Handled by the focus trap */"
186+
:placement="internalPlacement"
147187
:popper-class="popoverBaseClass"
188+
:popper-triggers
189+
:popper-hide-triggers
190+
:popper-show-triggers
148191
:shown="internalShown"
192+
:triggers="internalTriggers"
193+
:hide-triggers
194+
:show-triggers
149195
@update:shown="internalShown = $event"
150196
@apply-show="afterShow"
151197
@apply-hide="afterHide">
@@ -162,9 +208,10 @@ See: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/
162208
</template>
163209

164210
<script>
165-
import { warn } from 'vue'
211+
import { isRTL } from '@nextcloud/l10n'
166212
import { Dropdown } from 'floating-vue'
167213
import { createFocusTrap } from 'focus-trap'
214+
import { warn } from 'vue'
168215
import { getTrapStack } from '../../utils/focusTrap.ts'
169216
import NcPopoverTriggerProvider from './NcPopoverTriggerProvider.vue'
170217

@@ -182,27 +229,38 @@ export default {
182229

183230
props: {
184231
/**
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).
187233
*/
188-
shown: {
234+
boundary: {
235+
type: String,
236+
default: '',
237+
},
238+
239+
/**
240+
* Automatically hide the popover on click outside.
241+
*/
242+
closeOnClickOutside: {
189243
type: Boolean,
190244
default: false,
191245
},
192246

193247
/**
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.
196250
*/
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',
201254
},
202255

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,
206264
},
207265

208266
/**
@@ -213,6 +271,47 @@ export default {
213271
default: false,
214272
},
215273

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+
216315
/**
217316
* Set element to return focus to after focus trap deactivation
218317
*
@@ -222,23 +321,101 @@ export default {
222321
default: undefined,
223322
type: [HTMLElement, SVGElement, String, Boolean, Function],
224323
},
225-
},
226324

227-
emits: [
228-
'after-show',
229-
'after-hide',
230325
/**
231-
* @see https://floating-vue.starpad.dev/api/#update-shown
326+
* Show or hide the popper
232327
*/
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',
233355
'update:shown',
234356
],
235357

358+
setup() {
359+
return {
360+
isRtl: isRTL(),
361+
}
362+
},
363+
236364
data() {
237365
return {
238366
internalShown: this.shown,
239367
}
240368
},
241369

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+
242419
watch: {
243420
shown(value) {
244421
this.internalShown = value
@@ -297,7 +474,7 @@ export default {
297474
* @return {HTMLElement|undefined}
298475
*/
299476
getPopoverTriggerContainerElement() {
300-
return this.$refs.popover.$refs.popper.$refs.reference
477+
return this.$refs.popover?.$refs.popper?.$refs.reference
301478
},
302479

303480
/**
@@ -380,7 +557,7 @@ export default {
380557
* run earlier than this where there is no guarantee that the
381558
* tooltip is already visible and in the DOM.
382559
*/
383-
this.$emit('after-show')
560+
this.$emit('afterShow')
384561
}, { once: true, passive: true })
385562

386563
this.removeFloatingVueAriaDescribedBy()
@@ -398,7 +575,7 @@ export default {
398575
* run earlier than this where there is no guarantee that the
399576
* tooltip is already visible and in the DOM.
400577
*/
401-
this.$emit('after-hide')
578+
this.$emit('afterHide')
402579
}, { once: true, passive: true })
403580

404581
this.clearFocusTrap()

0 commit comments

Comments
 (0)