@orangesk/orange-design-system 2.0.0-beta.42 → 2.0.0-beta.44
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/build/components/AnchorNavigation/style.css +1 -1
- package/build/components/AnchorNavigation/style.css.map +1 -1
- package/build/components/BlockAction/style.css +1 -1
- package/build/components/BlockAction/style.css.map +1 -1
- package/build/components/BodyBanner/style.css +1 -1
- package/build/components/BodyBanner/style.css.map +1 -1
- package/build/components/Breadcrumbs/style.css +1 -1
- package/build/components/Breadcrumbs/style.css.map +1 -1
- package/build/components/Carousel/style.css +1 -1
- package/build/components/Carousel/style.css.map +1 -1
- package/build/components/Expander/style.css +1 -1
- package/build/components/Expander/style.css.map +1 -1
- package/build/components/Footer/style.css +1 -1
- package/build/components/Footer/style.css.map +1 -1
- package/build/components/Grid/style.css +1 -1
- package/build/components/Grid/style.css.map +1 -1
- package/build/components/Megamenu/style.css +1 -1
- package/build/components/Megamenu/style.css.map +1 -1
- package/build/components/Tag/style.css +1 -1
- package/build/components/Tag/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/components/tsconfig.tsbuildinfo +1 -1
- package/build/components/types/index.d.ts +4 -2
- package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.static.d.ts +27 -0
- package/build/components/types/src/components/Carousel/Carousel.static.d.ts +7 -5
- package/build/components/types/src/components/Expander/Expander.d.ts +2 -0
- package/build/components/types/src/components/Grid/Grid.d.ts +2 -2
- package/build/components/types/src/components/Megamenu/Megamenu.static.d.ts +3 -0
- package/build/components/types/src/components/Modal/Modal.static.d.ts +1 -0
- package/build/lib/base.css +1 -1
- package/build/lib/base.css.map +1 -1
- package/build/lib/components.css +1 -1
- package/build/lib/components.css.map +1 -1
- package/build/lib/footer.css +1 -1
- package/build/lib/footer.css.map +1 -1
- package/build/lib/megamenu.css +1 -1
- package/build/lib/megamenu.css.map +1 -1
- package/build/lib/megamenu.js +1 -1
- package/build/lib/megamenu.js.map +1 -1
- package/build/lib/scripts.js +1 -1
- package/build/lib/scripts.js.map +1 -1
- package/build/lib/style.css +1 -1
- package/build/lib/style.css.map +1 -1
- package/build/lib/tsconfig.tsbuildinfo +1 -1
- package/build/search-index.json +2 -2
- package/package.json +13 -13
- package/src/components/AnchorNavigation/AnchorNavigation.static.ts +247 -40
- package/src/components/AnchorNavigation/styles/mixins.scss +8 -0
- package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.jsx +72 -0
- package/src/components/BlockAction/styles/mixins.scss +1 -1
- package/src/components/BodyBanner/styles/mixins.scss +2 -12
- package/src/components/Carousel/Carousel.static.ts +110 -79
- package/src/components/Carousel/styles/mixins.scss +2 -2
- package/src/components/Carousel/tests/Carousel.static.test.jsx +203 -0
- package/src/components/Expander/Expander.tsx +4 -0
- package/src/components/Expander/styles/style.scss +12 -0
- package/src/components/Expander/tests/Expander.conformance.test.jsx +4 -0
- package/src/components/Expander/tests/Expander.unit.test.jsx +9 -0
- package/src/components/Grid/Grid.tsx +5 -2
- package/src/components/Grid/styles/config.scss +3 -2
- package/src/components/Grid/tests/Grid.unit.test.jsx +40 -10
- package/src/components/Megamenu/Megamenu.static.ts +27 -1
- package/src/components/Megamenu/styles/mixins.scss +4 -0
- package/src/components/Modal/Modal.static.ts +29 -7
- package/src/components/Tag/styles/config.scss +5 -1
- package/src/components/Tag/styles/mixins.scss +2 -1
- package/src/styles/base/globals.scss +1 -0
- package/src/styles/export/base.js +1 -1
- package/src/styles/tokens/base.scss +1 -1
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
margin-bottom: 0 !important;
|
|
33
33
|
padding: convert.to-rem(25px) 0 !important;
|
|
34
34
|
white-space: nowrap;
|
|
35
|
+
color: inherit;
|
|
35
36
|
text-decoration: none !important;
|
|
36
37
|
display: inline-block;
|
|
37
38
|
cursor: pointer;
|
|
@@ -39,6 +40,13 @@
|
|
|
39
40
|
line-height: convert.to-rem(20px) !important;
|
|
40
41
|
font-weight: 700 !important;
|
|
41
42
|
|
|
43
|
+
&:link,
|
|
44
|
+
&:visited,
|
|
45
|
+
&:active {
|
|
46
|
+
color: inherit;
|
|
47
|
+
text-decoration: none !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
&:last-child {
|
|
43
51
|
margin-right: 0;
|
|
44
52
|
}
|
|
@@ -603,6 +603,78 @@ describe("rendering AnchorNavigation", () => {
|
|
|
603
603
|
section.remove();
|
|
604
604
|
});
|
|
605
605
|
|
|
606
|
+
it("prefers document scroll-padding-top over internal sticky offset", () => {
|
|
607
|
+
const section = document.createElement("section");
|
|
608
|
+
section.id = "pricing";
|
|
609
|
+
Object.defineProperty(section, "getBoundingClientRect", {
|
|
610
|
+
configurable: true,
|
|
611
|
+
value: () => ({
|
|
612
|
+
top: 600 - window.scrollY,
|
|
613
|
+
bottom: 700 - window.scrollY,
|
|
614
|
+
left: 0,
|
|
615
|
+
right: 0,
|
|
616
|
+
width: 0,
|
|
617
|
+
height: 100,
|
|
618
|
+
x: 0,
|
|
619
|
+
y: 600,
|
|
620
|
+
toJSON: () => ({}),
|
|
621
|
+
}),
|
|
622
|
+
});
|
|
623
|
+
document.body.appendChild(section);
|
|
624
|
+
|
|
625
|
+
const megamenu = document.createElement("div");
|
|
626
|
+
megamenu.setAttribute("data-megamenu", "");
|
|
627
|
+
Object.defineProperty(megamenu, "offsetHeight", {
|
|
628
|
+
configurable: true,
|
|
629
|
+
value: 120,
|
|
630
|
+
});
|
|
631
|
+
document.body.appendChild(megamenu);
|
|
632
|
+
|
|
633
|
+
Object.defineProperty(window, "scrollY", {
|
|
634
|
+
configurable: true,
|
|
635
|
+
value: 0,
|
|
636
|
+
writable: true,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const previousScrollPaddingTop =
|
|
640
|
+
document.documentElement.style.scrollPaddingTop;
|
|
641
|
+
document.documentElement.style.scrollPaddingTop = "210px";
|
|
642
|
+
|
|
643
|
+
const scrollToSpy = vi
|
|
644
|
+
.spyOn(window, "scrollTo")
|
|
645
|
+
.mockImplementation((options) => {
|
|
646
|
+
if (typeof options === "object" && typeof options.top === "number") {
|
|
647
|
+
window.scrollY = options.top;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const { container } = render(<AnchorNavigation items={basicItems} />);
|
|
652
|
+
const anchorNavigationElement = initializeAnchorNavigation(container);
|
|
653
|
+
const anchorNavigation = AnchorNavigationStatic.getInstance(
|
|
654
|
+
anchorNavigationElement,
|
|
655
|
+
);
|
|
656
|
+
const pricingLink = container.querySelector('a[href="#pricing"]');
|
|
657
|
+
|
|
658
|
+
Object.defineProperty(anchorNavigationElement, "offsetHeight", {
|
|
659
|
+
configurable: true,
|
|
660
|
+
value: 50,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
fireEvent.click(pricingLink);
|
|
664
|
+
|
|
665
|
+
expect(scrollToSpy).toHaveBeenNthCalledWith(1, {
|
|
666
|
+
top: 390,
|
|
667
|
+
behavior: "smooth",
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
anchorNavigation?.destroy();
|
|
671
|
+
scrollToSpy.mockRestore();
|
|
672
|
+
document.documentElement.style.scrollPaddingTop =
|
|
673
|
+
previousScrollPaddingTop;
|
|
674
|
+
megamenu.remove();
|
|
675
|
+
section.remove();
|
|
676
|
+
});
|
|
677
|
+
|
|
606
678
|
it("realigns anchor after sticky offset changes during smooth scroll", () => {
|
|
607
679
|
vi.useFakeTimers();
|
|
608
680
|
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
width: 100%;
|
|
43
43
|
display: flex;
|
|
44
44
|
padding: space.get("large");
|
|
45
|
-
gap: convert.to-rem(
|
|
45
|
+
gap: convert.to-rem(60px);
|
|
46
46
|
|
|
47
47
|
> div:not(.body-banner__button) {
|
|
48
48
|
flex: 1;
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
@include breakpoint.get("lg", "down") {
|
|
57
57
|
display: flex;
|
|
58
58
|
flex-direction: column;
|
|
59
|
+
gap: convert.to-rem(30px);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
@include breakpoint.get("sm", "down") {
|
|
@@ -82,13 +83,7 @@
|
|
|
82
83
|
@mixin button {
|
|
83
84
|
display: flex;
|
|
84
85
|
align-items: center;
|
|
85
|
-
padding: 0 0 0 space.get("xlarge");
|
|
86
86
|
margin: 0 !important;
|
|
87
|
-
|
|
88
|
-
@include breakpoint.get("lg", "down") {
|
|
89
|
-
padding: 0;
|
|
90
|
-
margin-top: space.get("small") !important;
|
|
91
|
-
}
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
@mixin large {
|
|
@@ -113,9 +108,4 @@
|
|
|
113
108
|
display: block;
|
|
114
109
|
}
|
|
115
110
|
}
|
|
116
|
-
|
|
117
|
-
.body-banner__button {
|
|
118
|
-
padding: 0;
|
|
119
|
-
margin-top: space.get("small") !important;
|
|
120
|
-
}
|
|
121
111
|
}
|
|
@@ -54,12 +54,14 @@ export const defaultConfig: SwiperOptions = {
|
|
|
54
54
|
hide: false,
|
|
55
55
|
},
|
|
56
56
|
slidesPerView: 1.2,
|
|
57
|
+
spaceBetween: 20,
|
|
57
58
|
mousewheel: {
|
|
58
59
|
forceToAxis: true,
|
|
59
60
|
sensitivity: 1,
|
|
60
61
|
},
|
|
61
62
|
a11y: {
|
|
62
63
|
enabled: true,
|
|
64
|
+
scrollOnFocus: false,
|
|
63
65
|
prevSlideMessage: "Predchádzajúci snímok",
|
|
64
66
|
nextSlideMessage: "Nasledujúci snímok",
|
|
65
67
|
paginationBulletMessage: "Prejsť na snímok {index}",
|
|
@@ -96,6 +98,76 @@ export default class Carousel {
|
|
|
96
98
|
private resizeRafId?: number;
|
|
97
99
|
private boundWindowResizeHandler: () => void;
|
|
98
100
|
private bleedResizeHandler?: () => void;
|
|
101
|
+
private static readonly OVERFLOW_EPSILON_PX = 1;
|
|
102
|
+
|
|
103
|
+
private getViewportWrapper(): HTMLElement | null {
|
|
104
|
+
return this.element.querySelector(
|
|
105
|
+
`.${CLASS_VIEWPORT_WRAPPER}`,
|
|
106
|
+
) as HTMLElement | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private getSlidesPerView(): number {
|
|
110
|
+
return Number(this.instance?.params?.slidesPerView) || 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private setScrollbarVisible(visible: boolean): void {
|
|
114
|
+
const scrollbarEl = this.element.querySelector(
|
|
115
|
+
SELECTOR_SCROLLBAR,
|
|
116
|
+
) as HTMLElement | null;
|
|
117
|
+
if (scrollbarEl) {
|
|
118
|
+
scrollbarEl.style.display = visible ? "" : "none";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private getSlidesContentWidth(): number {
|
|
123
|
+
if (!this.instance) {
|
|
124
|
+
return this.track?.scrollWidth || 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const slides = Array.from(this.instance.slides) as HTMLElement[];
|
|
128
|
+
const widthsSum = slides.reduce((sum, slide) => sum + slide.offsetWidth, 0);
|
|
129
|
+
const spaceBetween = Number(this.instance.params?.spaceBetween) || 0;
|
|
130
|
+
const gaps = Math.max(slides.length - 1, 0) * spaceBetween;
|
|
131
|
+
const measuredWidth = widthsSum + gaps;
|
|
132
|
+
|
|
133
|
+
// Fallback for early lifecycle/test DOM where slides can report 0 width.
|
|
134
|
+
if (widthsSum <= 0 || measuredWidth <= 0) {
|
|
135
|
+
return this.track?.scrollWidth || 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return measuredWidth;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private getOverflowWidth(viewportWrapper?: HTMLElement): number {
|
|
142
|
+
const isBleedRight = this.element.classList.contains(CLASS_BLEED_RIGHT);
|
|
143
|
+
const containerWidth = isBleedRight
|
|
144
|
+
? viewportWrapper?.clientWidth || 0
|
|
145
|
+
: this.viewport?.clientWidth || 0;
|
|
146
|
+
|
|
147
|
+
if (containerWidth <= 0) {
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const contentWidth = this.getSlidesContentWidth();
|
|
152
|
+
|
|
153
|
+
return contentWidth - containerWidth;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private hasScrollableContent(viewportWrapper?: HTMLElement): boolean {
|
|
157
|
+
if (!this.instance || !this.instance.params) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const overflowWidth = this.getOverflowWidth(viewportWrapper);
|
|
162
|
+
if (overflowWidth !== 0) {
|
|
163
|
+
return overflowWidth > Carousel.OVERFLOW_EPSILON_PX;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback for hidden/zero-width initialization.
|
|
167
|
+
const slidesCount = this.instance.slides.length;
|
|
168
|
+
const slidesPerView = this.getSlidesPerView();
|
|
169
|
+
return slidesCount > slidesPerView;
|
|
170
|
+
}
|
|
99
171
|
|
|
100
172
|
constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
|
|
101
173
|
this.element = element;
|
|
@@ -130,8 +202,12 @@ export default class Carousel {
|
|
|
130
202
|
this.getCustomOptions();
|
|
131
203
|
}
|
|
132
204
|
|
|
205
|
+
const isBleedRight = this.element.classList.contains(CLASS_BLEED_RIGHT);
|
|
206
|
+
|
|
133
207
|
this.instance = new Swiper(this.viewport, {
|
|
134
208
|
...this.config,
|
|
209
|
+
// Swiper watchOverflow can mis-detect with slides offsets; manage bleed-right overflow ourselves.
|
|
210
|
+
watchOverflow: isBleedRight ? false : this.config.watchOverflow,
|
|
135
211
|
enabled: false,
|
|
136
212
|
modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard, Mousewheel],
|
|
137
213
|
on: {
|
|
@@ -236,9 +312,7 @@ export default class Carousel {
|
|
|
236
312
|
*/
|
|
237
313
|
fixBleedRightScrollbar() {
|
|
238
314
|
const updateScrollbar = () => {
|
|
239
|
-
const viewportWrapper = this.
|
|
240
|
-
`.${CLASS_VIEWPORT_WRAPPER}`,
|
|
241
|
-
) as HTMLElement;
|
|
315
|
+
const viewportWrapper = this.getViewportWrapper();
|
|
242
316
|
const scrollbar = this.instance?.scrollbar;
|
|
243
317
|
const swiper = this.instance;
|
|
244
318
|
|
|
@@ -246,26 +320,18 @@ export default class Carousel {
|
|
|
246
320
|
return;
|
|
247
321
|
}
|
|
248
322
|
|
|
249
|
-
const
|
|
250
|
-
const maxTranslate = swiper.maxTranslate();
|
|
251
|
-
const translateRange = Math.abs(maxTranslate - minTranslate);
|
|
323
|
+
const hasScrollableContent = this.hasScrollableContent(viewportWrapper);
|
|
252
324
|
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
scrollbar.el.style.display = "none";
|
|
256
|
-
}
|
|
325
|
+
if (!hasScrollableContent) {
|
|
326
|
+
this.setScrollbarVisible(false);
|
|
257
327
|
return;
|
|
258
328
|
}
|
|
259
329
|
|
|
260
|
-
|
|
261
|
-
scrollbar.el.style.display = "";
|
|
262
|
-
}
|
|
330
|
+
this.setScrollbarVisible(true);
|
|
263
331
|
|
|
264
332
|
const scrollbarWidth = scrollbar.el.offsetWidth;
|
|
265
333
|
const viewportWidth = viewportWrapper.clientWidth;
|
|
266
|
-
const
|
|
267
|
-
const offsetAfter = Number(swiper.params.slidesOffsetAfter) || 0;
|
|
268
|
-
const contentWidth = this.track.scrollWidth + offsetBefore + offsetAfter;
|
|
334
|
+
const contentWidth = this.getSlidesContentWidth();
|
|
269
335
|
|
|
270
336
|
const visibleRatio =
|
|
271
337
|
contentWidth > 0 ? Math.min(viewportWidth / contentWidth, 1) : 1;
|
|
@@ -347,40 +413,18 @@ export default class Carousel {
|
|
|
347
413
|
private updateCarouselEnabledState(): void {
|
|
348
414
|
if (!this.instance || !this.instance.params) return;
|
|
349
415
|
|
|
350
|
-
const
|
|
351
|
-
const
|
|
416
|
+
const viewportWrapper = this.getViewportWrapper();
|
|
417
|
+
const hasScrollableContent = this.hasScrollableContent(
|
|
418
|
+
viewportWrapper || undefined,
|
|
419
|
+
);
|
|
352
420
|
|
|
353
|
-
if (
|
|
421
|
+
if (hasScrollableContent) {
|
|
354
422
|
this.instance.enable();
|
|
355
423
|
} else {
|
|
356
424
|
this.instance.disable();
|
|
357
425
|
}
|
|
358
426
|
|
|
359
|
-
|
|
360
|
-
SELECTOR_SCROLLBAR,
|
|
361
|
-
) as HTMLElement | null;
|
|
362
|
-
if (scrollbarEl) {
|
|
363
|
-
scrollbarEl.style.display = this.instance.enabled ? "" : "none";
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
this.applyTrackMarginCompensation();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Apply negative margin-right to carousel track when all slides fit on screen.
|
|
371
|
-
* This compensates for the 20px padding-right on each slide, ensuring proper grid alignment.
|
|
372
|
-
*/
|
|
373
|
-
private applyTrackMarginCompensation(): void {
|
|
374
|
-
if (!this.instance || !this.instance.params || !this.viewport) return;
|
|
375
|
-
|
|
376
|
-
const slidesPerView = Number(this.instance.params.slidesPerView) || 1;
|
|
377
|
-
const slidesCount = this.instance.slides.length;
|
|
378
|
-
|
|
379
|
-
if (slidesCount <= slidesPerView) {
|
|
380
|
-
this.viewport.style.setProperty("margin-right", "-20px", "important");
|
|
381
|
-
} else {
|
|
382
|
-
this.viewport.style.removeProperty("margin-right");
|
|
383
|
-
}
|
|
427
|
+
this.setScrollbarVisible(this.instance.enabled);
|
|
384
428
|
}
|
|
385
429
|
|
|
386
430
|
private applyBleedInsets(viewportWrapper: HTMLElement): void {
|
|
@@ -388,42 +432,34 @@ export default class Carousel {
|
|
|
388
432
|
return;
|
|
389
433
|
}
|
|
390
434
|
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const rect = viewportWrapper.getBoundingClientRect();
|
|
397
|
-
const insetLeft = Math.max(Math.round(rect.left), 0);
|
|
398
|
-
const trailingSlidePadding = 20;
|
|
399
|
-
const insetAfter = Math.max(insetLeft - trailingSlidePadding, 0);
|
|
400
|
-
|
|
401
|
-
const slidesPerView = Number(this.instance.params.slidesPerView) || 1;
|
|
435
|
+
const projectedContentWidth = this.getSlidesContentWidth();
|
|
436
|
+
const projectedOverflowWidth =
|
|
437
|
+
projectedContentWidth - viewportWrapper.clientWidth;
|
|
438
|
+
const slidesPerView = this.getSlidesPerView();
|
|
402
439
|
const slidesCount = this.instance.slides.length;
|
|
403
|
-
const
|
|
404
|
-
|
|
440
|
+
const hasIntrinsicOverflow =
|
|
441
|
+
projectedContentWidth > 0
|
|
442
|
+
? projectedOverflowWidth > Carousel.OVERFLOW_EPSILON_PX
|
|
443
|
+
: slidesCount > slidesPerView;
|
|
444
|
+
const shouldDisableBleed = !hasIntrinsicOverflow;
|
|
405
445
|
|
|
406
446
|
this.element.classList.toggle(CLASS_BLEED_RIGHT, !shouldDisableBleed);
|
|
407
447
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
this.instance.params.slidesOffsetBefore = insetLeft;
|
|
415
|
-
this.instance.params.slidesOffsetAfter = insetAfter;
|
|
416
|
-
this.element.style.setProperty(
|
|
417
|
-
"--carousel-bleed-viewport-width",
|
|
418
|
-
"100dvw",
|
|
419
|
-
);
|
|
420
|
-
this.element.style.setProperty(
|
|
421
|
-
"--carousel-bleed-margin-left",
|
|
422
|
-
"calc(50% - 50dvw)",
|
|
423
|
-
);
|
|
448
|
+
const baseWidth = shouldDisableBleed
|
|
449
|
+
? undefined
|
|
450
|
+
: viewportWrapper.clientWidth;
|
|
451
|
+
this.instance.params.width = baseWidth;
|
|
452
|
+
if (this.instance.originalParams) {
|
|
453
|
+
this.instance.originalParams.width = baseWidth;
|
|
424
454
|
}
|
|
425
455
|
|
|
456
|
+
this.instance.params.slidesOffsetBefore = 0;
|
|
457
|
+
this.instance.params.slidesOffsetAfter = 0;
|
|
458
|
+
this.element.style.setProperty("--carousel-bleed-viewport-width", "100%");
|
|
459
|
+
this.element.style.setProperty("--carousel-bleed-margin-left", "0px");
|
|
460
|
+
|
|
426
461
|
this.instance.update();
|
|
462
|
+
this.updateCarouselEnabledState();
|
|
427
463
|
}
|
|
428
464
|
|
|
429
465
|
/**
|
|
@@ -436,16 +472,11 @@ export default class Carousel {
|
|
|
436
472
|
requestAnimationFrame(() => {
|
|
437
473
|
if (!this.instance) return;
|
|
438
474
|
|
|
439
|
-
const viewportWrapper = this.
|
|
440
|
-
`.${CLASS_VIEWPORT_WRAPPER}`,
|
|
441
|
-
) as HTMLElement;
|
|
475
|
+
const viewportWrapper = this.getViewportWrapper();
|
|
442
476
|
if (!viewportWrapper) return;
|
|
443
477
|
|
|
444
478
|
const updateBleedState = () => {
|
|
445
479
|
this.applyBleedInsets(viewportWrapper);
|
|
446
|
-
|
|
447
|
-
// Update margin compensation on resize
|
|
448
|
-
this.applyTrackMarginCompensation();
|
|
449
480
|
};
|
|
450
481
|
|
|
451
482
|
this.bleedResizeHandler = updateBleedState;
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
flex-direction: column;
|
|
106
106
|
max-width: 100%;
|
|
107
107
|
flex: 0 0 auto;
|
|
108
|
-
padding: 0
|
|
108
|
+
padding: 0;
|
|
109
109
|
user-select: none;
|
|
110
110
|
|
|
111
111
|
> * {
|
|
@@ -225,6 +225,6 @@
|
|
|
225
225
|
max-width: var(--carousel-bleed-viewport-width, 100%);
|
|
226
226
|
margin-right: 0;
|
|
227
227
|
margin-left: var(--carousel-bleed-margin-left, 0) !important;
|
|
228
|
-
overflow:
|
|
228
|
+
overflow: visible !important;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
@@ -124,6 +124,12 @@ describe("Carousel Static - External Controls", () => {
|
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
describe("External Controls Initialization", () => {
|
|
127
|
+
it("should disable a11y scrollOnFocus to prevent focus-triggered slide jumps", () => {
|
|
128
|
+
const swiperConfig = Swiper.mock.calls[0]?.[1];
|
|
129
|
+
|
|
130
|
+
expect(swiperConfig?.a11y?.scrollOnFocus).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
127
133
|
it("should find and initialize external controls", () => {
|
|
128
134
|
const prevButton = document.getElementById("prev-btn");
|
|
129
135
|
const nextButton = document.getElementById("next-btn");
|
|
@@ -644,5 +650,202 @@ describe("Carousel Static - Auto-Disable Feature", () => {
|
|
|
644
650
|
carouselInstance.updateCarouselEnabledState();
|
|
645
651
|
}).not.toThrow();
|
|
646
652
|
});
|
|
653
|
+
|
|
654
|
+
it("should not apply negative viewport margin on mobile when slides fit", () => {
|
|
655
|
+
const originalInnerWidth = window.innerWidth;
|
|
656
|
+
Object.defineProperty(window, "innerWidth", {
|
|
657
|
+
configurable: true,
|
|
658
|
+
writable: true,
|
|
659
|
+
value: 375,
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
mockSwiperInstance.slides = [document.createElement("div")];
|
|
663
|
+
mockSwiperInstance.params.slidesPerView = 1;
|
|
664
|
+
|
|
665
|
+
carouselInstance = new Carousel(carouselElement);
|
|
666
|
+
|
|
667
|
+
const viewport = carouselElement.querySelector(".carousel__viewport");
|
|
668
|
+
expect(viewport.style.getPropertyValue("margin-right")).toBe("");
|
|
669
|
+
|
|
670
|
+
Object.defineProperty(window, "innerWidth", {
|
|
671
|
+
configurable: true,
|
|
672
|
+
writable: true,
|
|
673
|
+
value: originalInnerWidth,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("should not apply negative viewport margin on desktop when slides fit", () => {
|
|
678
|
+
const originalInnerWidth = window.innerWidth;
|
|
679
|
+
Object.defineProperty(window, "innerWidth", {
|
|
680
|
+
configurable: true,
|
|
681
|
+
writable: true,
|
|
682
|
+
value: 1240,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
mockSwiperInstance.slides = [document.createElement("div")];
|
|
686
|
+
mockSwiperInstance.params.slidesPerView = 1;
|
|
687
|
+
|
|
688
|
+
carouselInstance = new Carousel(carouselElement);
|
|
689
|
+
|
|
690
|
+
const viewport = carouselElement.querySelector(".carousel__viewport");
|
|
691
|
+
expect(viewport.style.getPropertyValue("margin-right")).toBe("");
|
|
692
|
+
|
|
693
|
+
Object.defineProperty(window, "innerWidth", {
|
|
694
|
+
configurable: true,
|
|
695
|
+
writable: true,
|
|
696
|
+
value: originalInnerWidth,
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("should disable bleed-right when slides fit wrapper despite right inset", () => {
|
|
701
|
+
const originalInnerWidth = window.innerWidth;
|
|
702
|
+
Object.defineProperty(window, "innerWidth", {
|
|
703
|
+
configurable: true,
|
|
704
|
+
writable: true,
|
|
705
|
+
value: 1200,
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
mockSwiperInstance.slides = [
|
|
709
|
+
document.createElement("div"),
|
|
710
|
+
document.createElement("div"),
|
|
711
|
+
];
|
|
712
|
+
mockSwiperInstance.params.slidesPerView = 2;
|
|
713
|
+
|
|
714
|
+
document.body.innerHTML = `
|
|
715
|
+
<div class="carousel carousel--bleed-right" data-carousel-id="test-carousel" id="test-carousel">
|
|
716
|
+
<div class="carousel__viewport-wrapper">
|
|
717
|
+
<div class="carousel__viewport">
|
|
718
|
+
<div class="carousel__track">
|
|
719
|
+
<div class="carousel__slide">Slide 1</div>
|
|
720
|
+
<div class="carousel__slide">Slide 2</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
<div class="carousel__pagination"></div>
|
|
725
|
+
</div>
|
|
726
|
+
`;
|
|
727
|
+
|
|
728
|
+
carouselElement = document.querySelector(".carousel");
|
|
729
|
+
const viewportWrapper = carouselElement.querySelector(
|
|
730
|
+
".carousel__viewport-wrapper",
|
|
731
|
+
);
|
|
732
|
+
const track = carouselElement.querySelector(".carousel__track");
|
|
733
|
+
|
|
734
|
+
Object.defineProperty(viewportWrapper, "clientWidth", {
|
|
735
|
+
configurable: true,
|
|
736
|
+
value: 900,
|
|
737
|
+
});
|
|
738
|
+
Object.defineProperty(track, "scrollWidth", {
|
|
739
|
+
configurable: true,
|
|
740
|
+
value: 900,
|
|
741
|
+
});
|
|
742
|
+
Object.defineProperty(viewportWrapper, "getBoundingClientRect", {
|
|
743
|
+
configurable: true,
|
|
744
|
+
value: () => ({
|
|
745
|
+
top: 0,
|
|
746
|
+
right: 1000,
|
|
747
|
+
bottom: 200,
|
|
748
|
+
left: 100,
|
|
749
|
+
width: 900,
|
|
750
|
+
height: 200,
|
|
751
|
+
x: 100,
|
|
752
|
+
y: 0,
|
|
753
|
+
toJSON: () => ({}),
|
|
754
|
+
}),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
carouselInstance = new Carousel(carouselElement);
|
|
758
|
+
|
|
759
|
+
expect(mockSwiperInstance.params.slidesOffsetBefore).toBe(0);
|
|
760
|
+
expect(mockSwiperInstance.params.slidesOffsetAfter).toBe(0);
|
|
761
|
+
|
|
762
|
+
Object.defineProperty(window, "innerWidth", {
|
|
763
|
+
configurable: true,
|
|
764
|
+
writable: true,
|
|
765
|
+
value: originalInnerWidth,
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("should keep wrapper-based bleed geometry without offsets", () => {
|
|
770
|
+
const originalInnerWidth = window.innerWidth;
|
|
771
|
+
Object.defineProperty(window, "innerWidth", {
|
|
772
|
+
configurable: true,
|
|
773
|
+
writable: true,
|
|
774
|
+
value: 1000,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
mockSwiperInstance.slides = [
|
|
778
|
+
document.createElement("div"),
|
|
779
|
+
document.createElement("div"),
|
|
780
|
+
document.createElement("div"),
|
|
781
|
+
];
|
|
782
|
+
mockSwiperInstance.params.slidesPerView = 1.2;
|
|
783
|
+
mockSwiperInstance.params.spaceBetween = 20;
|
|
784
|
+
|
|
785
|
+
document.body.innerHTML = `
|
|
786
|
+
<div class="carousel carousel--bleed-right" data-carousel-id="test-carousel" id="test-carousel">
|
|
787
|
+
<div class="carousel__viewport-wrapper">
|
|
788
|
+
<div class="carousel__viewport">
|
|
789
|
+
<div class="carousel__track">
|
|
790
|
+
<div class="carousel__slide">Slide 1</div>
|
|
791
|
+
<div class="carousel__slide">Slide 2</div>
|
|
792
|
+
<div class="carousel__slide">Slide 3</div>
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
<div class="carousel__pagination"></div>
|
|
797
|
+
</div>
|
|
798
|
+
`;
|
|
799
|
+
|
|
800
|
+
carouselElement = document.querySelector(".carousel");
|
|
801
|
+
const viewportWrapper = carouselElement.querySelector(
|
|
802
|
+
".carousel__viewport-wrapper",
|
|
803
|
+
);
|
|
804
|
+
const track = carouselElement.querySelector(".carousel__track");
|
|
805
|
+
|
|
806
|
+
Object.defineProperty(viewportWrapper, "clientWidth", {
|
|
807
|
+
configurable: true,
|
|
808
|
+
value: 800,
|
|
809
|
+
});
|
|
810
|
+
Object.defineProperty(track, "scrollWidth", {
|
|
811
|
+
configurable: true,
|
|
812
|
+
value: 1200,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
Object.defineProperty(viewportWrapper, "getBoundingClientRect", {
|
|
816
|
+
configurable: true,
|
|
817
|
+
value: () => ({
|
|
818
|
+
top: 0,
|
|
819
|
+
right: 920,
|
|
820
|
+
bottom: 200,
|
|
821
|
+
left: 120,
|
|
822
|
+
width: 800,
|
|
823
|
+
height: 200,
|
|
824
|
+
x: 120,
|
|
825
|
+
y: 0,
|
|
826
|
+
toJSON: () => ({}),
|
|
827
|
+
}),
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
carouselInstance = new Carousel(carouselElement);
|
|
831
|
+
|
|
832
|
+
expect(mockSwiperInstance.params.slidesOffsetBefore).toBe(0);
|
|
833
|
+
expect(mockSwiperInstance.params.slidesOffsetAfter).toBe(0);
|
|
834
|
+
expect(mockSwiperInstance.params.width).toBe(800);
|
|
835
|
+
expect(
|
|
836
|
+
carouselElement.style.getPropertyValue(
|
|
837
|
+
"--carousel-bleed-viewport-width",
|
|
838
|
+
),
|
|
839
|
+
).toBe("100%");
|
|
840
|
+
expect(
|
|
841
|
+
carouselElement.style.getPropertyValue("--carousel-bleed-margin-left"),
|
|
842
|
+
).toBe("0px");
|
|
843
|
+
|
|
844
|
+
Object.defineProperty(window, "innerWidth", {
|
|
845
|
+
configurable: true,
|
|
846
|
+
writable: true,
|
|
847
|
+
value: originalInnerWidth,
|
|
848
|
+
});
|
|
849
|
+
});
|
|
647
850
|
});
|
|
648
851
|
});
|
|
@@ -25,6 +25,8 @@ interface ExpanderProps {
|
|
|
25
25
|
children?: React.ReactNode;
|
|
26
26
|
/** Expander takes full width of its container */
|
|
27
27
|
isFullWidth?: boolean;
|
|
28
|
+
/** Position of trigger relative to content when opened */
|
|
29
|
+
placement?: "top" | "bottom";
|
|
28
30
|
/** Group identifier for syncing multiple expanders together */
|
|
29
31
|
toggleGroup?: string;
|
|
30
32
|
/** Initial open state */
|
|
@@ -40,6 +42,7 @@ export const Expander: React.FC<ExpanderProps> = (props) => {
|
|
|
40
42
|
renderSummary,
|
|
41
43
|
renderSummaryOpened,
|
|
42
44
|
isFullWidth,
|
|
45
|
+
placement = "bottom",
|
|
43
46
|
toggleGroup,
|
|
44
47
|
...other
|
|
45
48
|
} = props;
|
|
@@ -49,6 +52,7 @@ export const Expander: React.FC<ExpanderProps> = (props) => {
|
|
|
49
52
|
CLASS_ROOT,
|
|
50
53
|
{
|
|
51
54
|
[`${CLASS_ROOT}--fullwidth`]: isFullWidth,
|
|
55
|
+
[`${CLASS_ROOT}--placement-top`]: placement === "top",
|
|
52
56
|
},
|
|
53
57
|
className,
|
|
54
58
|
);
|