@orangesk/orange-design-system 2.0.0-beta.7 → 2.0.0-beta.8

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.
Files changed (43) hide show
  1. package/build/components/index.js +4 -4
  2. package/build/components/index.js.map +1 -1
  3. package/build/components/tsconfig.tsbuildinfo +1 -1
  4. package/build/components/types/index.d.ts +2 -2
  5. package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.d.ts +1 -1
  6. package/build/components/types/src/components/AnchorNavigation/AnchorNavigation.static.d.ts +19 -17
  7. package/build/components/types/src/components/Card/Card.d.ts +1 -1
  8. package/build/components/types/src/components/Megamenu/constants.d.ts +2 -0
  9. package/build/components/types/src/scripts/index.d.ts +5 -0
  10. package/build/lib/after-components.css +1 -1
  11. package/build/lib/after-components.css.map +1 -1
  12. package/build/lib/before-components.css +1 -1
  13. package/build/lib/before-components.css.map +1 -1
  14. package/build/lib/components.css +1 -1
  15. package/build/lib/components.css.map +1 -1
  16. package/build/lib/megamenu.css +1 -1
  17. package/build/lib/megamenu.css.map +1 -1
  18. package/build/lib/megamenu.js +1 -1
  19. package/build/lib/megamenu.js.map +1 -1
  20. package/build/lib/scripts.js +4 -4
  21. package/build/lib/scripts.js.map +1 -1
  22. package/build/lib/style.css +1 -1
  23. package/build/lib/style.css.map +1 -1
  24. package/build/lib/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +10 -10
  26. package/src/components/AnchorNavigation/AnchorNavigation.static.ts +253 -73
  27. package/src/components/AnchorNavigation/AnchorNavigation.tsx +31 -24
  28. package/src/components/AnchorNavigation/styles/mixins.scss +14 -17
  29. package/src/components/AnchorNavigation/tests/AnchorNavigation.conformance.test.js +67 -0
  30. package/src/components/AnchorNavigation/tests/AnchorNavigation.unit.test.js +163 -0
  31. package/src/components/BlockAction/styles/mixins.scss +0 -6
  32. package/src/components/Card/Card.tsx +1 -0
  33. package/src/components/Link/styles/style.scss +1 -1
  34. package/src/components/Link/tests/Link.conformance.test.js +5 -20
  35. package/src/components/Link/tests/Link.unit.test.js +1 -10
  36. package/src/components/Megamenu/Megamenu.static.ts +2 -0
  37. package/src/components/Megamenu/Megamenu.tsx +671 -665
  38. package/src/components/Megamenu/MegamenuBlog.tsx +187 -183
  39. package/src/components/Megamenu/constants.ts +2 -0
  40. package/src/components/Megamenu/styles/mixins.scss +30 -1
  41. package/src/components/Megamenu/styles/style.scss +8 -0
  42. package/src/styles/base/globals.scss +18 -0
  43. package/src/styles/utilities/color.scss +4 -0
@@ -1,29 +1,29 @@
1
- interface AnchorNavigationConfig {
2
- itemSelector: string;
3
- }
4
-
5
- export const defaultConfig: AnchorNavigationConfig = {
6
- itemSelector: '.anchor-navigation__item',
7
- };
8
-
9
- export const configDocs = {
10
- itemSelector: "Anchor navigation item element selector",
11
- };
12
-
13
1
  export default class AnchorNavigation {
14
2
  private element: HTMLElement;
15
- private config: AnchorNavigationConfig;
16
- private items: HTMLElement[];
17
- private activeItemIndex: number | null;
3
+ private megamenuElement: HTMLElement | null;
4
+ private resizeObserver: ResizeObserver | null;
5
+ private scrollHandler: () => void;
6
+ private scrollSpyHandler: () => void;
7
+ private scrollEndHandler: () => void;
8
+ private resizeHandler: () => void;
9
+ private isAutoScrolling: boolean = false;
10
+ private navLinks: NodeListOf<HTMLAnchorElement> | null = null;
11
+ private sections: HTMLElement[] = [];
12
+ private currentPath: string;
13
+ private lastActiveIndex: number = 0;
18
14
 
19
- constructor(element: HTMLElement, config?: Partial<AnchorNavigationConfig>) {
15
+ constructor(element: HTMLElement) {
20
16
  this.element = element;
21
- this.config = { ...defaultConfig, ...config };
22
17
 
23
- this.items = [];
24
- this.activeItemIndex = null;
18
+ this.megamenuElement = null;
19
+ this.resizeObserver = null;
20
+ this.isAutoScrolling = false;
21
+ this.currentPath = window.location.pathname;
25
22
 
26
- this.handleClick = this.handleClick.bind(this);
23
+ this.scrollHandler = this.updateStickyPosition.bind(this);
24
+ this.scrollSpyHandler = this.handleScrollSpy.bind(this);
25
+ this.scrollEndHandler = this.handleScrollEnd.bind(this);
26
+ this.resizeHandler = this.initScrollSpy.bind(this);
27
27
 
28
28
  (this.element as any).ODS_AnchorNavigation = this;
29
29
 
@@ -33,83 +33,263 @@ export default class AnchorNavigation {
33
33
  }
34
34
 
35
35
  static getInstance(el: HTMLElement): AnchorNavigation | null {
36
- return el && (el as any).ODS_AnchorNavigation ? (el as any).ODS_AnchorNavigation : null;
36
+ return el && (el as any).ODS_AnchorNavigation
37
+ ? (el as any).ODS_AnchorNavigation
38
+ : null;
37
39
  }
38
40
 
39
- private init(): void {
40
- this.items = Array.from(
41
- this.element.querySelectorAll<HTMLElement>(this.config.itemSelector),
42
- );
41
+ private findMegamenuElement(): HTMLElement | null {
42
+ return document.querySelector("[data-megamenu]") as HTMLElement | null;
43
+ }
43
44
 
44
- this.items.forEach((item, index) => {
45
- item.addEventListener("click", this.handleClick);
45
+ private updateStickyPosition(): void {
46
+ if (!this.megamenuElement) return;
47
+ this.element.style.top = `${this.megamenuElement.offsetHeight}px`;
48
+ }
46
49
 
47
- if (this.isActive(item)) {
48
- this.activeItemIndex = index;
49
- }
50
- });
50
+ private setupMegamenuObserver(): void {
51
+ this.megamenuElement = this.findMegamenuElement();
51
52
 
52
- // Ak nie je žiadna položka aktívna, aktivuj prvú
53
- if (this.activeItemIndex === null && this.items.length > 0) {
54
- this.activateNthItem(0);
53
+ if (!this.megamenuElement) {
54
+ this.element.style.top = "0px";
55
+ return;
55
56
  }
57
+
58
+ this.updateStickyPosition();
59
+ window.addEventListener("scroll", this.scrollHandler, { passive: true });
60
+
61
+ this.resizeObserver = new ResizeObserver(
62
+ this.updateStickyPosition.bind(this),
63
+ );
64
+ this.resizeObserver.observe(this.megamenuElement);
56
65
  }
57
66
 
58
- destroy(): void {
59
- this.items.forEach((item) => {
60
- item.removeEventListener("click", this.handleClick);
67
+ private setupScrollSpy(): void {
68
+ // Set dynamic scroll margin for CSS
69
+ document.documentElement.style.setProperty(
70
+ "--extra-scroll-margin",
71
+ this.element.offsetHeight + "px",
72
+ );
73
+
74
+ // Get all anchor navigation links
75
+ this.navLinks = this.element.querySelectorAll(".anchor-navigation__item");
76
+
77
+ // Get all sections that correspond to the navigation links
78
+ this.sections = Array.from(this.navLinks || [])
79
+ .map((link) => link.getAttribute("href"))
80
+ .filter((href) => href?.includes("#"))
81
+ .map((href) => document.getElementById(href!.split("#")[1]))
82
+ .filter(Boolean) as HTMLElement[];
83
+
84
+ // Add click listeners to anchor navigation items
85
+ this.navLinks.forEach((anchor) => {
86
+ anchor.addEventListener("click", (event) => {
87
+ event.preventDefault();
88
+ const href = anchor.getAttribute("href");
89
+ if (href && href.includes("#")) {
90
+ const targetId = href.split("#")[1];
91
+ const targetElement = document.getElementById(targetId);
92
+
93
+ if (targetElement) {
94
+ this.isAutoScrolling = true;
95
+
96
+ // Calculate scroll position
97
+ const scrollOffset = this.megamenuElement
98
+ ? this.megamenuElement.offsetHeight
99
+ : 0;
100
+ const additionalOffset = this.element.offsetHeight;
101
+ const targetTop =
102
+ targetElement.offsetTop - scrollOffset - additionalOffset;
103
+
104
+ // Smooth scroll to target
105
+ window.scrollTo({
106
+ top: Math.max(0, targetTop),
107
+ behavior: "smooth",
108
+ });
109
+
110
+ // Update active state
111
+ this.initScrollSpy(targetId);
112
+ }
113
+ }
114
+ });
61
115
  });
62
116
 
63
- this.items = [];
64
- (this.element as any).ODS_AnchorNavigation = null;
65
- }
117
+ // Add scroll listeners
118
+ window.addEventListener("scroll", this.scrollSpyHandler, { passive: true });
66
119
 
67
- update(): void {
68
- this.destroy();
69
- this.init();
120
+ // Add scrollend listener with fallback
121
+ try {
122
+ window.addEventListener("scrollend", this.scrollEndHandler);
123
+ } catch {
124
+ // Fallback for browsers that don't support scrollend
125
+ let scrollTimeout: ReturnType<typeof setTimeout>;
126
+ const scrollEndFallback = () => {
127
+ clearTimeout(scrollTimeout);
128
+ scrollTimeout = setTimeout(() => {
129
+ this.handleScrollEnd();
130
+ }, 150);
131
+ };
132
+ window.addEventListener("scroll", scrollEndFallback, { passive: true });
133
+ }
134
+
135
+ window.addEventListener("resize", this.resizeHandler);
136
+
137
+ this.initScrollSpy();
70
138
  }
71
139
 
72
- private isActive = (el: HTMLElement): boolean => {
73
- return el.classList.contains('is-active');
74
- };
140
+ private initScrollSpy(forcedSectionId: string | null = null): void {
141
+ if (!this.navLinks || !this.sections.length) return;
142
+
143
+ let targetSection: HTMLElement | undefined;
144
+ let targetIndex: number = -1;
145
+
146
+ // Remove active class from all links
147
+ this.navLinks.forEach((link) => link.classList.remove("is-active"));
148
+
149
+ if (forcedSectionId) {
150
+ targetSection = document.getElementById(forcedSectionId) || undefined;
151
+ if (targetSection) {
152
+ // Find the index of the forced section
153
+ targetIndex = this.sections.findIndex(
154
+ (section) => section.id === forcedSectionId,
155
+ );
156
+ }
157
+ } else {
158
+ // Calculate scroll offset from megamenu height and anchor nav height
159
+ const scrollOffset = this.megamenuElement
160
+ ? this.megamenuElement.offsetHeight
161
+ : 0;
162
+ const anchorNavOffset = this.element.offsetHeight;
163
+ const totalOffset = scrollOffset + anchorNavOffset;
164
+ const effectiveCenter = window.scrollY + totalOffset + 50; // Add some buffer
165
+
166
+ // Find the section that's currently in view
167
+ for (let i = 0; i < this.sections.length; i++) {
168
+ const section = this.sections[i];
169
+ const { top: sectionTopRaw, height: sectionHeight } =
170
+ section.getBoundingClientRect();
171
+ const sectionTop = sectionTopRaw + window.scrollY;
172
+ const sectionBottom = sectionTop + sectionHeight;
75
173
 
76
- private handleClick(e: MouseEvent): void {
77
- const clickedItem = e.currentTarget as HTMLElement;
174
+ if (effectiveCenter >= sectionTop && effectiveCenter < sectionBottom) {
175
+ targetSection = section;
176
+ targetIndex = i;
177
+ break;
178
+ }
179
+ }
180
+ }
78
181
 
79
- // Najdi index kliknutej položky
80
- const clickedIndex = this.items.findIndex(item => item === clickedItem);
81
-
82
- if (clickedIndex !== -1) {
83
- this.activateNthItem(clickedIndex);
182
+ // Fallback logic: if no section is found, use fallback rules
183
+ if (!targetSection) {
184
+ // If we're at the very top, activate first item
185
+ if (window.scrollY <= 100) {
186
+ targetIndex = 0;
187
+ targetSection = this.sections[0];
188
+ } else {
189
+ // Keep the last active item
190
+ targetIndex = this.lastActiveIndex;
191
+ targetSection = this.sections[targetIndex];
192
+ }
84
193
  }
85
- }
86
194
 
87
- private toggleItem(el: HTMLElement, state: "on" | "off"): void {
88
- const isActive = this.isActive(el);
195
+ // Update last active index if we found a valid target
196
+ if (targetIndex >= 0) {
197
+ this.lastActiveIndex = targetIndex;
198
+ }
199
+
200
+ if (targetSection) {
201
+ const id = targetSection.getAttribute("id");
89
202
 
90
- if (state === "on" && !isActive) {
91
- el.classList.add('is-active');
203
+ // Find the matching navigation link - try different href patterns
204
+ let activeLink = this.element.querySelector(
205
+ `.anchor-navigation__item[href="#${id}"]`,
206
+ ) as HTMLElement;
207
+
208
+ if (!activeLink) {
209
+ activeLink = this.element.querySelector(
210
+ `.anchor-navigation__item[href="${this.currentPath}#${id}"]`,
211
+ ) as HTMLElement;
212
+ }
213
+
214
+ if (!activeLink) {
215
+ // Try without current path for relative links
216
+ activeLink = Array.from(this.navLinks).find((link) => {
217
+ const href = link.getAttribute("href");
218
+ return href && href.endsWith(`#${id}`);
219
+ }) as HTMLElement;
220
+ }
221
+
222
+ if (activeLink) {
223
+ activeLink.classList.add("is-active");
224
+
225
+ // Smooth scroll the navigation to center the active link
226
+ const contentLeft = this.element.querySelector(
227
+ ".anchor-navigation__content-left",
228
+ ) as HTMLElement;
229
+ if (contentLeft) {
230
+ contentLeft.scrollTo({
231
+ left:
232
+ activeLink.offsetLeft -
233
+ this.element.clientWidth / 2 +
234
+ activeLink.clientWidth / 2,
235
+ behavior: "smooth",
236
+ });
237
+ }
238
+ }
92
239
  }
240
+ }
93
241
 
94
- if (state === "off" && isActive) {
95
- el.classList.remove('is-active');
242
+ private handleScrollSpy(): void {
243
+ if (!this.isAutoScrolling) {
244
+ this.initScrollSpy();
96
245
  }
97
246
  }
98
247
 
99
- activateNthItem(index: number): void {
100
- if (index < 0 || index >= this.items.length) return;
248
+ private handleScrollEnd(): void {
249
+ this.isAutoScrolling = false;
250
+ this.initScrollSpy();
251
+ }
101
252
 
102
- this.items.forEach((item, i) => {
103
- if (i === index) {
104
- this.activeItemIndex = i;
105
- this.toggleItem(item, "on");
106
- } else {
107
- this.toggleItem(item, "off");
108
- }
109
- });
253
+ private init(): void {
254
+ this.setupMegamenuObserver();
255
+ this.setupScrollSpy();
110
256
  }
111
257
 
112
- getActiveItemIndex(): number | null {
113
- return this.activeItemIndex;
258
+ destroy(): void {
259
+ window.removeEventListener("scroll", this.scrollHandler);
260
+ window.removeEventListener("scroll", this.scrollSpyHandler);
261
+ window.removeEventListener("scrollend", this.scrollEndHandler);
262
+ window.removeEventListener("resize", this.resizeHandler);
263
+
264
+ if (this.resizeObserver) {
265
+ this.resizeObserver.disconnect();
266
+ this.resizeObserver = null;
267
+ }
268
+
269
+ this.element.style.top = "";
270
+ this.megamenuElement = null;
271
+ this.navLinks = null;
272
+ this.sections = [];
273
+ this.lastActiveIndex = 0;
274
+ (this.element as any).ODS_AnchorNavigation = null;
114
275
  }
115
- }
276
+
277
+ update(): void {
278
+ window.removeEventListener("scroll", this.scrollHandler);
279
+ window.removeEventListener("scroll", this.scrollSpyHandler);
280
+ window.removeEventListener("scrollend", this.scrollEndHandler);
281
+ window.removeEventListener("resize", this.resizeHandler);
282
+
283
+ if (this.resizeObserver) {
284
+ this.resizeObserver.disconnect();
285
+ this.resizeObserver = null;
286
+ }
287
+
288
+ this.megamenuElement = null;
289
+ this.navLinks = null;
290
+ this.sections = [];
291
+ this.lastActiveIndex = 0;
292
+
293
+ this.init();
294
+ }
295
+ }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import React from "react";
4
4
  import cx from "classnames";
5
- import { Grid, GridCol } from "../Grid";
6
5
  import { Link } from "../Link";
7
6
  import { Container } from "../Container";
8
7
  import { useStatic } from "../../utils/hooks";
@@ -28,6 +27,7 @@ const AnchorNavigation = ({
28
27
  className,
29
28
  colorScheme,
30
29
  children,
30
+ ...other
31
31
  }: AnchorNavigationProps) => {
32
32
  const classes = cx(
33
33
  CLASS_ROOT,
@@ -41,31 +41,38 @@ const AnchorNavigation = ({
41
41
  const [anchorNavRef] = useStatic(AnchorNavigationStatic);
42
42
 
43
43
  return (
44
- <div className={classes} ref={anchorNavRef}>
45
- <Container>
46
- <div className={`${CLASS_ROOT}__content`}>
47
- <Grid
48
- className={`list-unstyled list-inline horizontal-scroll mb-none ${CLASS_ROOT}__content-left`}
49
- >
50
- {items.map((item) => (
51
- <GridCol size={"auto"} key={item.href}>
52
- <Link
53
- href={item.href}
54
- className={cx(`${CLASS_ROOT}__item`, {
55
- "is-active": item.isActive,
56
- })}
57
- >
58
- {item.label}
59
- </Link>
60
- </GridCol>
61
- ))}
62
- </Grid>
63
- {children && (
64
- <div className={`${CLASS_ROOT}__content-right`}>{children}</div>
44
+ <nav
45
+ className={classes}
46
+ ref={anchorNavRef}
47
+ data-anchor-navigation
48
+ aria-label="Sekcie stránky"
49
+ {...other}
50
+ >
51
+ <Container className={`${CLASS_ROOT}__content`}>
52
+ <ul
53
+ className={cx(
54
+ "list-inline horizontal-scroll mb-none",
55
+ `${CLASS_ROOT}__content-left`,
65
56
  )}
66
- </div>
57
+ >
58
+ {items.map((item) => (
59
+ <li key={item.href}>
60
+ <Link
61
+ href={item.href}
62
+ className={cx(`${CLASS_ROOT}__item`, {
63
+ "is-active": item.isActive,
64
+ })}
65
+ >
66
+ {item.label}
67
+ </Link>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ {children && (
72
+ <div className={`${CLASS_ROOT}__content-right`}>{children}</div>
73
+ )}
67
74
  </Container>
68
- </div>
75
+ </nav>
69
76
  );
70
77
  };
71
78
 
@@ -4,16 +4,16 @@
4
4
  @use "./../../../styles/tools/generate";
5
5
  @use "./../../../styles/tools/convert";
6
6
  @use "./../../../styles/typography/config" as typography;
7
+ @use "../../Megamenu/styles/config" as megamenuConfig;
7
8
  @use "sass:map" as sass-map;
8
9
 
9
10
  @mixin anchor-navigation() {
10
11
  position: sticky;
11
12
  top: 0;
12
- z-index: 100;
13
+ z-index: 10;
13
14
  background-color: var(--color-background-primary) !important;
14
- border-bottom: 1px solid var(--color-border-subtle);
15
+ border-bottom: 1px solid var(--color-border-strong);
15
16
 
16
- // Štýly pre Grid s horizontal-scroll
17
17
  .horizontal-scroll {
18
18
  &::-webkit-scrollbar {
19
19
  display: none !important;
@@ -22,7 +22,6 @@
22
22
  background: transparent !important;
23
23
  }
24
24
 
25
- // Pre Firefox
26
25
  scrollbar-width: none !important;
27
26
  -ms-overflow-style: none !important;
28
27
  }
@@ -30,22 +29,13 @@
30
29
 
31
30
  @mixin anchor-navigation-item() {
32
31
  margin-right: space.get("large") !important;
32
+ margin-bottom: 0 !important;
33
33
  padding: convert.to-rem(25px) 0 !important;
34
34
  white-space: nowrap;
35
- border-bottom: 4px solid transparent;
36
35
  text-decoration: none !important;
37
36
  display: inline-block;
38
37
  cursor: pointer;
39
-
40
- &:focus {
41
- background-color: var(--color-background-primary) !important;
42
- color: var(--color-text-default) !important;
43
- outline: none !important;
44
- }
45
-
46
- &:hover {
47
- text-decoration: none !important;
48
- }
38
+ font-weight: 700 !important;
49
39
 
50
40
  &:last-child {
51
41
  margin-right: 0;
@@ -56,9 +46,14 @@
56
46
  padding: space.get("small") 0 !important;
57
47
  }
58
48
 
49
+ &:hover,
50
+ &:focus-visible,
51
+ &:active,
59
52
  &.is-active {
60
- border-bottom-color: var(--color-border-accent);
61
- font-weight: 600;
53
+ box-shadow: megamenuConfig.$active-line;
54
+ color: initial;
55
+ text-decoration: none !important;
56
+ outline: none;
62
57
  }
63
58
  }
64
59
 
@@ -75,6 +70,8 @@
75
70
  }
76
71
 
77
72
  @mixin anchor-navigation-content-left() {
73
+ display: flex;
74
+
78
75
  @include breakpoint.get("xs", "down") {
79
76
  width: 100%;
80
77
  }
@@ -0,0 +1,67 @@
1
+ import { render } from "@testing-library/react";
2
+ import { axe } from "jest-axe";
3
+
4
+ import { AnchorNavigation } from "../";
5
+
6
+ const basicItems = [
7
+ { label: "Key Features", href: "#features", isActive: true },
8
+ { label: "Pricing", href: "#pricing" },
9
+ { label: "Getting Started", href: "#getting-started" },
10
+ { label: "Contact", href: "#contact" },
11
+ ];
12
+
13
+ const example = (
14
+ <div id="root">
15
+ {/* Basic usage */}
16
+ <AnchorNavigation items={basicItems} aria-label="Basic navigation" />
17
+
18
+ {/* With color scheme */}
19
+ <AnchorNavigation
20
+ items={basicItems}
21
+ colorScheme="light"
22
+ aria-label="Light navigation"
23
+ />
24
+
25
+ {/* With additional content */}
26
+ <AnchorNavigation
27
+ items={basicItems}
28
+ colorScheme="dark"
29
+ aria-label="Dark navigation with content"
30
+ >
31
+ <div className="bold">
32
+ <div className="align-sm-right">2 €</div>
33
+ <div className="reset-font-weight">No commitment</div>
34
+ </div>
35
+ <button type="button">Get Started</button>
36
+ </AnchorNavigation>
37
+
38
+ {/* Multiple items */}
39
+ <AnchorNavigation
40
+ items={[
41
+ { label: "Overview", href: "#overview", isActive: true },
42
+ { label: "Features", href: "#features" },
43
+ { label: "Documentation", href: "#docs" },
44
+ { label: "API Reference", href: "#api" },
45
+ { label: "Examples", href: "#examples" },
46
+ { label: "Support", href: "#support" },
47
+ ]}
48
+ colorScheme="light"
49
+ aria-label="Extended navigation"
50
+ />
51
+ </div>
52
+ );
53
+
54
+ it("is valid html", () => {
55
+ const { container } = render(example);
56
+ expect(container).toHTMLValidate({
57
+ rules: {
58
+ "no-inline-style": "off",
59
+ "attribute-boolean-style": "off",
60
+ },
61
+ });
62
+ });
63
+
64
+ it("is accessible", async () => {
65
+ const { container } = render(example);
66
+ expect(await axe(container)).toHaveNoViolations();
67
+ });