@redseed/redseed-ui-vue3 8.66.0 → 8.66.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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, watchEffect, useSlots
|
|
2
|
+
import { computed, watchEffect, useSlots } from 'vue'
|
|
3
3
|
import { isSafeHref } from '../../helpers/href'
|
|
4
|
+
import { rendersContent } from '../../helpers/slots'
|
|
4
5
|
|
|
5
6
|
const props = defineProps({
|
|
6
7
|
showContent: {
|
|
@@ -62,31 +63,6 @@ const showViewAll = computed(() => hasViewAllLabel.value && hasViewAllHref.value
|
|
|
62
63
|
|
|
63
64
|
const slots = useSlots()
|
|
64
65
|
|
|
65
|
-
/**
|
|
66
|
-
* Whether a slot renders anything at all. Slot *presence* is not the same
|
|
67
|
-
* question: `<template #default><Button v-if="canManage" /></template>` leaves a
|
|
68
|
-
* truthy slot function that renders a comment placeholder, which would still
|
|
69
|
-
* claim a `flex-1` region and shove the centred link into one half of the footer.
|
|
70
|
-
*/
|
|
71
|
-
function rendersContent(slot) {
|
|
72
|
-
const nodes = slot?.()
|
|
73
|
-
|
|
74
|
-
if (!nodes) return false
|
|
75
|
-
|
|
76
|
-
// Comment vnodes are what a falsy `v-if` leaves behind. Fragments wrap lists
|
|
77
|
-
// and `<template v-if>` blocks, so a truthy wrapper around a falsy child is a
|
|
78
|
-
// Fragment whose only child is a Comment — counting its length would call that
|
|
79
|
-
// content and render an empty region claiming `flex-1`.
|
|
80
|
-
return nodes.some(rendersNode)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function rendersNode(node) {
|
|
84
|
-
if (node.type === Comment) return false
|
|
85
|
-
if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
|
|
86
|
-
|
|
87
|
-
return true
|
|
88
|
-
}
|
|
89
|
-
|
|
90
66
|
/**
|
|
91
67
|
* Whether the view-all has the footer to itself, which is the case that wants
|
|
92
68
|
* centring. With anything alongside it, a greedy region would split the row with
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed, watch, watchEffect, useSlots } from 'vue'
|
|
3
|
+
import { rendersContent } from '../../helpers/slots'
|
|
3
4
|
import { useScroll, useEventListener, watchDebounced } from '@vueuse/core'
|
|
4
5
|
import Section from './Section.vue'
|
|
5
6
|
import SectionHeader from './SectionHeader.vue'
|
|
@@ -124,13 +125,64 @@ const slots = useSlots()
|
|
|
124
125
|
/**
|
|
125
126
|
* A consumer-supplied `actions` slot always wins and always renders in the
|
|
126
127
|
* header — that slot exists so consumers can put their own controls up there.
|
|
127
|
-
* The built-in arrows go to whichever placement `hoverActions` selects
|
|
128
|
+
* The built-in arrows go to whichever placement `hoverActions` selects, unless
|
|
129
|
+
* a consumer `actions` slot displaces them from the header, in which case header
|
|
130
|
+
* mode renders none at all.
|
|
131
|
+
*
|
|
132
|
+
* Rendered output, not slot presence, and a plain function rather than a
|
|
133
|
+
* `computed`. Both matter, and getting either wrong breaks the combination this
|
|
134
|
+
* component exists to support — a consumer control in the header with the arrows
|
|
135
|
+
* on the rail:
|
|
136
|
+
*
|
|
137
|
+
* - `Boolean(slots.actions)` is true for `<template #actions><Link v-if="hasMore"
|
|
138
|
+
* /></template>` even when `hasMore` is false, because the slot function still
|
|
139
|
+
* exists and just returns a comment. The arrows are this slot's FALLBACK
|
|
140
|
+
* content, so Vue renders them back into the header the moment the slot
|
|
141
|
+
* resolves to nothing — while `hoverActions` has already put a pair on the
|
|
142
|
+
* rail. Two pairs on one rail, identical accessible names, and disagreeing
|
|
143
|
+
* disabled states. Measured at four buttons.
|
|
144
|
+
* - `useSlots()` is not reactive, so a `computed` over it has no dependency and
|
|
145
|
+
* caches its first answer for the life of the component. A consumer writing
|
|
146
|
+
* `<template v-if="hasMore" #actions>` against a count that arrives with a
|
|
147
|
+
* fetch would never see their control appear at all.
|
|
148
|
+
*
|
|
149
|
+
* Same reasoning and the same helper as SectionFooter — see helpers/slots.
|
|
128
150
|
*/
|
|
129
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The scope the `actions` slot is handed. Built once so the probe below and the
|
|
153
|
+
* real `<slot>` in the template receive the same object — a consumer template
|
|
154
|
+
* that destructures these compiles to a function destructuring its only
|
|
155
|
+
* parameter, so probing bare threw and rendered nothing at all.
|
|
156
|
+
*/
|
|
157
|
+
const actionsSlotScope = computed(() => ({
|
|
158
|
+
showNextSlide,
|
|
159
|
+
showPreviousSlide,
|
|
160
|
+
disabledPrevButton: disabledPrevButton.value,
|
|
161
|
+
disabledNextButton: disabledNextButton.value,
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
function hasCustomActions() {
|
|
165
|
+
return rendersContent(slots.actions, actionsSlotScope.value)
|
|
166
|
+
}
|
|
130
167
|
|
|
131
|
-
|
|
168
|
+
function showHeaderActions() {
|
|
169
|
+
return hasCustomActions() || !props.hoverActions
|
|
170
|
+
}
|
|
132
171
|
|
|
133
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Placement is `hoverActions`' decision alone. A custom `actions` slot does not
|
|
174
|
+
* suppress the overlay: the arrows are that slot's fallback content, so
|
|
175
|
+
* overriding it has already taken them out of the header — which is exactly when
|
|
176
|
+
* the rail needs them. Guarding on `hasCustomActions` here left the one
|
|
177
|
+
* combination consumers reach for (own header control, arrows on the rail) with
|
|
178
|
+
* no navigation at all from `lg` up, where the track is `overflow-x-hidden` and
|
|
179
|
+
* the arrows are the only way to move it. `hoverActions` exists to free the
|
|
180
|
+
* header for such a control in the first place.
|
|
181
|
+
*
|
|
182
|
+
* Safe as a `computed`: it reads only a prop. The header side is what has to
|
|
183
|
+
* track a slot.
|
|
184
|
+
*/
|
|
185
|
+
const showOverlayActions = computed(() => props.hoverActions)
|
|
134
186
|
|
|
135
187
|
/**
|
|
136
188
|
* Unique id for the slider
|
|
@@ -548,13 +600,8 @@ function showPreviousSlide() {
|
|
|
548
600
|
<slot name="see-all"></slot>
|
|
549
601
|
</template>
|
|
550
602
|
|
|
551
|
-
<template v-if="showHeaderActions" #actions>
|
|
552
|
-
<slot name="actions"
|
|
553
|
-
:showNextSlide="showNextSlide"
|
|
554
|
-
:showPreviousSlide="showPreviousSlide"
|
|
555
|
-
:disabledPrevButton="disabledPrevButton"
|
|
556
|
-
:disabledNextButton="disabledNextButton"
|
|
557
|
-
>
|
|
603
|
+
<template v-if="showHeaderActions()" #actions>
|
|
604
|
+
<slot name="actions" v-bind="actionsSlotScope">
|
|
558
605
|
<SectionSliderAction direction="prev"
|
|
559
606
|
:disabled="disabledPrevButton"
|
|
560
607
|
:invert="featured"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Comment, Fragment } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a slot renders anything at all.
|
|
5
|
+
*
|
|
6
|
+
* Internal on purpose — not re-exported from `helpers/index.js`. It walks vnode
|
|
7
|
+
* internals, which is a detail of how this library composes its own regions
|
|
8
|
+
* rather than something consuming apps should depend on.
|
|
9
|
+
*
|
|
10
|
+
* Slot *presence* is a different question, and the wrong one in almost every
|
|
11
|
+
* case. `Boolean(slots.actions)` is true for
|
|
12
|
+
* `<template #actions><Button v-if="canManage" /></template>` even when
|
|
13
|
+
* `canManage` is false, because the slot function still exists — it just returns
|
|
14
|
+
* a comment placeholder. Components that use presence to decide whether to stand
|
|
15
|
+
* their own content down then stand it down for a slot that renders nothing, and
|
|
16
|
+
* the region ends up empty, or the fallback it was suppressing comes back.
|
|
17
|
+
*
|
|
18
|
+
* Two callers, for two versions of that bug: SectionFooter, where an empty region
|
|
19
|
+
* claimed `flex-1` and shoved a centred link into one half of the footer, and
|
|
20
|
+
* SectionSlider, where an `#actions` slot rendering nothing let the arrows return
|
|
21
|
+
* to the header while `hoverActions` had already put a pair on the rail — two
|
|
22
|
+
* pairs on one rail, with identical accessible names.
|
|
23
|
+
*
|
|
24
|
+
* Call this from the template or from a plain function, never from a `computed`:
|
|
25
|
+
* `useSlots()` returns a non-reactive object, so a computed over it caches its
|
|
26
|
+
* first answer for the life of the component and a conditionally-supplied slot
|
|
27
|
+
* would never appear.
|
|
28
|
+
*
|
|
29
|
+
* `scope` is REQUIRED for a scoped slot, and passing it is the caller's job. A
|
|
30
|
+
* consumer template that destructures its scoped props — `<template #actions="{
|
|
31
|
+
* showPreviousSlide }">` — compiles to a function that destructures its only
|
|
32
|
+
* parameter, so probing it bare throws `Cannot destructure property … of
|
|
33
|
+
* 'undefined'` and takes the whole component down with it. Hand it the same
|
|
34
|
+
* object the real `<slot>` receives, so the probe and the render agree. A
|
|
35
|
+
* non-scoped slot ignores the argument, so passing nothing stays correct there.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately not wrapped in try/catch: that would swallow a genuine error
|
|
38
|
+
* thrown by the consumer's own slot content and leave a silently empty region.
|
|
39
|
+
*/
|
|
40
|
+
export function rendersContent(slot, scope) {
|
|
41
|
+
const nodes = slot?.(scope)
|
|
42
|
+
|
|
43
|
+
if (!nodes) return false
|
|
44
|
+
|
|
45
|
+
return nodes.some(rendersNode)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Comment vnodes are what a falsy `v-if` leaves behind. Fragments wrap lists and
|
|
50
|
+
* `<template v-if>` blocks, so a truthy wrapper around a falsy child is a
|
|
51
|
+
* Fragment whose only child is a Comment — counting length would treat that as
|
|
52
|
+
* content.
|
|
53
|
+
*/
|
|
54
|
+
function rendersNode(node) {
|
|
55
|
+
if (node.type === Comment) return false
|
|
56
|
+
if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
|
|
57
|
+
|
|
58
|
+
return true
|
|
59
|
+
}
|