@boostdev/design-system-components 2.3.0 → 2.4.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/dist/client.cjs +114 -69
- package/dist/client.css +537 -537
- package/dist/client.d.cts +30 -26
- package/dist/client.d.ts +30 -26
- package/dist/client.js +114 -69
- package/dist/index.cjs +114 -69
- package/dist/index.css +537 -537
- package/dist/index.d.cts +30 -26
- package/dist/index.d.ts +30 -26
- package/dist/index.js +114 -69
- package/dist/web-components/index.d.ts +9 -5
- package/dist/web-components/index.js +55 -12
- package/package.json +1 -1
- package/src/components/layout/Grid/Grid.mdx +4 -2
- package/src/components/layout/Grid/Grid.spec.tsx +85 -6
- package/src/components/layout/Grid/Grid.stories.tsx +16 -9
- package/src/components/layout/Grid/Grid.tsx +11 -6
- package/src/components/layout/Grid/masonry.ts +135 -33
- package/src/web-components/layout/BdsGrid.mdx +5 -3
- package/src/web-components/layout/BdsGrid.stories.tsx +16 -10
- package/src/web-components/layout/bds-grid.spec.ts +3 -3
- package/src/web-components/layout/bds-grid.ts +19 -6
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
import { RefObject, useLayoutEffect } from 'react';
|
|
2
|
+
import type { GridItemSpanValue } from './GridItem';
|
|
3
|
+
|
|
4
|
+
type NamedSpan = Exclude<GridItemSpanValue, 'auto' | number>;
|
|
5
|
+
|
|
6
|
+
// Named spans resolve through the foundation's responsive column tokens so they
|
|
7
|
+
// match the behavior of `columnSpan` on `GridItem`. The polyfill reads the live
|
|
8
|
+
// value at each run, which is how a single `preferredColumnSpan: 'half'` gives
|
|
9
|
+
// 2 cols on a 4-col mobile grid and 6 cols on a 12-col desktop grid.
|
|
10
|
+
const NAMED_SPAN_VAR: Record<Exclude<NamedSpan, 'full'>, string> = {
|
|
11
|
+
'three-quarters': '--bds-grid_columns-75',
|
|
12
|
+
'two-thirds': '--bds-grid_columns-66',
|
|
13
|
+
half: '--bds-grid_columns-50',
|
|
14
|
+
'one-third': '--bds-grid_columns-33',
|
|
15
|
+
'one-quarter': '--bds-grid_columns-25',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mathematical fallback for environments where the foundation CSS isn't loaded
|
|
19
|
+
// (e.g. jsdom tests). Round to the nearest integer; clamp to [1, totalCols].
|
|
20
|
+
const NAMED_SPAN_FRACTION: Record<Exclude<NamedSpan, 'full'>, number> = {
|
|
21
|
+
'three-quarters': 0.75,
|
|
22
|
+
'two-thirds': 2 / 3,
|
|
23
|
+
half: 0.5,
|
|
24
|
+
'one-third': 1 / 3,
|
|
25
|
+
'one-quarter': 0.25,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolvePreferredColumns(
|
|
29
|
+
container: HTMLElement,
|
|
30
|
+
value: Exclude<GridItemSpanValue, 'auto'>,
|
|
31
|
+
totalCols: number,
|
|
32
|
+
): number {
|
|
33
|
+
if (value === 'full') return totalCols;
|
|
34
|
+
if (typeof value === 'number') return Math.max(1, Math.min(totalCols, value));
|
|
35
|
+
const varName = NAMED_SPAN_VAR[value];
|
|
36
|
+
const raw = window.getComputedStyle(container).getPropertyValue(varName).trim();
|
|
37
|
+
const n = parseInt(raw, 10);
|
|
38
|
+
if (Number.isFinite(n) && n > 0) return Math.min(totalCols, n);
|
|
39
|
+
return Math.max(1, Math.round(totalCols * NAMED_SPAN_FRACTION[value]));
|
|
40
|
+
}
|
|
2
41
|
|
|
3
42
|
/**
|
|
4
43
|
* Masonry layout polyfill based on CSS Grid Layout Module Level 3 (a.k.a. "grid-lanes").
|
|
@@ -38,16 +77,24 @@ export function supportsNativeMasonry(): boolean {
|
|
|
38
77
|
|
|
39
78
|
export type MasonryOptions = {
|
|
40
79
|
/**
|
|
41
|
-
* When set
|
|
42
|
-
* `data-column-span-auto
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
80
|
+
* When set, the polyfill assigns column spans to items marked
|
|
81
|
+
* `data-column-span-auto`. Named values (`'half'`, `'one-third'`, etc.) are
|
|
82
|
+
* resolved through the foundation's responsive column tokens so the same
|
|
83
|
+
* option yields the right span at every breakpoint. A `number` is a literal
|
|
84
|
+
* column count. If a preferred-span item would overflow the row, it is
|
|
85
|
+
* demoted to fill only the remaining columns (never less than 1). Items with
|
|
86
|
+
* an explicit `columnSpan` keep their span; their span still consumes column
|
|
87
|
+
* budget for the demotion calculation.
|
|
47
88
|
*/
|
|
48
|
-
preferredColumnSpan?:
|
|
89
|
+
preferredColumnSpan?: Exclude<GridItemSpanValue, 'auto'>;
|
|
49
90
|
};
|
|
50
91
|
|
|
92
|
+
// Data attribute used to remember an auto item's inline `grid-column` before the
|
|
93
|
+
// polyfill overwrites it — restored on cleanup or when the preferred option is
|
|
94
|
+
// unset, so a Storybook control going from preferred=2 → undefined doesn't leave
|
|
95
|
+
// items pinned to their polyfill-assigned span.
|
|
96
|
+
const ORIGINAL_COL_ATTR = 'data-bds-masonry-original-column';
|
|
97
|
+
|
|
51
98
|
export function applyMasonryLayout(
|
|
52
99
|
container: HTMLElement,
|
|
53
100
|
rawChildren: HTMLElement[],
|
|
@@ -63,34 +110,55 @@ export function applyMasonryLayout(
|
|
|
63
110
|
container.style.gridAutoRows = `${ROW_UNIT_PX}px`;
|
|
64
111
|
container.style.rowGap = '0px';
|
|
65
112
|
container.style.gridAutoFlow = 'dense';
|
|
113
|
+
// `dense` + per-child `ResizeObserver` can fight the browser's scroll anchoring:
|
|
114
|
+
// a subpixel child resize (scrollbar toggling, late image decode, font swap) re-runs
|
|
115
|
+
// the pack, items above the viewport shift, and the browser yanks the viewport up
|
|
116
|
+
// to "follow" them. `overflow-anchor: none` doesn't inherit, so setting it on the
|
|
117
|
+
// container alone doesn't exclude grid children from anchor selection — the browser
|
|
118
|
+
// will still pick one of the `<GridItem>`s as the anchor and drag the viewport when
|
|
119
|
+
// that item's row-span is rewritten. We set it on the container AND every child so
|
|
120
|
+
// the browser picks an anchor outside the grid entirely. Cleared in clearMasonryLayout.
|
|
121
|
+
container.style.overflowAnchor = 'none';
|
|
66
122
|
|
|
67
123
|
const children = rawChildren.filter(
|
|
68
124
|
(el) => el.nodeType === 1 && window.getComputedStyle(el).display !== 'none',
|
|
69
125
|
);
|
|
70
126
|
|
|
71
|
-
// Pass 1 —
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
127
|
+
// Pass 1 — force `align-self: start`: the default `stretch` would grow each item
|
|
128
|
+
// to fill the 1px track, making `offsetHeight` return 1 for every child. Also
|
|
129
|
+
// propagate `overflow-anchor: none` to each child (see container comment above).
|
|
130
|
+
//
|
|
131
|
+
// We intentionally do NOT clear `gridRow*` before measuring — with `align-self: start`,
|
|
132
|
+
// an item's `offsetHeight` is its natural content height regardless of the assigned
|
|
133
|
+
// span, so the stale span doesn't bias the measurement. Keeping the old span during
|
|
134
|
+
// the measurement phase means the container's block-size doesn't momentarily collapse
|
|
135
|
+
// (which is the other shape the scroll jump can take: grid height drops → document
|
|
136
|
+
// scrollHeight drops → browser clamps `scrollTop` → visible jump even with
|
|
137
|
+
// `overflow-anchor: none`).
|
|
75
138
|
for (const child of children) {
|
|
76
|
-
child.style.gridRow = '';
|
|
77
|
-
child.style.gridRowStart = '';
|
|
78
|
-
child.style.gridRowEnd = '';
|
|
79
139
|
child.style.alignSelf = 'start';
|
|
140
|
+
child.style.overflowAnchor = 'none';
|
|
80
141
|
}
|
|
81
142
|
|
|
82
143
|
// Preferred-column-span pass — optional. Assigns column spans to `data-column-span-auto`
|
|
83
144
|
// items, demoting to fit remaining row space. Runs before height measurement so items
|
|
84
|
-
// are measured at the width they will actually render.
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
// are measured at the width they will actually render. When the option is not set,
|
|
146
|
+
// any previously-written spans are restored to their original inline value so
|
|
147
|
+
// a Storybook control change from preferred='half' → undefined doesn't leave auto
|
|
148
|
+
// items pinned.
|
|
149
|
+
const preferred = options.preferredColumnSpan;
|
|
150
|
+
if (preferred !== undefined) {
|
|
87
151
|
const totalCols = countGridColumns(container);
|
|
88
|
-
|
|
152
|
+
const preferredCols = totalCols > 0 ? resolvePreferredColumns(container, preferred, totalCols) : 0;
|
|
153
|
+
if (totalCols > 0 && preferredCols > 0) {
|
|
89
154
|
let col = 0;
|
|
90
155
|
for (const child of children) {
|
|
91
156
|
if (child.hasAttribute('data-column-span-auto')) {
|
|
157
|
+
if (!child.hasAttribute(ORIGINAL_COL_ATTR)) {
|
|
158
|
+
child.setAttribute(ORIGINAL_COL_ATTR, child.style.gridColumn);
|
|
159
|
+
}
|
|
92
160
|
const remaining = totalCols - col;
|
|
93
|
-
const span = Math.max(1, Math.min(
|
|
161
|
+
const span = Math.max(1, Math.min(preferredCols, remaining));
|
|
94
162
|
child.style.gridColumn = `span ${span}`;
|
|
95
163
|
col = (col + span) % totalCols;
|
|
96
164
|
} else {
|
|
@@ -100,6 +168,11 @@ export function applyMasonryLayout(
|
|
|
100
168
|
}
|
|
101
169
|
}
|
|
102
170
|
}
|
|
171
|
+
} else {
|
|
172
|
+
// No preferred span — restore any items the polyfill previously wrote.
|
|
173
|
+
for (const child of children) {
|
|
174
|
+
restoreOriginalColumn(child);
|
|
175
|
+
}
|
|
103
176
|
}
|
|
104
177
|
|
|
105
178
|
// Pass 2 — read all heights in a tight loop. The first offsetHeight read forces one
|
|
@@ -149,21 +222,26 @@ export function clearMasonryLayout(container: HTMLElement, rawChildren: HTMLElem
|
|
|
149
222
|
container.style.gridAutoRows = '';
|
|
150
223
|
container.style.gridAutoFlow = '';
|
|
151
224
|
container.style.rowGap = '';
|
|
225
|
+
container.style.overflowAnchor = '';
|
|
152
226
|
for (const child of rawChildren) {
|
|
153
227
|
if (child.nodeType !== 1) continue;
|
|
154
228
|
child.style.gridRow = '';
|
|
155
229
|
child.style.gridRowStart = '';
|
|
156
230
|
child.style.gridRowEnd = '';
|
|
157
231
|
child.style.alignSelf = '';
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
child.style.gridColumn = '';
|
|
163
|
-
}
|
|
232
|
+
child.style.overflowAnchor = '';
|
|
233
|
+
// Restore any `gridColumn` value the polyfill overwrote. Items the
|
|
234
|
+
// polyfill never touched have no ORIGINAL_COL_ATTR and are left alone.
|
|
235
|
+
restoreOriginalColumn(child);
|
|
164
236
|
}
|
|
165
237
|
}
|
|
166
238
|
|
|
239
|
+
function restoreOriginalColumn(el: HTMLElement): void {
|
|
240
|
+
if (!el.hasAttribute(ORIGINAL_COL_ATTR)) return;
|
|
241
|
+
el.style.gridColumn = el.getAttribute(ORIGINAL_COL_ATTR) ?? '';
|
|
242
|
+
el.removeAttribute(ORIGINAL_COL_ATTR);
|
|
243
|
+
}
|
|
244
|
+
|
|
167
245
|
/**
|
|
168
246
|
* React hook that runs the polyfill against `ref.current` and re-runs on
|
|
169
247
|
* size / mutation changes. No-op when `enabled` is false or when the browser
|
|
@@ -181,31 +259,55 @@ export function useMasonry(
|
|
|
181
259
|
if (supportsNativeMasonry()) return;
|
|
182
260
|
|
|
183
261
|
let frame = 0;
|
|
184
|
-
const
|
|
262
|
+
const runNow = () => {
|
|
185
263
|
cancelAnimationFrame(frame);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
preferredColumnSpan,
|
|
189
|
-
});
|
|
264
|
+
applyMasonryLayout(container, Array.from(container.children) as HTMLElement[], {
|
|
265
|
+
preferredColumnSpan,
|
|
190
266
|
});
|
|
191
267
|
};
|
|
268
|
+
const schedule = () => {
|
|
269
|
+
cancelAnimationFrame(frame);
|
|
270
|
+
frame = requestAnimationFrame(runNow);
|
|
271
|
+
};
|
|
192
272
|
|
|
193
273
|
schedule();
|
|
194
274
|
|
|
275
|
+
// ResizeObserver fires during the "update the rendering" step, AFTER layout and
|
|
276
|
+
// BEFORE paint. Running the polyfill synchronously here means the paint that
|
|
277
|
+
// follows uses the freshly-computed row-spans — not the next frame's. If we
|
|
278
|
+
// defer to rAF instead, there's a visible frame where a grown item (e.g. a
|
|
279
|
+
// lazy-loaded image whose dimensions just resolved) overflows its stale cell
|
|
280
|
+
// and visually overlaps the next item in its column — easy to see on fast
|
|
281
|
+
// scroll through a long gallery.
|
|
195
282
|
const ro =
|
|
196
|
-
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(
|
|
283
|
+
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(runNow) : null;
|
|
284
|
+
const observeDescendants = (child: Element) => {
|
|
285
|
+
if (!ro) return;
|
|
286
|
+
ro.observe(child);
|
|
287
|
+
// Also observe media descendants directly. `loading="lazy"` images decode
|
|
288
|
+
// asynchronously; if the consumer hasn't reserved aspect ratio, the parent
|
|
289
|
+
// GridItem's size only changes once the image pushes it larger — by which
|
|
290
|
+
// point the next frame has painted with the stale cell. Observing the image
|
|
291
|
+
// itself means the polyfill re-runs in the same frame as the decode.
|
|
292
|
+
for (const media of Array.from(child.querySelectorAll('img, video'))) {
|
|
293
|
+
ro.observe(media);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
197
296
|
if (ro) {
|
|
198
297
|
// Observe each child's size, not the container — re-running when the container
|
|
199
298
|
// resizes causes a feedback loop (we set grid-auto-rows, which changes container
|
|
200
299
|
// block-size, which fires the observer). Child-size changes are what we actually
|
|
201
300
|
// need to respond to.
|
|
202
|
-
for (const child of Array.from(container.children))
|
|
301
|
+
for (const child of Array.from(container.children)) observeDescendants(child);
|
|
203
302
|
}
|
|
204
303
|
|
|
304
|
+
// MutationObserver fires as a microtask after DOM mutations — layout hasn't
|
|
305
|
+
// happened yet, so defer to rAF to let the browser lay out the new children
|
|
306
|
+
// before we measure them.
|
|
205
307
|
const mo =
|
|
206
308
|
typeof MutationObserver !== 'undefined'
|
|
207
309
|
? new MutationObserver(() => {
|
|
208
|
-
|
|
310
|
+
for (const child of Array.from(container.children)) observeDescendants(child);
|
|
209
311
|
schedule();
|
|
210
312
|
})
|
|
211
313
|
: null;
|
|
@@ -49,7 +49,7 @@ Numeric `column-span="n"` is literal — it does not adapt. Use named values for
|
|
|
49
49
|
| `is-centered` | boolean | `false` | Caps max-width via `--bds-container_max-width` |
|
|
50
50
|
| `is-masonry` | boolean | `false` | Enables masonry layout (CSS Grid Level 3 "grid-lanes"). Uses native CSS when supported, otherwise runs a JS polyfill. |
|
|
51
51
|
| `auto-span-media` | boolean | `false` | Child `<bds-grid-item>`s without an explicit `column-span` resolve their span from the intrinsic aspect ratio of their first `<img>`/`<video>` descendant. See [Aspect-ratio auto-span](#aspect-ratio-auto-span). |
|
|
52
|
-
| `masonry-preferred-column-span` | number
|
|
52
|
+
| `masonry-preferred-column-span` | named span \| number | — | In masonry mode, items without an explicit `column-span` try to span this value, demoting to fit at row ends. Accepts the same named values as `column-span` (`"half"`, `"one-third"`, etc.). See [Masonry preferred column span](#masonry-preferred-column-span). |
|
|
53
53
|
|
|
54
54
|
## Slots
|
|
55
55
|
|
|
@@ -164,15 +164,17 @@ Named-span responsive behavior still applies — on mobile every item collapses
|
|
|
164
164
|
|
|
165
165
|
## Masonry preferred column span
|
|
166
166
|
|
|
167
|
-
For a uniform "cards mostly
|
|
167
|
+
For a uniform "cards mostly half-width" feed, set `masonry-preferred-column-span` on the `<bds-grid>`. Items without an explicit `column-span` try to span that named fraction of the row. When an item would sit at the end of a row with fewer columns remaining than the preferred span, the polyfill **demotes** its span to just fill the remaining columns — the item never wraps to a new row leaving a visible gap behind it.
|
|
168
168
|
|
|
169
169
|
```html
|
|
170
|
-
<bds-grid is-masonry masonry-preferred-column-span="
|
|
170
|
+
<bds-grid is-masonry masonry-preferred-column-span="half">
|
|
171
171
|
<bds-grid-item>…</bds-grid-item>
|
|
172
172
|
<bds-grid-item>…</bds-grid-item>
|
|
173
173
|
</bds-grid>
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
Accepted values match `column-span` on `<bds-grid-item>`: `"full"`, `"three-quarters"`, `"two-thirds"`, `"half"`, `"one-third"`, `"one-quarter"`. Named values resolve through the foundation's responsive column tokens, so the same option adapts across breakpoints — `"half"` yields 2 cols on a 4-col mobile grid, 4 cols on an 8-col tablet grid, and 6 cols on a 12-col desktop grid. A numeric string (e.g. `"2"`) is accepted too, but is a literal column count and does **not** adapt — prefer named values unless you're using `variant="custom"`.
|
|
177
|
+
|
|
176
178
|
Rules:
|
|
177
179
|
|
|
178
180
|
- Requires `is-masonry`. No-op on non-masonry grids.
|
|
@@ -16,7 +16,7 @@ function BdsGrid({
|
|
|
16
16
|
columns?: number;
|
|
17
17
|
isCentered?: boolean;
|
|
18
18
|
isMasonry?: boolean;
|
|
19
|
-
masonryPreferredColumnSpan?:
|
|
19
|
+
masonryPreferredColumnSpan?: string;
|
|
20
20
|
children?: React.ReactNode;
|
|
21
21
|
}) {
|
|
22
22
|
return React.createElement(
|
|
@@ -74,7 +74,10 @@ const meta = {
|
|
|
74
74
|
columns: { control: 'number' },
|
|
75
75
|
isCentered: { control: 'boolean' },
|
|
76
76
|
isMasonry: { control: 'boolean' },
|
|
77
|
-
masonryPreferredColumnSpan: {
|
|
77
|
+
masonryPreferredColumnSpan: {
|
|
78
|
+
control: 'select',
|
|
79
|
+
options: ['full', 'three-quarters', 'two-thirds', 'half', 'one-third', 'one-quarter'],
|
|
80
|
+
},
|
|
78
81
|
},
|
|
79
82
|
} satisfies Meta<typeof BdsGrid>;
|
|
80
83
|
|
|
@@ -194,16 +197,19 @@ export const MasonryWithSpans: Story = {
|
|
|
194
197
|
};
|
|
195
198
|
|
|
196
199
|
/**
|
|
197
|
-
* `masonry-preferred-column-span="
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
+
* `masonry-preferred-column-span="half"` makes every auto-span item try to
|
|
201
|
+
* span half the grid. Named values resolve through the foundation's responsive
|
|
202
|
+
* column tokens, so the same value yields 2 cols on a 4-col mobile grid, 4 on
|
|
203
|
+
* an 8-col tablet grid, and 6 on a 12-col desktop grid.
|
|
204
|
+
*
|
|
205
|
+
* When an item would overflow the row, the polyfill **demotes** its span to
|
|
206
|
+
* fill just the remaining columns, so no trailing gap appears.
|
|
200
207
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* current column count.
|
|
208
|
+
* Swap `masonryPreferredColumnSpan` between named values in the Controls and
|
|
209
|
+
* resize the viewport to watch the demotion adapt to the current column count.
|
|
204
210
|
*/
|
|
205
211
|
export const MasonryPreferredColumnSpan: Story = {
|
|
206
|
-
args: { isMasonry: true, masonryPreferredColumnSpan:
|
|
212
|
+
args: { isMasonry: true, masonryPreferredColumnSpan: 'half' },
|
|
207
213
|
render: (args) => (
|
|
208
214
|
<BdsGrid {...args}>
|
|
209
215
|
{Array.from({ length: 7 }).map((_, i) => (
|
|
@@ -219,7 +225,7 @@ export const MasonryPreferredColumnSpan: Story = {
|
|
|
219
225
|
* auto items fill the remaining row consistently.
|
|
220
226
|
*/
|
|
221
227
|
export const MasonryPreferredColumnSpanWithHero: Story = {
|
|
222
|
-
args: { isMasonry: true, masonryPreferredColumnSpan:
|
|
228
|
+
args: { isMasonry: true, masonryPreferredColumnSpan: 'half' },
|
|
223
229
|
render: (args) => (
|
|
224
230
|
<BdsGrid {...args}>
|
|
225
231
|
<Item columnSpan="three-quarters"><div style={masonryCellStyle(0)}>Hero (three-quarters)</div></Item>
|
|
@@ -60,11 +60,11 @@ describe('bds-grid', () => {
|
|
|
60
60
|
cleanup(el);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it('reflects masonry-preferred-column-span as a
|
|
63
|
+
it('reflects masonry-preferred-column-span as a string property', async () => {
|
|
64
64
|
const el = await fixture(
|
|
65
|
-
'<bds-grid is-masonry masonry-preferred-column-span="
|
|
65
|
+
'<bds-grid is-masonry masonry-preferred-column-span="half"></bds-grid>',
|
|
66
66
|
) as BdsGrid;
|
|
67
|
-
expect(el.masonryPreferredColumnSpan).toBe(
|
|
67
|
+
expect(el.masonryPreferredColumnSpan).toBe('half');
|
|
68
68
|
cleanup(el);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, css, html } from 'lit';
|
|
2
2
|
import { applyMasonryLayout, clearMasonryLayout, supportsNativeMasonry } from '../../components/layout/Grid/masonry';
|
|
3
|
+
import type { GridItemSpanValue } from '../../components/layout/Grid/GridItem';
|
|
3
4
|
|
|
4
5
|
export type GridVariant = 'main' | 'page' | 'funnel' | 'custom';
|
|
5
6
|
|
|
@@ -16,9 +17,12 @@ export type GridVariant = 'main' | 'page' | 'funnel' | 'custom';
|
|
|
16
17
|
* auto-span-media — boolean; child `<bds-grid-item>`s without a `column-span`
|
|
17
18
|
* attribute resolve their span from the intrinsic aspect
|
|
18
19
|
* ratio of their first `<img>`/`<video>` child.
|
|
19
|
-
* masonry-preferred-column-span — number; masonry only. Child
|
|
20
|
-
* `column-span` attribute try to span this
|
|
21
|
-
*
|
|
20
|
+
* masonry-preferred-column-span — named span or number; masonry only. Child
|
|
21
|
+
* items without a `column-span` attribute try to span this
|
|
22
|
+
* value (e.g. `"half"`, `"one-third"`). Named values adapt
|
|
23
|
+
* across breakpoints via the foundation's responsive column
|
|
24
|
+
* tokens. An item at row-end is demoted to the remaining
|
|
25
|
+
* column count. Numeric values are literal and don't adapt.
|
|
22
26
|
*
|
|
23
27
|
* Slots:
|
|
24
28
|
* (default) — grid content; typically `<bds-grid-item>` children
|
|
@@ -80,7 +84,7 @@ export class BdsGrid extends LitElement {
|
|
|
80
84
|
isMasonry: { type: Boolean, attribute: 'is-masonry', reflect: true },
|
|
81
85
|
autoSpanMedia: { type: Boolean, attribute: 'auto-span-media', reflect: true },
|
|
82
86
|
masonryPreferredColumnSpan: {
|
|
83
|
-
type:
|
|
87
|
+
type: String,
|
|
84
88
|
attribute: 'masonry-preferred-column-span',
|
|
85
89
|
reflect: true,
|
|
86
90
|
},
|
|
@@ -91,7 +95,7 @@ export class BdsGrid extends LitElement {
|
|
|
91
95
|
declare isCentered: boolean;
|
|
92
96
|
declare isMasonry: boolean;
|
|
93
97
|
declare autoSpanMedia: boolean;
|
|
94
|
-
declare masonryPreferredColumnSpan:
|
|
98
|
+
declare masonryPreferredColumnSpan: string | undefined;
|
|
95
99
|
|
|
96
100
|
private _ro?: ResizeObserver;
|
|
97
101
|
private _mo?: MutationObserver;
|
|
@@ -156,12 +160,21 @@ export class BdsGrid extends LitElement {
|
|
|
156
160
|
const container = this._gridEl();
|
|
157
161
|
if (container) {
|
|
158
162
|
applyMasonryLayout(container, this._items(), {
|
|
159
|
-
preferredColumnSpan: this.
|
|
163
|
+
preferredColumnSpan: this._resolvePreferredColumnSpan(),
|
|
160
164
|
});
|
|
161
165
|
}
|
|
162
166
|
});
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
private _resolvePreferredColumnSpan(): Exclude<GridItemSpanValue, 'auto'> | undefined {
|
|
170
|
+
const raw = this.masonryPreferredColumnSpan;
|
|
171
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
172
|
+
// Numeric attribute → literal column count; otherwise treat as a named span.
|
|
173
|
+
const n = Number(raw);
|
|
174
|
+
if (Number.isFinite(n) && String(n) === raw) return n;
|
|
175
|
+
return raw as Exclude<GridItemSpanValue, 'auto'>;
|
|
176
|
+
}
|
|
177
|
+
|
|
165
178
|
private _teardownMasonry() {
|
|
166
179
|
cancelAnimationFrame(this._frame);
|
|
167
180
|
this._ro?.disconnect();
|