@redseed/redseed-ui-vue3 10.5.1 → 11.0.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
|
@@ -29,7 +29,6 @@ const props = defineProps({
|
|
|
29
29
|
|
|
30
30
|
const emit = defineEmits(['click'])
|
|
31
31
|
|
|
32
|
-
const triggerId = _.uniqueId('disclosure-trigger-')
|
|
33
32
|
const contentId = _.uniqueId('disclosure-content-')
|
|
34
33
|
|
|
35
34
|
const isOpen = ref(props.open)
|
|
@@ -139,14 +138,15 @@ watch(isOpen, (open, wasOpen) => {
|
|
|
139
138
|
}, { flush: 'sync' })
|
|
140
139
|
|
|
141
140
|
/**
|
|
142
|
-
*
|
|
141
|
+
* The content teleport target's current element, or null while it is absent.
|
|
143
142
|
*
|
|
144
143
|
* Re-checked rather than latched. These used to only ever flip true, and the
|
|
145
144
|
* observer disconnected once both had been found — so a consumer that removed a
|
|
146
145
|
* target left a Teleport mounted against nothing and the trigger vanished.
|
|
147
|
-
* `SectionHeader`
|
|
148
|
-
*
|
|
149
|
-
*
|
|
146
|
+
* `SectionHeader` did exactly that: it width-gated the slots a target was mounted
|
|
147
|
+
* into, so narrowing a header past 640px took the trigger away and the section
|
|
148
|
+
* could no longer be opened (#372). The trigger no longer teleports at all (#408);
|
|
149
|
+
* the content still does, because 27 consumers still place a content pad.
|
|
150
150
|
*
|
|
151
151
|
* The element itself goes to `to`, not an `#id` selector. Vue resolves a
|
|
152
152
|
* selector once, when the Teleport mounts, and never again when only
|
|
@@ -156,13 +156,11 @@ watch(isOpen, (open, wasOpen) => {
|
|
|
156
156
|
* changed `to` is re-resolved, so a target that appears late or is re-created
|
|
157
157
|
* gets the content moved into the live element.
|
|
158
158
|
*/
|
|
159
|
-
const triggerTarget = shallowRef(null)
|
|
160
159
|
const contentTarget = shallowRef(null)
|
|
161
160
|
|
|
162
161
|
let observer = null
|
|
163
162
|
|
|
164
163
|
function setTeleport() {
|
|
165
|
-
triggerTarget.value = document.getElementById(triggerId)
|
|
166
164
|
contentTarget.value = document.getElementById(contentId)
|
|
167
165
|
}
|
|
168
166
|
|
|
@@ -189,39 +187,41 @@ onBeforeUnmount(() => {
|
|
|
189
187
|
<div class="rsui-disclosure">
|
|
190
188
|
<slot name="child"
|
|
191
189
|
:isOpen="isOpen"
|
|
192
|
-
:triggerId="triggerId"
|
|
193
190
|
:contentId="contentId"
|
|
194
191
|
:handleTrigger="handleTrigger"
|
|
195
192
|
></slot>
|
|
196
193
|
</div>
|
|
197
194
|
|
|
198
195
|
<!--
|
|
199
|
-
|
|
200
|
-
consumer supplies no target instead of not rendering at all.
|
|
196
|
+
Rendered IN PLACE. There is no trigger Teleport any more.
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
198
|
+
It used to be teleported into an empty `<div :id="triggerId">` the consumer left
|
|
199
|
+
somewhere in their own markup, and every failure this component has had came from
|
|
200
|
+
that handoff — a pad in a width-gated slot (#372), a pad removed by
|
|
201
|
+
`:showMoreActions="false"` (#407), banned utility classes to sit a pad beside a
|
|
202
|
+
title. `collapsible` on SectionHeader and CardHeader replaced it: the header asks
|
|
203
|
+
whether it is inside a Disclosure and renders the trigger itself.
|
|
207
204
|
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
With no consumer placing a trigger pad, the Teleport had nothing to do — it was
|
|
206
|
+
already `:disabled` for every one of them, which renders exactly here. So this is
|
|
207
|
+
not a behaviour change, it is the removal of a branch nothing took. See #408.
|
|
208
|
+
|
|
209
|
+
A consumer wanting the trigger somewhere specific puts a `collapsible` header
|
|
210
|
+
there, or writes `<DisclosureTrigger>` where they want it.
|
|
210
211
|
-->
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
<!--
|
|
213
|
+
Gated as a whole, not just the default content. A header that claims the trigger
|
|
214
|
+
(`collapsible`) suppresses this ENTIRELY, so a consumer supplying a custom `#trigger`
|
|
215
|
+
alongside a collapsible header gets one control rather than two. That is what the
|
|
216
|
+
Teleport's own `v-if` used to do, and dropping it to the default slot content alone
|
|
217
|
+
would have quietly changed it.
|
|
218
|
+
-->
|
|
219
|
+
<template v-if="rendersOwnTrigger">
|
|
215
220
|
<slot name="trigger"
|
|
216
221
|
:handleTrigger="handleTrigger"
|
|
217
222
|
:isOpen="isOpen"
|
|
218
223
|
:contentId="contentId"
|
|
219
224
|
>
|
|
220
|
-
<!--
|
|
221
|
-
The markup itself moved to DisclosureTrigger so a header can render
|
|
222
|
-
the same button in its own title row. Rendered here it is unchanged:
|
|
223
|
-
ButtonSlot treats an unset size prop and a false one alike.
|
|
224
|
-
-->
|
|
225
225
|
<DisclosureTrigger
|
|
226
226
|
:sm="$attrs.sm"
|
|
227
227
|
:md="$attrs.md"
|
|
@@ -231,7 +231,7 @@ onBeforeUnmount(() => {
|
|
|
231
231
|
:full="$attrs.full"
|
|
232
232
|
/>
|
|
233
233
|
</slot>
|
|
234
|
-
</
|
|
234
|
+
</template>
|
|
235
235
|
|
|
236
236
|
<!-- Same reasoning as the trigger above: in place rather than nowhere. -->
|
|
237
237
|
<Teleport :to="contentTarget"
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, provide, ref } from 'vue'
|
|
3
|
+
import { useResponsiveWidth } from '../../helpers'
|
|
4
|
+
|
|
2
5
|
const props = defineProps({
|
|
3
6
|
topAside: {
|
|
4
7
|
type: Boolean,
|
|
@@ -14,9 +17,41 @@ const props = defineProps({
|
|
|
14
17
|
default: false,
|
|
15
18
|
},
|
|
16
19
|
})
|
|
20
|
+
|
|
21
|
+
const layoutElement = ref(null)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 896, because that is `@4xl` — the container query in `two_column_layout.css` that puts
|
|
25
|
+
* the columns side by side. Measured here as well as queried there so components INSIDE a
|
|
26
|
+
* column can ask the same question, which CSS alone cannot tell them.
|
|
27
|
+
*
|
|
28
|
+
* Kept honest by a test asserting the stylesheet still breaks at `@4xl`. Two numbers that
|
|
29
|
+
* must agree is a drift risk; an unasserted one is a certainty.
|
|
30
|
+
*/
|
|
31
|
+
const SIDE_BY_SIDE_WIDTH = 896
|
|
32
|
+
|
|
33
|
+
const { responsiveWidth } = useResponsiveWidth(layoutElement, SIDE_BY_SIDE_WIDTH)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the layout is showing two columns right now.
|
|
37
|
+
*
|
|
38
|
+
* Provided because a descendant cannot work this out for itself. `SectionHeader` needs it:
|
|
39
|
+
* where its actions go depends on whether it is a COLUMN on a page or the page itself, and
|
|
40
|
+
* a 330px aside and a 330px phone are the same measurement with opposite answers. Its own
|
|
41
|
+
* container cannot tell them apart, and neither can a viewport breakpoint — this collapses
|
|
42
|
+
* on its own width, so the viewport it collapses at moves with whatever chrome is around
|
|
43
|
+
* it. In Storybook that is 32px of padding and the columns hold down to a 928px viewport.
|
|
44
|
+
*
|
|
45
|
+
* A ref rather than a value, through a slot boundary, same as `Card` provides
|
|
46
|
+
* `rsuiCardHasBody` — slot content is compiled in the parent's scope but mounted with this
|
|
47
|
+
* component as its parent instance, so inject resolves here.
|
|
48
|
+
*/
|
|
49
|
+
provide('rsuiTwoColumnLayout', {
|
|
50
|
+
isSideBySide: computed(() => responsiveWidth.value.specific),
|
|
51
|
+
})
|
|
17
52
|
</script>
|
|
18
53
|
<template>
|
|
19
|
-
<div class="rsui-two-column-layout">
|
|
54
|
+
<div ref="layoutElement" class="rsui-two-column-layout">
|
|
20
55
|
<div
|
|
21
56
|
:class="[
|
|
22
57
|
'rsui-two-column-layout__container',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed, useAttrs, useSlots } from 'vue'
|
|
2
|
+
import { ref, computed, inject, useAttrs, useSlots } from 'vue'
|
|
3
3
|
import { rendersContent } from '../../helpers/slots'
|
|
4
4
|
import ButtonTertiary from '../Button/ButtonTertiary.vue'
|
|
5
5
|
import Icon from '../Icon/Icon.vue'
|
|
@@ -72,19 +72,27 @@ const props = defineProps({
|
|
|
72
72
|
default: false,
|
|
73
73
|
},
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* Drop the badge onto its own line on a narrow header.
|
|
76
76
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* carrying a count then stacks three rows above the first card, which is the second
|
|
80
|
-
* of the two reasons TeamCoachingIndex gives for hand-placing its badge inside the
|
|
81
|
-
* title rather than using `#badge` at all.
|
|
77
|
+
* OPT-IN. It used to be the default, with `inlineBadge` to turn it off, and that was the
|
|
78
|
+
* wrong way round — BREAKING, hence the major.
|
|
82
79
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
80
|
+
* The slot is a count. The Section standards say so outright ("a badge in a section
|
|
81
|
+
* header says how many, not how it is going"), and 12 of 17 lms1 call sites are counts:
|
|
82
|
+
* `filteredCount`, `categoryGroup.count`, group counts. A number on its own line is
|
|
83
|
+
* severed from the thing it counts.
|
|
84
|
+
*
|
|
85
|
+
* `PageHeader` already settled the layout: its `__status` badge sits in
|
|
86
|
+
* `__title-text-container` with `items-center` and has no narrow branch at all.
|
|
87
|
+
*
|
|
88
|
+
* Measured before the flip: a 25px badge with 211px of clear room beside it still went
|
|
89
|
+
* to row two, costing 26px of header height for nothing. Of four test cases only one had
|
|
90
|
+
* a title long enough to need the wrap.
|
|
91
|
+
*
|
|
92
|
+
* Kept rather than deleted, because stacking is defensible when it is CHOSEN — a long
|
|
93
|
+
* label badge, rather than a count, may genuinely want its own line. See #407.
|
|
86
94
|
*/
|
|
87
|
-
|
|
95
|
+
stackBadge: {
|
|
88
96
|
type: Boolean,
|
|
89
97
|
default: false,
|
|
90
98
|
},
|
|
@@ -117,6 +125,43 @@ const sectionHeaderElement = ref(null)
|
|
|
117
125
|
|
|
118
126
|
const { responsiveWidth } = useResponsiveWidth(sectionHeaderElement, 640)
|
|
119
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Whether the enclosing `TwoColumnLayout` is currently showing two columns, or null when
|
|
130
|
+
* there is no layout around this header.
|
|
131
|
+
*
|
|
132
|
+
* This is the question `responsiveWidth` cannot answer. The container says how much room
|
|
133
|
+
* the header has — truncation, whether a badge fits — and that is the right question for
|
|
134
|
+
* everything inside it. Where the ACTIONS go is a different question: a 330px aside on a
|
|
135
|
+
* desktop wants its single "View all" beside the title, and the same 330px on a phone
|
|
136
|
+
* wants it underneath. Identical container, opposite answers, so the container cannot
|
|
137
|
+
* decide it.
|
|
138
|
+
*
|
|
139
|
+
* A viewport breakpoint cannot decide it either, which is what the first cut of this got
|
|
140
|
+
* wrong. `TwoColumnLayout` collapses on a CONTAINER query — 896px of its own width — so
|
|
141
|
+
* the viewport it collapses at moves with whatever chrome surrounds it. In Storybook that
|
|
142
|
+
* is 32px of padding and the columns hold down to a 928px viewport, and the 64rem constant
|
|
143
|
+
* stacked the actions across the whole 928–1023 band while the aside was still an aside.
|
|
144
|
+
* More page furniture moves the boundary the other way. No constant fits.
|
|
145
|
+
*
|
|
146
|
+
* So the header rides on the layout's own answer rather than approximating it, and the
|
|
147
|
+
* two can never disagree.
|
|
148
|
+
*/
|
|
149
|
+
const twoColumnLayout = inject('rsuiTwoColumnLayout', null)
|
|
150
|
+
|
|
151
|
+
const isColumnBesideAnother = computed(() => twoColumnLayout?.isSideBySide.value ?? false)
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The actions stay in the header row while this header is one of two columns, however
|
|
155
|
+
* narrow the column is. A single "View all" beside a wrapped title reads better in a 330px
|
|
156
|
+
* aside than the same link dropped into a left-aligned block below it — and a view-all is
|
|
157
|
+
* the only thing in this slot in practice: of 17 lms1 headers using `#actions`, 12 hold
|
|
158
|
+
* exactly that and none renders two controls at once.
|
|
159
|
+
*
|
|
160
|
+
* Once the layout collapses, every column is the full width of the page and the container
|
|
161
|
+
* measurement answers it correctly on its own, which is what it did before any of this.
|
|
162
|
+
*/
|
|
163
|
+
const keepsActionsInline = computed(() => responsiveWidth.value.specific || isColumnBesideAnother.value)
|
|
164
|
+
|
|
120
165
|
const emit = defineEmits(['click:more-actions'])
|
|
121
166
|
|
|
122
167
|
function handleMoreActionsClick() {
|
|
@@ -217,7 +262,11 @@ const titleTag = computed(() => HEADING_LEVELS.includes(String(props.headingLeve
|
|
|
217
262
|
// It used to come from `md:` — a viewport query at 768 — while the icon
|
|
218
263
|
// gate, the actions swap and the badge stacking all read the container at
|
|
219
264
|
// 640, so a narrow column on a wide screen got both layouts at once. #420.
|
|
220
|
-
'rsui-section-header--narrow': !
|
|
265
|
+
'rsui-section-header--narrow': !keepsActionsInline,
|
|
266
|
+
// Container only, deliberately NOT the one above. Where the actions go is a
|
|
267
|
+
// question about the page; whether the title has room is a question about this
|
|
268
|
+
// column, and the two want different answers in an aside on a wide screen.
|
|
269
|
+
'rsui-section-header--narrow-column': !responsiveWidth.specific,
|
|
221
270
|
}
|
|
222
271
|
]"
|
|
223
272
|
:role="isClickable ? 'button' : undefined"
|
|
@@ -263,7 +312,7 @@ const titleTag = computed(() => HEADING_LEVELS.includes(String(props.headingLeve
|
|
|
263
312
|
<!-- Title slot, default slot -->
|
|
264
313
|
<div :class="[
|
|
265
314
|
'rsui-section-header__title',
|
|
266
|
-
{ 'rsui-section-header__title--stacked': !responsiveWidth.specific &&
|
|
315
|
+
{ 'rsui-section-header__title--stacked': !responsiveWidth.specific && stackBadge },
|
|
267
316
|
]">
|
|
268
317
|
<component :is="titleTag"
|
|
269
318
|
:class="[
|
|
@@ -292,7 +341,7 @@ const titleTag = computed(() => HEADING_LEVELS.includes(String(props.headingLeve
|
|
|
292
341
|
>
|
|
293
342
|
<!-- Desktop actions slot, optional -->
|
|
294
343
|
<div class="rsui-section-header__actions-desktop"
|
|
295
|
-
v-if="
|
|
344
|
+
v-if="keepsActionsInline && $slots.actions"
|
|
296
345
|
>
|
|
297
346
|
<slot name="actions"></slot>
|
|
298
347
|
</div>
|
|
@@ -321,7 +370,7 @@ const titleTag = computed(() => HEADING_LEVELS.includes(String(props.headingLeve
|
|
|
321
370
|
|
|
322
371
|
<!-- Mobile actions slot, optional -->
|
|
323
372
|
<div class="rsui-section-header__actions-mobile"
|
|
324
|
-
v-if="showMobileActions && !
|
|
373
|
+
v-if="showMobileActions && !keepsActionsInline && $slots.actions"
|
|
325
374
|
>
|
|
326
375
|
<slot name="actions"></slot>
|
|
327
376
|
</div>
|