@orangesk/orange-design-system 3.6.2 → 3.8.0
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/Alert/style.css +1 -1
- package/build/components/Alert/style.css.map +1 -1
- package/build/components/Bar/style.css +1 -1
- package/build/components/Bar/style.css.map +1 -1
- package/build/components/Drawer/style.css +2 -0
- package/build/components/Drawer/style.css.map +1 -0
- 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/Modal/style.css +1 -1
- package/build/components/Modal/style.css.map +1 -1
- package/build/components/StatusIndicator/style.css +2 -0
- package/build/components/StatusIndicator/style.css.map +1 -0
- package/build/components/Table/style.css +1 -1
- package/build/components/Table/style.css.map +1 -1
- package/build/components/index.js +1 -1
- package/build/components/index.js.map +1 -1
- package/build/components/types/index.d.ts +89 -3
- 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.js +1 -1
- package/build/lib/footer.js.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/utilities.css +1 -1
- package/build/lib/utilities.css.map +1 -1
- package/package.json +29 -29
- package/src/components/Alert/Alert.tsx +2 -2
- package/src/components/Alert/data.ts +8 -5
- package/src/components/Alert/styles/config.scss +4 -4
- package/src/components/Alert/styles/mixins.scss +8 -1
- package/src/components/Alert/tests/Alert.test.jsx +11 -1
- package/src/components/Bar/Bar.tsx +1 -1
- package/src/components/Bar/styles/config.scss +1 -0
- package/src/components/Bar/tests/Bar.unit.test.jsx +1 -1
- package/src/components/Carousel/Carousel.static.ts +103 -12
- package/src/components/Carousel/tests/Carousel.static.test.jsx +134 -0
- package/src/components/DocumentationSidebar/DocumentationSidebar.tsx +12 -4
- package/src/components/Drawer/Drawer.static.ts +281 -0
- package/src/components/Drawer/Drawer.tsx +82 -0
- package/src/components/Drawer/DrawerBody.tsx +21 -0
- package/src/components/Drawer/DrawerFooter.tsx +21 -0
- package/src/components/Drawer/DrawerHeader.tsx +21 -0
- package/src/components/Drawer/index.ts +5 -0
- package/src/components/Drawer/styles/config.scss +17 -0
- package/src/components/Drawer/styles/mixins.scss +119 -0
- package/src/components/Drawer/styles/style.scss +71 -0
- package/src/components/Drawer/tests/Drawer.unit.test.jsx +149 -0
- package/src/components/Footer/data.ts +4 -0
- package/src/components/Forms/SegmentedControl/SegmentedControl.tsx +122 -0
- package/src/components/Forms/SegmentedControl/index.ts +5 -0
- package/src/components/Forms/SegmentedControl/styles/mixins.scss +52 -0
- package/src/components/Forms/SegmentedControl/styles/style.scss +35 -0
- package/src/components/Forms/SegmentedControl/tests/SegmentedControl.conformance.test.jsx +36 -0
- package/src/components/Forms/SegmentedControl/tests/SegmentedControl.unit.test.jsx +125 -0
- package/src/components/Forms/SelectionTile/PLAN.md +206 -0
- package/src/components/Forms/SelectionTile/SelectionTile.tsx +145 -0
- package/src/components/Forms/SelectionTile/index.ts +5 -0
- package/src/components/Forms/SelectionTile/styles/mixins.scss +83 -0
- package/src/components/Forms/SelectionTile/styles/style.scss +58 -0
- package/src/components/Forms/SelectionTile/tests/SelectionTile.conformance.test.jsx +32 -0
- package/src/components/Forms/SelectionTile/tests/SelectionTile.unit.test.jsx +146 -0
- package/src/components/Forms/index.ts +2 -0
- package/src/components/Icon/index.ts +1 -0
- package/src/components/Megamenu/Megamenu.tsx +20 -16
- package/src/components/Megamenu/MegamenuBlog.tsx +10 -7
- package/src/components/Megamenu/MyOrangeMobilePanel.tsx +2 -3
- package/src/components/Megamenu/constants.ts +3 -1
- package/src/components/Megamenu/styles/mixins.scss +53 -18
- package/src/components/Megamenu/styles/style.scss +12 -4
- package/src/components/Megamenu/tests/Megamenu.unit.test.jsx +32 -0
- package/src/components/Modal/styles/config.scss +3 -2
- package/src/components/StatusIndicator/StatusIndicator.tsx +29 -0
- package/src/components/StatusIndicator/index.ts +2 -0
- package/src/components/StatusIndicator/styles/config.scss +12 -0
- package/src/components/StatusIndicator/styles/mixins.scss +36 -0
- package/src/components/StatusIndicator/styles/style.scss +16 -0
- package/src/components/StatusIndicator/tests/StatusIndicator.conformance.test.jsx +22 -0
- package/src/components/StatusIndicator/tests/StatusIndicator.unit.test.jsx +70 -0
- package/src/components/Table/Table.static.ts +23 -10
- package/src/components/Table/Table.tsx +7 -2
- package/src/components/Table/styles/mixins.scss +59 -0
- package/src/components/Table/styles/style.scss +25 -0
- package/src/components/Table/tests/Table.static.unit.test.jsx +72 -0
- package/src/components/Table/tests/Table.unit.test.jsx +33 -0
- package/src/components/index.ts +13 -0
- package/src/styles/tokens/overlay.scss +3 -0
- package/src/styles/typography/style.scss +0 -13
- package/src/styles/utilities/border.scss +4 -0
- package/src/styles/utilities/border.styles.unit.test.jsx +9 -0
- package/src/styles/utilities/text.scss +12 -0
- package/src/components/Carousel/CHANGELOG.md +0 -11
|
@@ -241,6 +241,9 @@ export default class Carousel {
|
|
|
241
241
|
modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard, Mousewheel],
|
|
242
242
|
on: {
|
|
243
243
|
slideChange: this.handleSlideChange,
|
|
244
|
+
snapIndexChange: () => {
|
|
245
|
+
this.updateExternalControlsState();
|
|
246
|
+
},
|
|
244
247
|
slideChangeTransitionEnd: () => {
|
|
245
248
|
this.updateExternalControlsState();
|
|
246
249
|
},
|
|
@@ -680,6 +683,9 @@ export default class Carousel {
|
|
|
680
683
|
}
|
|
681
684
|
|
|
682
685
|
this.carouselId = carouselId;
|
|
686
|
+
if (!this.element.id) {
|
|
687
|
+
this.element.id = carouselId;
|
|
688
|
+
}
|
|
683
689
|
|
|
684
690
|
const controlElements = document.querySelectorAll(
|
|
685
691
|
`[data-carousel-controls="${carouselId}"]`,
|
|
@@ -698,8 +704,22 @@ export default class Carousel {
|
|
|
698
704
|
(htmlControl as any)._carouselClickHandler,
|
|
699
705
|
);
|
|
700
706
|
}
|
|
707
|
+
if ((htmlControl as any)._carouselKeydownHandler) {
|
|
708
|
+
htmlControl.removeEventListener(
|
|
709
|
+
"keydown",
|
|
710
|
+
(htmlControl as any)._carouselKeydownHandler,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
701
713
|
|
|
702
714
|
const action = htmlControl.dataset.carouselAction;
|
|
715
|
+
const slideIndexValue = htmlControl.dataset.carouselSlideIndex;
|
|
716
|
+
const slideIndex =
|
|
717
|
+
slideIndexValue !== undefined ? Number(slideIndexValue) : undefined;
|
|
718
|
+
const hasValidSlideIndex =
|
|
719
|
+
!action &&
|
|
720
|
+
slideIndex !== undefined &&
|
|
721
|
+
Number.isInteger(slideIndex) &&
|
|
722
|
+
slideIndex >= 0;
|
|
703
723
|
|
|
704
724
|
const clickHandler = (e: Event) => {
|
|
705
725
|
e.preventDefault();
|
|
@@ -710,23 +730,51 @@ export default class Carousel {
|
|
|
710
730
|
this.slideNext();
|
|
711
731
|
} else if (action === "prev") {
|
|
712
732
|
this.slidePrev();
|
|
733
|
+
} else if (hasValidSlideIndex) {
|
|
734
|
+
this.goToSlide(slideIndex);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const keydownHandler = (e: KeyboardEvent) => {
|
|
739
|
+
if (e.key !== "Enter" && e.key !== " ") {
|
|
740
|
+
return;
|
|
713
741
|
}
|
|
742
|
+
|
|
743
|
+
clickHandler(e);
|
|
714
744
|
};
|
|
715
745
|
|
|
716
|
-
if (action === "next" || action === "prev") {
|
|
746
|
+
if (action === "next" || action === "prev" || hasValidSlideIndex) {
|
|
717
747
|
htmlControl.addEventListener("click", clickHandler);
|
|
718
748
|
|
|
719
|
-
htmlControl.setAttribute(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
? this.config.a11y
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
749
|
+
htmlControl.setAttribute("aria-controls", this.element.id);
|
|
750
|
+
|
|
751
|
+
if (action === "next" || action === "prev") {
|
|
752
|
+
if (!htmlControl.hasAttribute("aria-label")) {
|
|
753
|
+
htmlControl.setAttribute(
|
|
754
|
+
"aria-label",
|
|
755
|
+
action === "next"
|
|
756
|
+
? (typeof this.config.a11y === "object"
|
|
757
|
+
? this.config.a11y?.nextSlideMessage
|
|
758
|
+
: undefined) || "Nasledujúci snímok"
|
|
759
|
+
: (typeof this.config.a11y === "object"
|
|
760
|
+
? this.config.a11y?.prevSlideMessage
|
|
761
|
+
: undefined) || "Predchádzajúci snímok",
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (htmlControl instanceof HTMLButtonElement) {
|
|
767
|
+
htmlControl.setAttribute("type", "button");
|
|
768
|
+
} else {
|
|
769
|
+
if (!htmlControl.hasAttribute("role")) {
|
|
770
|
+
htmlControl.setAttribute("role", "button");
|
|
771
|
+
}
|
|
772
|
+
if (!htmlControl.hasAttribute("tabindex")) {
|
|
773
|
+
htmlControl.setAttribute("tabindex", "0");
|
|
774
|
+
}
|
|
775
|
+
htmlControl.addEventListener("keydown", keydownHandler);
|
|
776
|
+
(htmlControl as any)._carouselKeydownHandler = keydownHandler;
|
|
777
|
+
}
|
|
730
778
|
|
|
731
779
|
(htmlControl as any)._carouselClickHandler = clickHandler;
|
|
732
780
|
}
|
|
@@ -773,6 +821,9 @@ export default class Carousel {
|
|
|
773
821
|
const nextControls = document.querySelectorAll(
|
|
774
822
|
`[data-carousel-controls="${this.carouselId}"][data-carousel-action="next"]`,
|
|
775
823
|
);
|
|
824
|
+
const slideControls = document.querySelectorAll(
|
|
825
|
+
`[data-carousel-controls="${this.carouselId}"][data-carousel-slide-index]:not([data-carousel-action])`,
|
|
826
|
+
);
|
|
776
827
|
|
|
777
828
|
prevControls.forEach((control) => {
|
|
778
829
|
const htmlControl = control as HTMLElement;
|
|
@@ -799,6 +850,25 @@ export default class Carousel {
|
|
|
799
850
|
htmlControl.style.cursor = "pointer";
|
|
800
851
|
}
|
|
801
852
|
});
|
|
853
|
+
|
|
854
|
+
const activeIndex = this.instance.params.loop
|
|
855
|
+
? (this.instance.realIndex ?? this.instance.activeIndex ?? 0)
|
|
856
|
+
: (this.instance.snapIndex ?? this.instance.activeIndex ?? 0);
|
|
857
|
+
slideControls.forEach((control) => {
|
|
858
|
+
const htmlControl = control as HTMLElement;
|
|
859
|
+
const slideIndex = Number(htmlControl.dataset.carouselSlideIndex);
|
|
860
|
+
if (
|
|
861
|
+
!Number.isInteger(slideIndex) ||
|
|
862
|
+
slideIndex < 0 ||
|
|
863
|
+
slideIndex >= this.instance.slides.length
|
|
864
|
+
) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const isActive = slideIndex === activeIndex;
|
|
868
|
+
|
|
869
|
+
htmlControl.classList.toggle(CLASS_ACTIVE, isActive);
|
|
870
|
+
htmlControl.setAttribute("aria-pressed", String(isActive));
|
|
871
|
+
});
|
|
802
872
|
}
|
|
803
873
|
|
|
804
874
|
/**
|
|
@@ -819,6 +889,27 @@ export default class Carousel {
|
|
|
819
889
|
}
|
|
820
890
|
}
|
|
821
891
|
|
|
892
|
+
/**
|
|
893
|
+
* Navigate to a specific slide index from an external control.
|
|
894
|
+
*/
|
|
895
|
+
goToSlide(index: number) {
|
|
896
|
+
if (
|
|
897
|
+
!this.instance ||
|
|
898
|
+
!Number.isInteger(index) ||
|
|
899
|
+
index < 0 ||
|
|
900
|
+
index >= this.instance.slides.length
|
|
901
|
+
) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (this.instance.params.loop && this.instance.slideToLoop) {
|
|
906
|
+
this.instance.slideToLoop(index);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
this.instance.slideTo(index);
|
|
911
|
+
}
|
|
912
|
+
|
|
822
913
|
/**
|
|
823
914
|
* Get the current active slide index.
|
|
824
915
|
*/
|
|
@@ -12,6 +12,7 @@ vi.mock("swiper", () => ({
|
|
|
12
12
|
return {
|
|
13
13
|
slideNext: vi.fn(),
|
|
14
14
|
slidePrev: vi.fn(),
|
|
15
|
+
slideTo: vi.fn(),
|
|
15
16
|
destroy: vi.fn(),
|
|
16
17
|
update: vi.fn(),
|
|
17
18
|
on: vi.fn(),
|
|
@@ -64,6 +65,7 @@ describe("Carousel Static - External Controls", () => {
|
|
|
64
65
|
mockSwiperInstance = {
|
|
65
66
|
slideNext: vi.fn(),
|
|
66
67
|
slidePrev: vi.fn(),
|
|
68
|
+
slideTo: vi.fn(),
|
|
67
69
|
destroy: vi.fn(),
|
|
68
70
|
update: vi.fn(),
|
|
69
71
|
on: vi.fn(),
|
|
@@ -142,6 +144,21 @@ describe("Carousel Static - External Controls", () => {
|
|
|
142
144
|
expect(nextButton.getAttribute("aria-label")).toBe("Nasledujúci snímok");
|
|
143
145
|
expect(prevButton.getAttribute("type")).toBe("button");
|
|
144
146
|
expect(nextButton.getAttribute("type")).toBe("button");
|
|
147
|
+
expect(prevButton.getAttribute("aria-controls")).toBe("test-carousel");
|
|
148
|
+
expect(nextButton.getAttribute("aria-controls")).toBe("test-carousel");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should preserve an explicit accessible label", () => {
|
|
152
|
+
const customNext = document.createElement("button");
|
|
153
|
+
customNext.dataset.carouselControls = "test-carousel";
|
|
154
|
+
customNext.dataset.carouselAction = "next";
|
|
155
|
+
customNext.setAttribute("aria-label", "Ďalší produkt");
|
|
156
|
+
document.body.appendChild(customNext);
|
|
157
|
+
|
|
158
|
+
carouselInstance.initExternalControls();
|
|
159
|
+
|
|
160
|
+
expect(customNext).toHaveAttribute("aria-label", "Ďalší produkt");
|
|
161
|
+
expect(customNext).toHaveAttribute("aria-controls", "test-carousel");
|
|
145
162
|
});
|
|
146
163
|
|
|
147
164
|
it("should not initialize controls without carousel ID", () => {
|
|
@@ -178,6 +195,123 @@ describe("Carousel Static - External Controls", () => {
|
|
|
178
195
|
});
|
|
179
196
|
|
|
180
197
|
describe("External Controls Navigation", () => {
|
|
198
|
+
it("should navigate by slide index and synchronize index controls", () => {
|
|
199
|
+
document.body.insertAdjacentHTML(
|
|
200
|
+
"beforeend",
|
|
201
|
+
`
|
|
202
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="0" id="slide-0">XL</button>
|
|
203
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="1" id="slide-1">M</button>
|
|
204
|
+
`,
|
|
205
|
+
);
|
|
206
|
+
carouselInstance.initExternalControls();
|
|
207
|
+
|
|
208
|
+
const firstControl = document.getElementById("slide-0");
|
|
209
|
+
const secondControl = document.getElementById("slide-1");
|
|
210
|
+
expect(firstControl).toHaveClass("is-active");
|
|
211
|
+
expect(firstControl).toHaveAttribute("aria-pressed", "true");
|
|
212
|
+
expect(secondControl).toHaveAttribute("aria-pressed", "false");
|
|
213
|
+
|
|
214
|
+
secondControl.click();
|
|
215
|
+
expect(mockSwiperInstance.slideTo).toHaveBeenCalledWith(1);
|
|
216
|
+
|
|
217
|
+
mockSwiperInstance.activeIndex = 1;
|
|
218
|
+
const swiperConfig = Swiper.mock.calls[0]?.[1];
|
|
219
|
+
swiperConfig?.on?.slideChange?.();
|
|
220
|
+
|
|
221
|
+
expect(firstControl).not.toHaveClass("is-active");
|
|
222
|
+
expect(firstControl).toHaveAttribute("aria-pressed", "false");
|
|
223
|
+
expect(secondControl).toHaveClass("is-active");
|
|
224
|
+
expect(secondControl).toHaveAttribute("aria-pressed", "true");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should make non-native index controls keyboard accessible", () => {
|
|
228
|
+
document.body.insertAdjacentHTML(
|
|
229
|
+
"beforeend",
|
|
230
|
+
`<span data-carousel-controls="test-carousel" data-carousel-slide-index="1" id="keyboard-slide">M</span>`,
|
|
231
|
+
);
|
|
232
|
+
carouselInstance.initExternalControls();
|
|
233
|
+
|
|
234
|
+
const control = document.getElementById("keyboard-slide");
|
|
235
|
+
expect(control).toHaveAttribute("role", "button");
|
|
236
|
+
expect(control).toHaveAttribute("tabindex", "0");
|
|
237
|
+
expect(control).toHaveAttribute("aria-controls", "test-carousel");
|
|
238
|
+
|
|
239
|
+
control.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
|
240
|
+
control.dispatchEvent(new KeyboardEvent("keydown", { key: " " }));
|
|
241
|
+
|
|
242
|
+
expect(mockSwiperInstance.slideTo).toHaveBeenCalledTimes(2);
|
|
243
|
+
expect(mockSwiperInstance.slideTo).toHaveBeenNthCalledWith(1, 1);
|
|
244
|
+
expect(mockSwiperInstance.slideTo).toHaveBeenNthCalledWith(2, 1);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should ignore index controls with an invalid slide index", () => {
|
|
248
|
+
document.body.insertAdjacentHTML(
|
|
249
|
+
"beforeend",
|
|
250
|
+
`<button data-carousel-controls="test-carousel" data-carousel-slide-index="nope" id="invalid-slide">Invalid</button>`,
|
|
251
|
+
);
|
|
252
|
+
carouselInstance.initExternalControls();
|
|
253
|
+
|
|
254
|
+
document.getElementById("invalid-slide").click();
|
|
255
|
+
|
|
256
|
+
expect(mockSwiperInstance.slideTo).not.toHaveBeenCalled();
|
|
257
|
+
expect(document.getElementById("invalid-slide")).not.toHaveAttribute(
|
|
258
|
+
"aria-pressed",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should highlight the last index control when the carousel reaches the end", () => {
|
|
263
|
+
document.body.insertAdjacentHTML(
|
|
264
|
+
"beforeend",
|
|
265
|
+
`
|
|
266
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="0" id="end-slide-0">XL</button>
|
|
267
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="1" id="end-slide-1">M</button>
|
|
268
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="2" id="end-slide-2">S</button>
|
|
269
|
+
`,
|
|
270
|
+
);
|
|
271
|
+
carouselInstance.initExternalControls();
|
|
272
|
+
|
|
273
|
+
mockSwiperInstance.activeIndex = 1;
|
|
274
|
+
mockSwiperInstance.realIndex = 1;
|
|
275
|
+
mockSwiperInstance.snapIndex = 2;
|
|
276
|
+
mockSwiperInstance.isEnd = true;
|
|
277
|
+
carouselInstance.updateExternalControlsState();
|
|
278
|
+
|
|
279
|
+
expect(document.getElementById("end-slide-2")).toHaveClass("is-active");
|
|
280
|
+
expect(document.getElementById("end-slide-2")).toHaveAttribute(
|
|
281
|
+
"aria-pressed",
|
|
282
|
+
"true",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("should synchronize index controls when only the snap index changes", () => {
|
|
287
|
+
document.body.insertAdjacentHTML(
|
|
288
|
+
"beforeend",
|
|
289
|
+
`
|
|
290
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="0" id="snap-slide-0">XL</button>
|
|
291
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="1" id="snap-slide-1">M</button>
|
|
292
|
+
<button data-carousel-controls="test-carousel" data-carousel-slide-index="2" id="snap-slide-2">S</button>
|
|
293
|
+
`,
|
|
294
|
+
);
|
|
295
|
+
carouselInstance.initExternalControls();
|
|
296
|
+
const swiperConfig = Swiper.mock.calls[0]?.[1];
|
|
297
|
+
|
|
298
|
+
mockSwiperInstance.activeIndex = 1;
|
|
299
|
+
mockSwiperInstance.realIndex = 1;
|
|
300
|
+
mockSwiperInstance.snapIndex = 2;
|
|
301
|
+
mockSwiperInstance.isEnd = true;
|
|
302
|
+
swiperConfig?.on?.slideChange?.();
|
|
303
|
+
expect(document.getElementById("snap-slide-2")).toHaveClass("is-active");
|
|
304
|
+
|
|
305
|
+
mockSwiperInstance.snapIndex = 1;
|
|
306
|
+
mockSwiperInstance.isEnd = false;
|
|
307
|
+
swiperConfig?.on?.snapIndexChange?.();
|
|
308
|
+
|
|
309
|
+
expect(document.getElementById("snap-slide-1")).toHaveClass("is-active");
|
|
310
|
+
expect(document.getElementById("snap-slide-2")).not.toHaveClass(
|
|
311
|
+
"is-active",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
181
315
|
it("should call slideNext when next button is clicked", () => {
|
|
182
316
|
const nextButton = document.getElementById("next-btn");
|
|
183
317
|
nextButton.click();
|
|
@@ -9,13 +9,19 @@ import packageJson from "../../../package.json";
|
|
|
9
9
|
import { TextInput } from "../Forms/TextInput";
|
|
10
10
|
import { Icon } from "../Icon";
|
|
11
11
|
import { Image } from "../Image";
|
|
12
|
+
import type { TagColor } from "../Tag";
|
|
12
13
|
import { Tag } from "../Tag";
|
|
13
14
|
|
|
15
|
+
export interface DocumentationSidebarTag {
|
|
16
|
+
text: string;
|
|
17
|
+
color?: TagColor;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
export interface DocumentationSidebarItem {
|
|
15
21
|
label: string;
|
|
16
22
|
href?: string;
|
|
17
23
|
target?: React.HTMLAttributeAnchorTarget;
|
|
18
|
-
tag?:
|
|
24
|
+
tag?: DocumentationSidebarTag;
|
|
19
25
|
items?: DocumentationSidebarItem[];
|
|
20
26
|
content?: string; // Optional: MDX content for searching
|
|
21
27
|
}
|
|
@@ -207,11 +213,11 @@ const SidebarItem: React.FC<{
|
|
|
207
213
|
<span>{item.label}</span>
|
|
208
214
|
{item.tag ? (
|
|
209
215
|
<Tag
|
|
210
|
-
color="warning"
|
|
216
|
+
color={item.tag.color || "warning"}
|
|
211
217
|
size="small"
|
|
212
218
|
className={`${CLASS_ROOT}__tag`}
|
|
213
219
|
>
|
|
214
|
-
{item.tag}
|
|
220
|
+
{item.tag.text}
|
|
215
221
|
</Tag>
|
|
216
222
|
) : null}
|
|
217
223
|
</span>
|
|
@@ -392,7 +398,9 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
|
|
|
392
398
|
aria-label="Sidebar navigation"
|
|
393
399
|
ref={sidebarRef}
|
|
394
400
|
>
|
|
395
|
-
<
|
|
401
|
+
<Link href="/">
|
|
402
|
+
<Image src="/logo.svg" alt="logo" />
|
|
403
|
+
</Link>
|
|
396
404
|
|
|
397
405
|
<div className="mb">
|
|
398
406
|
<TextInput
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import A11yDialog from "a11y-dialog";
|
|
2
|
+
import Modal from "../Modal/Modal.static";
|
|
3
|
+
|
|
4
|
+
interface DrawerConfig {
|
|
5
|
+
classDrawerIsOpen: string;
|
|
6
|
+
drawersRoot: string;
|
|
7
|
+
disablePortal?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const defaultConfig = (): DrawerConfig => ({
|
|
11
|
+
classDrawerIsOpen: "is-active",
|
|
12
|
+
drawersRoot: "#root-drawers",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const SWIPE_DISTANCE = 80;
|
|
16
|
+
const SWIPE_VELOCITY = 0.5;
|
|
17
|
+
|
|
18
|
+
interface HTMLElementWithDrawer extends HTMLElement {
|
|
19
|
+
ODS_Drawer?: Drawer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Accessible vanilla bottom-sheet drawer backed by a11y-dialog. */
|
|
23
|
+
export default class Drawer {
|
|
24
|
+
element: HTMLElementWithDrawer;
|
|
25
|
+
config: DrawerConfig;
|
|
26
|
+
instance!: A11yDialog;
|
|
27
|
+
private gestureStart: { x: number; y: number; time: number } | null = null;
|
|
28
|
+
private gestureTarget: HTMLElement | null = null;
|
|
29
|
+
private useTouchGestures = false;
|
|
30
|
+
|
|
31
|
+
constructor(element: HTMLElement, config?: Partial<DrawerConfig>) {
|
|
32
|
+
this.element = element as HTMLElementWithDrawer;
|
|
33
|
+
this.config = { ...defaultConfig(), ...config };
|
|
34
|
+
|
|
35
|
+
this.handleShow = this.handleShow.bind(this);
|
|
36
|
+
this.handleHide = this.handleHide.bind(this);
|
|
37
|
+
this.show = this.show.bind(this);
|
|
38
|
+
this.hide = this.hide.bind(this);
|
|
39
|
+
this.handlePointerDown = this.handlePointerDown.bind(this);
|
|
40
|
+
this.handlePointerMove = this.handlePointerMove.bind(this);
|
|
41
|
+
this.handlePointerUp = this.handlePointerUp.bind(this);
|
|
42
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
43
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
|
44
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
45
|
+
|
|
46
|
+
this.element.ODS_Drawer = this;
|
|
47
|
+
this.init();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static getInstance(element: HTMLElement | null): Drawer | null {
|
|
51
|
+
if (!element) return null;
|
|
52
|
+
return (element as HTMLElementWithDrawer).ODS_Drawer || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
handleShow(element: HTMLElement): void {
|
|
56
|
+
Modal.lockBody();
|
|
57
|
+
element.classList.add(this.config.classDrawerIsOpen);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
handleHide(element: HTMLElement): void {
|
|
61
|
+
this.resetGesture();
|
|
62
|
+
Modal.unlockBody();
|
|
63
|
+
element.classList.remove(this.config.classDrawerIsOpen);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
show(): void {
|
|
67
|
+
this.instance.show();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hide(): void {
|
|
71
|
+
this.instance.hide();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
init(): void {
|
|
75
|
+
if (this.config.drawersRoot && !this.config.disablePortal) {
|
|
76
|
+
const container = document.querySelector(this.config.drawersRoot);
|
|
77
|
+
if (container) {
|
|
78
|
+
container.appendChild(this.element);
|
|
79
|
+
} else {
|
|
80
|
+
// biome-ignore lint/suspicious/noConsole: missing portal root is an integration diagnostic
|
|
81
|
+
console.warn(
|
|
82
|
+
`\`${this.config.drawersRoot}\` element is not present in DOM. Drawer will be placed inside its current parent. Please provide \`${this.config.drawersRoot}\` selector outside of the main content.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.instance = new A11yDialog(this.element);
|
|
88
|
+
this.instance.on("show", (event: Event) => {
|
|
89
|
+
this.handleShow(event.currentTarget as HTMLElement);
|
|
90
|
+
});
|
|
91
|
+
this.instance.on("hide", (event: Event) => {
|
|
92
|
+
this.handleHide(event.currentTarget as HTMLElement);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const panel = this.element.querySelector<HTMLElement>(".drawer__panel");
|
|
96
|
+
const overlay = this.element.querySelector<HTMLElement>(".drawer__overlay");
|
|
97
|
+
this.useTouchGestures = navigator.maxTouchPoints > 0;
|
|
98
|
+
[panel, overlay].forEach((target) => {
|
|
99
|
+
if (this.useTouchGestures) {
|
|
100
|
+
target?.addEventListener("touchstart", this.handleTouchStart, {
|
|
101
|
+
passive: true,
|
|
102
|
+
});
|
|
103
|
+
target?.addEventListener("touchmove", this.handleTouchMove, {
|
|
104
|
+
passive: false,
|
|
105
|
+
});
|
|
106
|
+
target?.addEventListener("touchend", this.handleTouchEnd);
|
|
107
|
+
target?.addEventListener("touchcancel", this.handleTouchEnd);
|
|
108
|
+
} else {
|
|
109
|
+
target?.addEventListener("pointerdown", this.handlePointerDown);
|
|
110
|
+
target?.addEventListener("pointermove", this.handlePointerMove);
|
|
111
|
+
target?.addEventListener("pointerup", this.handlePointerUp);
|
|
112
|
+
target?.addEventListener("pointercancel", this.handlePointerUp);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private isDesktop(): boolean {
|
|
118
|
+
return window.innerWidth >= 768;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private handlePointerDown(event: PointerEvent): void {
|
|
122
|
+
if (event.isPrimary === false) return;
|
|
123
|
+
this.gestureStart = {
|
|
124
|
+
x: event.clientX,
|
|
125
|
+
y: event.clientY,
|
|
126
|
+
time: Date.now(),
|
|
127
|
+
};
|
|
128
|
+
this.gestureTarget = event.currentTarget as HTMLElement;
|
|
129
|
+
this.gestureTarget.setPointerCapture?.(event.pointerId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private canStartTouchGesture(event: TouchEvent): boolean {
|
|
133
|
+
const touch = event.touches[0];
|
|
134
|
+
if (!touch) return false;
|
|
135
|
+
|
|
136
|
+
const body = this.element.querySelector<HTMLElement>(".drawer__body");
|
|
137
|
+
const target = event.target as Node | null;
|
|
138
|
+
if (body?.contains(target) && body.scrollTop > 0) return false;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleTouchStart(event: TouchEvent): void {
|
|
143
|
+
if (!this.canStartTouchGesture(event)) return;
|
|
144
|
+
const touch = event.touches[0];
|
|
145
|
+
this.gestureStart = {
|
|
146
|
+
x: touch.clientX,
|
|
147
|
+
y: touch.clientY,
|
|
148
|
+
time: Date.now(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handlePointerMove(event: PointerEvent): void {
|
|
153
|
+
if (!this.gestureStart) return;
|
|
154
|
+
const deltaX = event.clientX - this.gestureStart.x;
|
|
155
|
+
const deltaY = event.clientY - this.gestureStart.y;
|
|
156
|
+
const delta = this.isDesktop() ? deltaX : deltaY;
|
|
157
|
+
const crossAxisDelta = this.isDesktop() ? deltaY : deltaX;
|
|
158
|
+
if (delta <= 0 || Math.abs(crossAxisDelta) > Math.abs(delta)) {
|
|
159
|
+
this.resetGesture();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
const property = this.isDesktop() ? "translateX" : "translateY";
|
|
165
|
+
const panel = this.element.querySelector<HTMLElement>(".drawer__panel");
|
|
166
|
+
if (panel) {
|
|
167
|
+
panel.style.transition = "none";
|
|
168
|
+
panel.style.transform = `${property}(${delta}px)`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private handlePointerUp(event: PointerEvent): void {
|
|
173
|
+
if (!this.gestureStart) return;
|
|
174
|
+
const deltaX = event.clientX - this.gestureStart.x;
|
|
175
|
+
const deltaY = event.clientY - this.gestureStart.y;
|
|
176
|
+
const delta = this.isDesktop() ? deltaX : deltaY;
|
|
177
|
+
const crossAxisDelta = this.isDesktop() ? deltaY : deltaX;
|
|
178
|
+
const elapsed = Math.max(Date.now() - this.gestureStart.time, 1);
|
|
179
|
+
const velocity = delta / elapsed;
|
|
180
|
+
const shouldClose =
|
|
181
|
+
delta > 0 &&
|
|
182
|
+
Math.abs(crossAxisDelta) <= Math.abs(delta) &&
|
|
183
|
+
(delta >= SWIPE_DISTANCE || (delta >= 20 && velocity >= SWIPE_VELOCITY));
|
|
184
|
+
|
|
185
|
+
this.resetGesture();
|
|
186
|
+
if (shouldClose) this.hide();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private handleTouchMove(event: TouchEvent): void {
|
|
190
|
+
if (!this.gestureStart) return;
|
|
191
|
+
const touch = event.touches[0];
|
|
192
|
+
if (!touch) return;
|
|
193
|
+
|
|
194
|
+
const deltaX = touch.clientX - this.gestureStart.x;
|
|
195
|
+
const deltaY = touch.clientY - this.gestureStart.y;
|
|
196
|
+
const delta = this.isDesktop() ? deltaX : deltaY;
|
|
197
|
+
const crossAxisDelta = this.isDesktop() ? deltaY : deltaX;
|
|
198
|
+
if (delta <= 0 || Math.abs(crossAxisDelta) > Math.abs(delta)) {
|
|
199
|
+
this.resetGesture();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
this.updateGestureTransform(delta);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleTouchEnd(event: TouchEvent): void {
|
|
208
|
+
if (!this.gestureStart) return;
|
|
209
|
+
const touch = event.changedTouches[0];
|
|
210
|
+
if (!touch) return;
|
|
211
|
+
|
|
212
|
+
this.finishGesture(touch.clientX, touch.clientY);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private updateGestureTransform(delta: number): void {
|
|
216
|
+
const property = this.isDesktop() ? "translateX" : "translateY";
|
|
217
|
+
const panel = this.element.querySelector<HTMLElement>(".drawer__panel");
|
|
218
|
+
if (panel) {
|
|
219
|
+
panel.style.transition = "none";
|
|
220
|
+
panel.style.transform = `${property}(${delta}px)`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private finishGesture(clientX: number, clientY: number): void {
|
|
225
|
+
if (!this.gestureStart) return;
|
|
226
|
+
const deltaX = clientX - this.gestureStart.x;
|
|
227
|
+
const deltaY = clientY - this.gestureStart.y;
|
|
228
|
+
const delta = this.isDesktop() ? deltaX : deltaY;
|
|
229
|
+
const crossAxisDelta = this.isDesktop() ? deltaY : deltaX;
|
|
230
|
+
const elapsed = Math.max(Date.now() - this.gestureStart.time, 1);
|
|
231
|
+
const velocity = delta / elapsed;
|
|
232
|
+
const shouldClose =
|
|
233
|
+
delta > 0 &&
|
|
234
|
+
Math.abs(crossAxisDelta) <= Math.abs(delta) &&
|
|
235
|
+
(delta >= SWIPE_DISTANCE || (delta >= 20 && velocity >= SWIPE_VELOCITY));
|
|
236
|
+
|
|
237
|
+
this.resetGesture();
|
|
238
|
+
if (shouldClose) this.hide();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private resetGesture(): void {
|
|
242
|
+
const panel = this.element.querySelector<HTMLElement>(".drawer__panel");
|
|
243
|
+
if (panel) {
|
|
244
|
+
panel.style.transition = "";
|
|
245
|
+
panel.style.transform = "";
|
|
246
|
+
}
|
|
247
|
+
this.gestureStart = null;
|
|
248
|
+
this.gestureTarget = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
destroy(): void {
|
|
252
|
+
if (!this.instance) return;
|
|
253
|
+
|
|
254
|
+
this.instance.hide();
|
|
255
|
+
const dialog = this.instance as any;
|
|
256
|
+
document.removeEventListener("click", dialog.handleTriggerClicks, true);
|
|
257
|
+
document.body.removeEventListener("focus", dialog.maintainFocus, true);
|
|
258
|
+
this.element.removeEventListener("keydown", dialog.bindKeypress, true);
|
|
259
|
+
const panel = this.element.querySelector<HTMLElement>(".drawer__panel");
|
|
260
|
+
const overlay = this.element.querySelector<HTMLElement>(".drawer__overlay");
|
|
261
|
+
[panel, overlay].forEach((target) => {
|
|
262
|
+
if (this.useTouchGestures) {
|
|
263
|
+
target?.removeEventListener("touchstart", this.handleTouchStart);
|
|
264
|
+
target?.removeEventListener("touchmove", this.handleTouchMove);
|
|
265
|
+
target?.removeEventListener("touchend", this.handleTouchEnd);
|
|
266
|
+
target?.removeEventListener("touchcancel", this.handleTouchEnd);
|
|
267
|
+
} else {
|
|
268
|
+
target?.removeEventListener("pointerdown", this.handlePointerDown);
|
|
269
|
+
target?.removeEventListener("pointermove", this.handlePointerMove);
|
|
270
|
+
target?.removeEventListener("pointerup", this.handlePointerUp);
|
|
271
|
+
target?.removeEventListener("pointercancel", this.handlePointerUp);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
Modal.unlockBody();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
update(): void {
|
|
278
|
+
this.destroy();
|
|
279
|
+
this.init();
|
|
280
|
+
}
|
|
281
|
+
}
|