@orangesk/orange-design-system 2.0.0-beta.43 → 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.
Files changed (53) hide show
  1. package/build/components/BodyBanner/style.css +1 -1
  2. package/build/components/BodyBanner/style.css.map +1 -1
  3. package/build/components/Breadcrumbs/style.css +1 -1
  4. package/build/components/Breadcrumbs/style.css.map +1 -1
  5. package/build/components/Carousel/style.css +1 -1
  6. package/build/components/Carousel/style.css.map +1 -1
  7. package/build/components/Expander/style.css +1 -1
  8. package/build/components/Expander/style.css.map +1 -1
  9. package/build/components/Footer/style.css +1 -1
  10. package/build/components/Footer/style.css.map +1 -1
  11. package/build/components/Grid/style.css +1 -1
  12. package/build/components/Grid/style.css.map +1 -1
  13. package/build/components/Megamenu/style.css +1 -1
  14. package/build/components/Megamenu/style.css.map +1 -1
  15. package/build/components/Tag/style.css +1 -1
  16. package/build/components/Tag/style.css.map +1 -1
  17. package/build/components/index.js +1 -1
  18. package/build/components/index.js.map +1 -1
  19. package/build/components/tsconfig.tsbuildinfo +1 -1
  20. package/build/components/types/index.d.ts +4 -2
  21. package/build/components/types/src/components/Carousel/Carousel.static.d.ts +7 -0
  22. package/build/components/types/src/components/Expander/Expander.d.ts +2 -0
  23. package/build/components/types/src/components/Grid/Grid.d.ts +2 -2
  24. package/build/lib/base.css +1 -1
  25. package/build/lib/base.css.map +1 -1
  26. package/build/lib/components.css +1 -1
  27. package/build/lib/components.css.map +1 -1
  28. package/build/lib/footer.css +1 -1
  29. package/build/lib/footer.css.map +1 -1
  30. package/build/lib/megamenu.css +1 -1
  31. package/build/lib/megamenu.css.map +1 -1
  32. package/build/lib/scripts.js +1 -1
  33. package/build/lib/scripts.js.map +1 -1
  34. package/build/lib/style.css +1 -1
  35. package/build/lib/style.css.map +1 -1
  36. package/build/search-index.json +2 -2
  37. package/package.json +6 -6
  38. package/src/components/BodyBanner/styles/mixins.scss +2 -12
  39. package/src/components/Carousel/Carousel.static.ts +109 -57
  40. package/src/components/Carousel/styles/mixins.scss +1 -1
  41. package/src/components/Carousel/tests/Carousel.static.test.jsx +91 -5
  42. package/src/components/Expander/Expander.tsx +4 -0
  43. package/src/components/Expander/styles/style.scss +12 -0
  44. package/src/components/Expander/tests/Expander.conformance.test.jsx +4 -0
  45. package/src/components/Expander/tests/Expander.unit.test.jsx +9 -0
  46. package/src/components/Grid/Grid.tsx +5 -2
  47. package/src/components/Grid/styles/config.scss +3 -2
  48. package/src/components/Grid/tests/Grid.unit.test.jsx +40 -10
  49. package/src/components/Tag/styles/config.scss +5 -1
  50. package/src/components/Tag/styles/mixins.scss +2 -1
  51. package/src/styles/base/globals.scss +1 -0
  52. package/src/styles/export/base.js +1 -1
  53. package/src/styles/tokens/base.scss +1 -1
@@ -81,7 +81,7 @@
81
81
  },
82
82
  {
83
83
  "href": "/components/expander",
84
- "content": "import React from \"react\"; Expander The Expander component is used to hide and show content. It can be used to reduce the amount of content visible on the page by default, making it easier for users to find what they are looking for. Basic Usage {This is the content that will be hidden until the user clicks the summary text.} Changing the summary text Add the summaryOpened prop to change the text displayed when the content is visible. {This is the content that will be hidden until the user clicks the summary text.} Custom Icon Use the renderSummary prop to customize the summary icon. Icon is rotating thanks to data-summary-icon attribute, which needs to be present on the icon. Custom Summary text Use the renderSummary and renderSummaryOpened props to customize the summary text. Icons do not rotate in this case. Toggle Group Use the toggleGroup prop to synchronize multiple expanders together. When one expander in the group is toggled, all others with the same toggleGroup value will toggle together. Used mostly in Product cards. Full Width Use the isFullWidth prop to make the expander take the full width of its container. Accessibility The Expander component uses the native <details> and <summary> HTML elements, which provide built-in accessibility support. Screen readers automatically announce the expanded/collapsed state without requiring manual ARIA attributes. The native <details> element ensures: - Keyboard navigation works automatically (Enter/Space to toggle) - Screen readers announce the state changes - Focus management is handled by the browser Important: Ensure that the summary prop provides a clear and concise description of the content that will be revealed. Use summaryOpened to provide appropriate text for the expanded state to improve user experience. API React Props | Prop | Type | Default | Description | | --------------------- | ------------------------------------------- | ------- | -------------------------------------------------------- | | summary | string | - | Trigger text when collapsed | | summaryOpened | string | - | Trigger text when expanded | | renderSummary | (props: ExpanderProps) => React.ReactNode | - | Custom summary renderer for collapsed state | | renderSummaryOpened | (props: ExpanderProps) => React.ReactNode | - | Custom summary renderer for expanded state | | isFullWidth | boolean | - | Expander takes full width of its container | | toggleGroup | string | - | Group identifier for syncing multiple expanders together | | className | string | - | Additional CSS classes | | children | React.ReactNode | - | Content to show/hide | JS module reference All elements with data-expander are initialized automatically. Custom initialization example: Expander Methods | Method | Type | Description | | --------- | ------ | ---------------------------------------------------------- | | toggle | func | Toggle the expander open/closed state | | destroy | func | Destroy the instance - removes all listeners | | update | func | Re-initialize the component (useful after content changes) |"
84
+ "content": "import React from \"react\"; Expander The Expander component is used to hide and show content. It can be used to reduce the amount of content visible on the page by default, making it easier for users to find what they are looking for. Basic Usage {This is the content that will be hidden until the user clicks the summary text.} Changing the summary text Add the summaryOpened prop to change the text displayed when the content is visible. {This is the content that will be hidden until the user clicks the summary text.} Custom Icon Use the renderSummary prop to customize the summary icon. Icon is rotating thanks to data-summary-icon attribute, which needs to be present on the icon. Custom Summary text Use the renderSummary and renderSummaryOpened props to customize the summary text. Icons do not rotate in this case. Toggle Group Use the toggleGroup prop to synchronize multiple expanders together. When one expander in the group is toggled, all others with the same toggleGroup value will toggle together. Used mostly in Product cards. Full Width Use the isFullWidth prop to make the expander take the full width of its container. Content placement Use the placement prop with value top to render opened content above trigger. Accessibility The Expander component uses the native <details> and <summary> HTML elements, which provide built-in accessibility support. Screen readers automatically announce the expanded/collapsed state without requiring manual ARIA attributes. The native <details> element ensures: - Keyboard navigation works automatically (Enter/Space to toggle) - Screen readers announce the state changes - Focus management is handled by the browser Important: Ensure that the summary prop provides a clear and concise description of the content that will be revealed. Use summaryOpened to provide appropriate text for the expanded state to improve user experience. API React Props | Prop | Type | Default | Description | | --------------------- | ------------------------------------------- | ---------- | -------------------------------------------------------- | | summary | string | - | Trigger text when collapsed | | summaryOpened | string | - | Trigger text when expanded | | renderSummary | (props: ExpanderProps) => React.ReactNode | - | Custom summary renderer for collapsed state | | renderSummaryOpened | (props: ExpanderProps) => React.ReactNode | - | Custom summary renderer for expanded state | | isFullWidth | boolean | - | Expander takes full width of its container | | placement | \"top\" \\| \"bottom\" | \"bottom\" | Position of trigger relative to content when opened | | toggleGroup | string | - | Group identifier for syncing multiple expanders together | | className | string | - | Additional CSS classes | | children | React.ReactNode | - | Content to show/hide | JS module reference All elements with data-expander are initialized automatically. Custom initialization example: Expander Methods | Method | Type | Description | | --------- | ------ | ---------------------------------------------------------- | | toggle | func | Toggle the expander open/closed state | | destroy | func | Destroy the instance - removes all listeners | | update | func | Re-initialize the component (useful after content changes) |"
85
85
  },
86
86
  {
87
87
  "href": "/components/feature-accordion",
@@ -165,7 +165,7 @@
165
165
  },
166
166
  {
167
167
  "href": "/components/grid",
168
- "content": "import spaces from \"@/styles/export/space.js\"; Grid The main function of the grid is to distribute items to rows and columns. Our grid has a maximum of 12 columns. Each column can have a custom size defined by breakpoints. Breakpoints Grid uses the mobile-first definition of breakpoints, which means that smallest breakpoint defines default size. Columns You can have a maximum of 12 columns in one row. A default column takes all the available space. <code>(default) 12</code> <code>6</code> <hr /> <code>auto</code> <code>4</code> <code>6</code> <hr /> <code>fill</code> <code>4</code> <code>6</code> <hr /> <code>shrink</code> <code>6</code> Resizing There are two ways of resizing a column. 1. .gridcol--[size] where size is a number 1-12 or one of [shrink/fill/auto] 2. .gridcol--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the column size > XS breakpoint size > > There is no need to specify the xs breakpoint size because it's a default by definition of mobile-first philosophy. What is shrink/fill/auto? - shrink column takes the least amount of space available - fill column takes all the space available and does not shrink to accomodate other columns - auto takes all available space, also tries to shrink to fit into one row. This is the same as .gridcol. <code>(default) 12</code> <code>4</code> <code>shrink</code> <code>fill</code> <code>auto</code> Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Available combinations You can use combinations of different size types for one column at once. Available types are [1-12/shrink/fill/auto] for every breakpoint. <code>(default) 12, sm-9, md-6, lg-fill</code> <code>(default) 12, sm-3, md-6, lg-fill</code> Justify content height Grid with justified content height uses flexbox with an attempt to equalize height of column children <div className=\"surface-subtle\"> <p>Children of these columns are the same height</p> </div> <div className=\"surface-subtle\"> <p>The most high column dictates the height of the row</p> </div> <div className=\"surface-subtle\"> <p>Subsequent rows remain unaffected</p> </div> Container alignment Horizontal Vertical Children alignment Vertical Column gap size Add larger space between grid columns, use smaller gap, or remove them completely. There are two ways of adding a column gap: 1. .grid-column-gap--[size] where size is one of [none/small/default/large] 2. .grid-column-gap--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the gap size {[\"large\", \"default\", \"small\", \"none\"].map((size, index) => ( <div key={index.toString()}> <h3 className=\"bold\">Column gap size {JSON.stringify(size)}</h3> {[1, 2, 3].map((key) => ( <code>4</code> ))} </div> ))} Row gap size Row gap helps to maintain space between grid rows and fixes unwanted spacing problems on the bottom of grid's last row elements by removing their content margins. There are two ways of adding a row gap: 1. .grid-row-gap--[size] where size is one of [small/medium/large/xlarge] 2. .grid-row-gap--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the gap size {[\"small\", \"medium\", \"large\", \"xlarge\"].map((size, index, array) => ( <h3 className=\"mb-2\">Row gap size {JSON.stringify(size)}</h3> {[1, 2, 3, 4, 5, 6].map((key) => ( <code>6</code> ))} ))} Advanced Examples Complex Layout with Alignment <code>top aligned</code> <code>auto middle</code> <code>fill bottom</code> <code>stretch</code> Semantic HTML Tags <code>main content</code> <code>sidebar</code> All Column Sizes (1-12) {Array.from({ length: 12 }, (, i) => ( <code>{i + 1}</code> ))} API Grid React Props | Name | Type | Default | Description | | --------------- | ------------------------------------------------------------------- | ------- | ------------------------------------------ | | vAlign | \"stretch\" \\| \"start\" \\| \"end\" \\| \"center\" | - | Vertical alignment of grid items. | | hAlign | \"end\" \\| \"center\" \\| \"space-around\" \\| \"space-between\" | - | Horizontal alignment of grid items. | | tag | React.ElementType | \"div\" | Rendered HTML element. | | justifyHeight | boolean | false | Whether grid should justify height. | | rowGapSize | \"small\" \\| \"medium\" \\| \"large\" \\| \"xlarge\" \\| Record<string, ...> | - | Row gap size, can be responsive object. | | columnGapSize | \"none\" \\| \"small\" \\| \"default\" \\| \"large\" \\| Record<string, ...> | - | Column gap size, can be responsive object. | | className | string | - | Additional CSS classes. | GridCol React Props | Name | Type | Default | Description | | ----------- | ------------------- | ------- | --------------------------------------------- | | size | ResponsiveSize | - | Column size (1-12), can be responsive object. | | offset | ResponsiveSize | - | Column offset, can be responsive object. | | tag | React.ElementType | \"div\" | Rendered HTML element. | | className | string | - | Additional CSS classes. |"
168
+ "content": "import spaces from \"@/styles/export/space.js\"; Grid The main function of the grid is to distribute items to rows and columns. Our grid has a maximum of 12 columns. Each column can have a custom size defined by breakpoints. Breakpoints Grid uses the mobile-first definition of breakpoints, which means that smallest breakpoint defines default size. Columns You can have a maximum of 12 columns in one row. A default column takes all the available space. <code>(default) 12</code> <code>6</code> <hr /> <code>auto</code> <code>4</code> <code>6</code> <hr /> <code>fill</code> <code>4</code> <code>6</code> <hr /> <code>shrink</code> <code>6</code> Resizing There are two ways of resizing a column. 1. .gridcol--[size] where size is a number 1-12 or one of [shrink/fill/auto] 2. .gridcol--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the column size > XS breakpoint size > > There is no need to specify the xs breakpoint size because it's a default by definition of mobile-first philosophy. What is shrink/fill/auto? - shrink column takes the least amount of space available - fill column takes all the space available and does not shrink to accomodate other columns - auto takes all available space, also tries to shrink to fit into one row. This is the same as .gridcol. <code>(default) 12</code> <code>4</code> <code>shrink</code> <code>fill</code> <code>auto</code> Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Available combinations You can use combinations of different size types for one column at once. Available types are [1-12/shrink/fill/auto] for every breakpoint. <code>(default) 12, sm-9, md-6, lg-fill</code> <code>(default) 12, sm-3, md-6, lg-fill</code> Justify content height Grid with justified content height uses flexbox with an attempt to equalize height of column children <div className=\"surface-subtle\"> <p>Children of these columns are the same height</p> </div> <div className=\"surface-subtle\"> <p>The most high column dictates the height of the row</p> </div> <div className=\"surface-subtle\"> <p>Subsequent rows remain unaffected</p> </div> Container alignment Horizontal Vertical Children alignment Vertical Column gap size Add larger space between grid columns, use smaller gap, or remove them completely. There are two ways of adding a column gap: 1. .grid-column-gap--[size] where size is one of [none/small/default/large/xlarge] 2. .grid-column-gap--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the gap size {[\"xlarge\", \"large\", \"default\", \"small\", \"none\"].map((size, index) => ( <div key={index.toString()}> <h3 className=\"bold\">Column gap size {JSON.stringify(size)}</h3> {[1, 2, 3].map((key) => ( <code>4</code> ))} </div> ))} <h3 className=\"mb-2\"> Responsive column gap size {'{\"xs\":\"small\",\"md\":\"xlarge\"}'} </h3> {[1, 2, 3].map((key) => ( <code>4</code> ))} Row gap size Row gap helps to maintain space between grid rows and fixes unwanted spacing problems on the bottom of grid's last row elements by removing their content margins. There are two ways of adding a row gap: 1. .grid-row-gap--[size] where size is one of [none/xsmall/small/medium/large/xlarge] 2. .grid-row-gap--[breakpoint]-[size] where breakpoint is used as a minimum display width to change the gap size {[\"none\", \"xsmall\", \"small\", \"medium\", \"large\", \"xlarge\"].map( (size, index, array) => ( <h3 className=\"mb-2\">Row gap size {JSON.stringify(size)}</h3> {[1, 2, 3, 4, 5, 6].map((key) => ( <code>6</code> ))} ), )} <h3 className=\"mb-2\"> Responsive row gap size {'{\"xs\":\"medium\",\"md\":\"xlarge\"}'} </h3> {[1, 2, 3, 4, 5, 6].map((key) => ( <code>6</code> ))} Advanced Examples Complex Layout with Alignment <code>top aligned</code> <code>auto middle</code> <code>fill bottom</code> <code>stretch</code> Semantic HTML Tags <code>main content</code> <code>sidebar</code> All Column Sizes (1-12) {Array.from({ length: 12 }, (, i) => ( <code>{i + 1}</code> ))} API Grid React Props | Name | Type | Default | Description | | --------------- | ----------------------------------------------------------------------------------------- | ------- | ------------------------------------------ | | vAlign | \"stretch\" \\| \"start\" \\| \"end\" \\| \"center\" | - | Vertical alignment of grid items. | | hAlign | \"end\" \\| \"center\" \\| \"space-around\" \\| \"space-between\" | - | Horizontal alignment of grid items. | | tag | React.ElementType | \"div\" | Rendered HTML element. | | justifyHeight | boolean | false | Whether grid should justify height. | | rowGapSize | \"none\" \\| \"xsmall\" \\| \"small\" \\| \"medium\" \\| \"large\" \\| \"xlarge\" \\| Record<string, ...> | - | Row gap size, can be responsive object. | | columnGapSize | \"none\" \\| \"small\" \\| \"default\" \\| \"large\" \\| \"xlarge\" \\| Record<string, ...> | - | Column gap size, can be responsive object. | | className | string | - | Additional CSS classes. | GridCol React Props | Name | Type | Default | Description | | ----------- | ------------------- | ------- | --------------------------------------------- | | size | ResponsiveSize | - | Column size (1-12), can be responsive object. | | offset | ResponsiveSize | - | Column offset, can be responsive object. | | tag | React.ElementType | \"div\" | Rendered HTML element. | | className | string | - | Additional CSS classes. |"
169
169
  },
170
170
  {
171
171
  "href": "/components/hero",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangesk/orange-design-system",
3
- "version": "2.0.0-beta.43",
3
+ "version": "2.0.0-beta.44",
4
4
  "private": false,
5
5
  "engines": {
6
6
  "node": ">=20.x"
@@ -109,10 +109,10 @@
109
109
  "@types/react-dom": "19.2.3",
110
110
  "@types/wnumb": "^1.2.3",
111
111
  "@vitejs/plugin-react": "6.0.1",
112
- "@vitest/browser": "^4.1.4",
113
- "@vitest/browser-playwright": "^4.1.4",
114
- "@vitest/coverage-v8": "^4.1.4",
115
- "@vitest/ui": "^4.1.4",
112
+ "@vitest/browser": "^4.1.5",
113
+ "@vitest/browser-playwright": "^4.1.5",
114
+ "@vitest/coverage-v8": "^4.1.5",
115
+ "@vitest/ui": "^4.1.5",
116
116
  "canvas": "^3.2.3",
117
117
  "fs-extra": "^11.3.4",
118
118
  "glob": "13.0.6",
@@ -130,7 +130,7 @@
130
130
  "sass": "^1.99.0",
131
131
  "svg-sprite": "^2.0.4",
132
132
  "typescript": "6.0.3",
133
- "vitest": "^4.1.4",
133
+ "vitest": "^4.1.5",
134
134
  "vitest-axe": "^0.1.0",
135
135
  "vitest-browser-react": "^2.2.0"
136
136
  },
@@ -42,7 +42,7 @@
42
42
  width: 100%;
43
43
  display: flex;
44
44
  padding: space.get("large");
45
- gap: convert.to-rem(30px);
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
  }
@@ -61,6 +61,7 @@ export const defaultConfig: SwiperOptions = {
61
61
  },
62
62
  a11y: {
63
63
  enabled: true,
64
+ scrollOnFocus: false,
64
65
  prevSlideMessage: "Predchádzajúci snímok",
65
66
  nextSlideMessage: "Nasledujúci snímok",
66
67
  paginationBulletMessage: "Prejsť na snímok {index}",
@@ -97,6 +98,76 @@ export default class Carousel {
97
98
  private resizeRafId?: number;
98
99
  private boundWindowResizeHandler: () => void;
99
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
+ }
100
171
 
101
172
  constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
102
173
  this.element = element;
@@ -131,8 +202,12 @@ export default class Carousel {
131
202
  this.getCustomOptions();
132
203
  }
133
204
 
205
+ const isBleedRight = this.element.classList.contains(CLASS_BLEED_RIGHT);
206
+
134
207
  this.instance = new Swiper(this.viewport, {
135
208
  ...this.config,
209
+ // Swiper watchOverflow can mis-detect with slides offsets; manage bleed-right overflow ourselves.
210
+ watchOverflow: isBleedRight ? false : this.config.watchOverflow,
136
211
  enabled: false,
137
212
  modules: [Navigation, Pagination, Scrollbar, A11y, Keyboard, Mousewheel],
138
213
  on: {
@@ -237,9 +312,7 @@ export default class Carousel {
237
312
  */
238
313
  fixBleedRightScrollbar() {
239
314
  const updateScrollbar = () => {
240
- const viewportWrapper = this.element.querySelector(
241
- `.${CLASS_VIEWPORT_WRAPPER}`,
242
- ) as HTMLElement;
315
+ const viewportWrapper = this.getViewportWrapper();
243
316
  const scrollbar = this.instance?.scrollbar;
244
317
  const swiper = this.instance;
245
318
 
@@ -247,26 +320,18 @@ export default class Carousel {
247
320
  return;
248
321
  }
249
322
 
250
- const minTranslate = swiper.minTranslate();
251
- const maxTranslate = swiper.maxTranslate();
252
- const translateRange = Math.abs(maxTranslate - minTranslate);
323
+ const hasScrollableContent = this.hasScrollableContent(viewportWrapper);
253
324
 
254
- if (translateRange <= 1 || !swiper.enabled) {
255
- if (scrollbar.el) {
256
- scrollbar.el.style.display = "none";
257
- }
325
+ if (!hasScrollableContent) {
326
+ this.setScrollbarVisible(false);
258
327
  return;
259
328
  }
260
329
 
261
- if (scrollbar.el) {
262
- scrollbar.el.style.display = "";
263
- }
330
+ this.setScrollbarVisible(true);
264
331
 
265
332
  const scrollbarWidth = scrollbar.el.offsetWidth;
266
333
  const viewportWidth = viewportWrapper.clientWidth;
267
- const offsetBefore = Number(swiper.params.slidesOffsetBefore) || 0;
268
- const offsetAfter = Number(swiper.params.slidesOffsetAfter) || 0;
269
- const contentWidth = this.track.scrollWidth + offsetBefore + offsetAfter;
334
+ const contentWidth = this.getSlidesContentWidth();
270
335
 
271
336
  const visibleRatio =
272
337
  contentWidth > 0 ? Math.min(viewportWidth / contentWidth, 1) : 1;
@@ -348,21 +413,18 @@ export default class Carousel {
348
413
  private updateCarouselEnabledState(): void {
349
414
  if (!this.instance || !this.instance.params) return;
350
415
 
351
- const slidesCount = this.instance.slides.length;
352
- const slidesPerView = Number(this.instance.params.slidesPerView) || 1;
416
+ const viewportWrapper = this.getViewportWrapper();
417
+ const hasScrollableContent = this.hasScrollableContent(
418
+ viewportWrapper || undefined,
419
+ );
353
420
 
354
- if (slidesCount > slidesPerView) {
421
+ if (hasScrollableContent) {
355
422
  this.instance.enable();
356
423
  } else {
357
424
  this.instance.disable();
358
425
  }
359
426
 
360
- const scrollbarEl = this.element.querySelector(
361
- SELECTOR_SCROLLBAR,
362
- ) as HTMLElement | null;
363
- if (scrollbarEl) {
364
- scrollbarEl.style.display = this.instance.enabled ? "" : "none";
365
- }
427
+ this.setScrollbarVisible(this.instance.enabled);
366
428
  }
367
429
 
368
430
  private applyBleedInsets(viewportWrapper: HTMLElement): void {
@@ -370,42 +432,34 @@ export default class Carousel {
370
432
  return;
371
433
  }
372
434
 
373
- const viewportWidth =
374
- document.documentElement.clientWidth ||
375
- window.visualViewport?.width ||
376
- window.innerWidth;
377
-
378
- const rect = viewportWrapper.getBoundingClientRect();
379
- const insetLeft = Math.max(Math.round(rect.left), 0);
380
- const insetRight = Math.max(Math.round(viewportWidth - rect.right), 0);
381
- const insetAfter = insetRight;
382
-
383
- 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();
384
439
  const slidesCount = this.instance.slides.length;
385
- const shouldDisableBleed =
386
- viewportWidth >= 2560 || slidesCount <= slidesPerView;
440
+ const hasIntrinsicOverflow =
441
+ projectedContentWidth > 0
442
+ ? projectedOverflowWidth > Carousel.OVERFLOW_EPSILON_PX
443
+ : slidesCount > slidesPerView;
444
+ const shouldDisableBleed = !hasIntrinsicOverflow;
387
445
 
388
446
  this.element.classList.toggle(CLASS_BLEED_RIGHT, !shouldDisableBleed);
389
447
 
390
- if (shouldDisableBleed) {
391
- this.instance.params.slidesOffsetBefore = 0;
392
- this.instance.params.slidesOffsetAfter = 0;
393
- this.element.style.setProperty("--carousel-bleed-viewport-width", "100%");
394
- this.element.style.setProperty("--carousel-bleed-margin-left", "0px");
395
- } else {
396
- this.instance.params.slidesOffsetBefore = insetLeft;
397
- this.instance.params.slidesOffsetAfter = insetAfter;
398
- this.element.style.setProperty(
399
- "--carousel-bleed-viewport-width",
400
- `${Math.round(viewportWidth)}px`,
401
- );
402
- this.element.style.setProperty(
403
- "--carousel-bleed-margin-left",
404
- `${-insetLeft}px`,
405
- );
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;
406
454
  }
407
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
+
408
461
  this.instance.update();
462
+ this.updateCarouselEnabledState();
409
463
  }
410
464
 
411
465
  /**
@@ -418,9 +472,7 @@ export default class Carousel {
418
472
  requestAnimationFrame(() => {
419
473
  if (!this.instance) return;
420
474
 
421
- const viewportWrapper = this.element.querySelector(
422
- `.${CLASS_VIEWPORT_WRAPPER}`,
423
- ) as HTMLElement;
475
+ const viewportWrapper = this.getViewportWrapper();
424
476
  if (!viewportWrapper) return;
425
477
 
426
478
  const updateBleedState = () => {
@@ -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: hidden !important;
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");
@@ -691,7 +697,76 @@ describe("Carousel Static - Auto-Disable Feature", () => {
691
697
  });
692
698
  });
693
699
 
694
- it("should use right inset for bleed-right trailing offset", () => {
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", () => {
695
770
  const originalInnerWidth = window.innerWidth;
696
771
  Object.defineProperty(window, "innerWidth", {
697
772
  configurable: true,
@@ -726,6 +801,16 @@ describe("Carousel Static - Auto-Disable Feature", () => {
726
801
  const viewportWrapper = carouselElement.querySelector(
727
802
  ".carousel__viewport-wrapper",
728
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
+ });
729
814
 
730
815
  Object.defineProperty(viewportWrapper, "getBoundingClientRect", {
731
816
  configurable: true,
@@ -744,16 +829,17 @@ describe("Carousel Static - Auto-Disable Feature", () => {
744
829
 
745
830
  carouselInstance = new Carousel(carouselElement);
746
831
 
747
- expect(mockSwiperInstance.params.slidesOffsetBefore).toBe(120);
748
- expect(mockSwiperInstance.params.slidesOffsetAfter).toBe(80);
832
+ expect(mockSwiperInstance.params.slidesOffsetBefore).toBe(0);
833
+ expect(mockSwiperInstance.params.slidesOffsetAfter).toBe(0);
834
+ expect(mockSwiperInstance.params.width).toBe(800);
749
835
  expect(
750
836
  carouselElement.style.getPropertyValue(
751
837
  "--carousel-bleed-viewport-width",
752
838
  ),
753
- ).toBe("1000px");
839
+ ).toBe("100%");
754
840
  expect(
755
841
  carouselElement.style.getPropertyValue("--carousel-bleed-margin-left"),
756
- ).toBe("-120px");
842
+ ).toBe("0px");
757
843
 
758
844
  Object.defineProperty(window, "innerWidth", {
759
845
  configurable: true,
@@ -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
  );
@@ -51,6 +51,18 @@
51
51
  }
52
52
  }
53
53
 
54
+ &--placement-top {
55
+ &[open] {
56
+ display: flex;
57
+ flex-direction: column-reverse;
58
+
59
+ summary {
60
+ margin-top: space.get("large");
61
+ margin-bottom: 0;
62
+ }
63
+ }
64
+ }
65
+
54
66
  &[open] {
55
67
  svg[data-summary-icon] {
56
68
  transform: rotate(180deg) !important;
@@ -47,6 +47,10 @@ const example = (
47
47
  >
48
48
  Content
49
49
  </Expander>
50
+
51
+ <Expander summary="Open above trigger" placement="top">
52
+ Content
53
+ </Expander>
50
54
  </>
51
55
  );
52
56
 
@@ -184,4 +184,13 @@ describe("Expander", () => {
184
184
  );
185
185
  expect(getByTestId("test-id")).toHaveClass("expander--fullwidth");
186
186
  });
187
+
188
+ it("applies placement-top class when placement prop is top", () => {
189
+ const { getByTestId } = render(
190
+ <Expander data-testid="test-id" summary="Show details" placement="top">
191
+ Content
192
+ </Expander>,
193
+ );
194
+ expect(getByTestId("test-id")).toHaveClass("expander--placement-top");
195
+ });
187
196
  });
@@ -5,17 +5,20 @@ import { genResponsiveClasses } from "../../utils";
5
5
  export type GridVAlign = "stretch" | "start" | "end" | "center";
6
6
  export type GridHAlign = "end" | "center" | "space-around" | "space-between";
7
7
  export type GridRowGapSize =
8
+ | "none"
9
+ | "xsmall"
8
10
  | "small"
9
11
  | "medium"
10
12
  | "large"
11
13
  | "xlarge"
12
- | Record<string, "small" | "medium" | "large" | "xlarge">;
14
+ | Record<string, "none" | "xsmall" | "small" | "medium" | "large" | "xlarge">;
13
15
  export type GridColumnGapSize =
14
16
  | "none"
15
17
  | "small"
16
18
  | "default"
17
19
  | "large"
18
- | Record<string, "none" | "small" | "default" | "large">;
20
+ | "xlarge"
21
+ | Record<string, "none" | "small" | "default" | "large" | "xlarge">;
19
22
 
20
23
  export interface GridProps extends React.HTMLAttributes<HTMLElement> {
21
24
  vAlign?: GridVAlign;
@@ -2,12 +2,13 @@
2
2
 
3
3
  $grid-base: 12 !default;
4
4
 
5
- $grid-row-gap-sizes: ("small", "medium", "large", "xlarge");
6
- $grid-column-gap-sizes: ("none", "small", "default", "large");
5
+ $grid-row-gap-sizes: ("none", "xsmall", "small", "medium", "large", "xlarge");
6
+ $grid-column-gap-sizes: ("none", "small", "default", "large", "xlarge");
7
7
 
8
8
  $column-gap: (
9
9
  none: 0,
10
10
  small: space.get("xsmall"),
11
11
  default: space.get("small"),
12
12
  large: space.get("medium"),
13
+ xlarge: space.get("large"),
13
14
  );