@redseed/redseed-ui-vue3 10.3.1 → 10.3.3

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": "@redseed/redseed-ui-vue3",
3
- "version": "10.3.1",
3
+ "version": "10.3.3",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, watch, watchEffect, nextTick, onMounted, onBeforeUnmount } from 'vue'
2
+ import { ref, shallowRef, watch, watchEffect, nextTick, onMounted, onBeforeUnmount } from 'vue'
3
3
  import { ChevronDownIcon } from '@heroicons/vue/24/outline'
4
4
  import { ButtonTertiary } from '../Button'
5
5
  import Icon from '../Icon/Icon.vue'
@@ -50,6 +50,8 @@ async function setContentMaxHeight() {
50
50
 
51
51
  await nextTick()
52
52
 
53
+ if (!contentRef.value) return
54
+
53
55
  contentRef.value.style.minHeight = contentRef.value.scrollHeight + 'px'
54
56
  }
55
57
 
@@ -58,6 +60,8 @@ async function resetContentMaxHeight() {
58
60
 
59
61
  await nextTick()
60
62
 
63
+ if (!contentRef.value) return
64
+
61
65
  contentRef.value.style.removeProperty('min-height')
62
66
  }
63
67
 
@@ -88,7 +92,7 @@ watch(isOpen, (open, wasOpen) => {
88
92
  }, { flush: 'sync' })
89
93
 
90
94
  /**
91
- * Whether each teleport target currently exists.
95
+ * Each teleport target's current element, or null while it is absent.
92
96
  *
93
97
  * Re-checked rather than latched. These used to only ever flip true, and the
94
98
  * observer disconnected once both had been found — so a consumer that removed a
@@ -96,15 +100,23 @@ watch(isOpen, (open, wasOpen) => {
96
100
  * `SectionHeader` does exactly that: it width-gates the slots a target is
97
101
  * mounted into, so narrowing a header past 640px took the trigger away and the
98
102
  * section could no longer be opened. See #372.
103
+ *
104
+ * The element itself goes to `to`, not an `#id` selector. Vue resolves a
105
+ * selector once, when the Teleport mounts, and never again when only
106
+ * `disabled` flips. A Disclosure mounted inside a subtree that is not yet in the
107
+ * document resolved null there, and enabling it then threw "Cannot read
108
+ * properties of null (reading 'insertBefore')" and wedged the whole page. A
109
+ * changed `to` is re-resolved, so a target that appears late or is re-created
110
+ * gets the content moved into the live element.
99
111
  */
100
- const canTeleportTrigger = ref(false)
101
- const canTeleportContent = ref(false)
112
+ const triggerTarget = shallowRef(null)
113
+ const contentTarget = shallowRef(null)
102
114
 
103
115
  let observer = null
104
116
 
105
117
  function setTeleport() {
106
- canTeleportTrigger.value = Boolean(document.getElementById(triggerId))
107
- canTeleportContent.value = Boolean(document.getElementById(contentId))
118
+ triggerTarget.value = document.getElementById(triggerId)
119
+ contentTarget.value = document.getElementById(contentId)
108
120
  }
109
121
 
110
122
  onMounted(() => {
@@ -149,8 +161,8 @@ onBeforeUnmount(() => {
149
161
  Vue's own `disabled` keeps this as one definition rendered in one of two
150
162
  places, rather than two copies that can drift.
151
163
  -->
152
- <Teleport :to="`#${triggerId}`"
153
- :disabled="!canTeleportTrigger"
164
+ <Teleport :to="triggerTarget"
165
+ :disabled="!triggerTarget"
154
166
  >
155
167
  <slot name="trigger"
156
168
  :handleTrigger="handleTrigger"
@@ -184,8 +196,8 @@ onBeforeUnmount(() => {
184
196
  </Teleport>
185
197
 
186
198
  <!-- Same reasoning as the trigger above: in place rather than nowhere. -->
187
- <Teleport :to="`#${contentId}`"
188
- :disabled="!canTeleportContent"
199
+ <Teleport :to="contentTarget"
200
+ :disabled="!contentTarget"
189
201
  >
190
202
  <div ref="contentRef"
191
203
  :class="[
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
- import { ref, computed, watch, watchEffect, useSlots, Comment, Fragment } from 'vue'
2
+ import { ref, computed, watch, watchEffect, useSlots } from 'vue'
3
3
  import Icon from '../Icon/Icon.vue'
4
4
  import { XMarkIcon } from '@heroicons/vue/24/outline'
5
+ import { rendersContent } from '../../helpers/slots'
5
6
 
6
7
  const props = defineProps({
7
8
  closed: {
@@ -81,40 +82,6 @@ watch(() => props.closed, (value) => {
81
82
  isClosed.value = value
82
83
  })
83
84
 
84
- /**
85
- * A Fragment is a wrapper, so its own presence answers nothing — only what is
86
- * inside it does. Hence the recursion: a `v-for` compiles to a Fragment and
87
- * leaves a comment placeholder for each hidden item, so counting children
88
- * reported content for a list whose every action was filtered out (the usual
89
- * shape for permission-gated actions), and a nested `<template>` puts another
90
- * Fragment inside that one.
91
- *
92
- * `Array.isArray` rather than recursing straight into `children`, which is a raw
93
- * string on a Fragment built by hand as `h(Fragment, null, 'text')`. Vue's own
94
- * `ensureValidVNode` assumes the array and throws on that shape, so answering
95
- * "renders" here only hands it a slot it cannot render; treating it as empty
96
- * drops the row instead, which is the quieter of the two failures. Compiled
97
- * templates always produce the array.
98
- */
99
- function rendersNode(node) {
100
- if (node.type === Comment) return false
101
- if (node.type === Fragment) return Array.isArray(node.children) && node.children.some(rendersNode)
102
-
103
- return true
104
- }
105
-
106
- /**
107
- * Whether a slot renders anything at all. Slot *presence* is not the same
108
- * question: a slot holding only a falsy `v-if` is still a truthy slot function
109
- * that renders a comment placeholder, which would put an empty padded row under
110
- * the message.
111
- */
112
- function rendersContent(slot) {
113
- const nodes = slot?.()
114
-
115
- return Boolean(nodes) && nodes.some(rendersNode)
116
- }
117
-
118
85
  /**
119
86
  * Announce the box as a live region so flash / MOTD content reaches assistive
120
87
  * tech (WCAG 4.1.3 Status Messages). `alert` is assertive — reserved for errors
@@ -195,7 +162,15 @@ function close() {
195
162
  text on a coloured tint is a contrast problem, and the action is a way
196
163
  out of the message rather than a restatement of its severity.
197
164
  -->
198
- <div v-if="hasDismiss || rendersContent($slots.actions)" class="rsui-message-box__actions">
165
+ <!--
166
+ The scope is forwarded, and that is the whole of #331. `#actions` is declared
167
+ scoped below — `<slot name="actions" :close="close">` — so a consumer writing
168
+ the documented `<template #actions="{ close }">` compiles to a function that
169
+ destructures its only parameter. Probing it bare passed `undefined` and threw
170
+ `Cannot destructure property 'close' of 'undefined'`, taking the whole
171
+ MessageBox down. Shipped in 8.66.0.
172
+ -->
173
+ <div v-if="hasDismiss || rendersContent($slots.actions, { close })" class="rsui-message-box__actions">
199
174
  <button v-if="hasDismiss"
200
175
  type="button"
201
176
  class="rsui-message-box__dismiss"
@@ -52,7 +52,15 @@ export function rendersContent(slot, scope) {
52
52
  * Comment vnodes are what a falsy `v-if` leaves behind. Fragments wrap lists and
53
53
  * `<template v-if>` blocks, so a truthy wrapper around a falsy child is a
54
54
  * Fragment whose only child is a Comment — counting length would treat that as
55
- * content.
55
+ * content. A `v-for` over a filtered list is the usual shape, and a nested
56
+ * `<template>` puts another Fragment inside that one.
57
+ *
58
+ * `Array.isArray` rather than recursing straight into `children`, which is a raw
59
+ * string on a Fragment built by hand as `h(Fragment, null, 'text')`. Vue's own
60
+ * `ensureValidVNode` assumes the array and throws on that shape, so answering
61
+ * "renders" there only hands it a slot it cannot render; treating it as empty
62
+ * drops the region instead, which is the quieter of the two failures. Compiled
63
+ * templates always produce the array.
56
64
  */
57
65
  function rendersNode(node) {
58
66
  if (node.type === Comment) return false