vanilla-framework 4.44.0 → 4.45.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-framework",
3
- "version": "4.44.0",
3
+ "version": "4.45.0",
4
4
  "author": {
5
5
  "email": "webteam@canonical.com",
6
6
  "name": "Canonical Webteam"
@@ -97,6 +97,18 @@
97
97
  }
98
98
  }
99
99
 
100
+ %vf-pseudo-border--left {
101
+ position: relative;
102
+
103
+ &::before {
104
+ @extend %vf-pseudo-border;
105
+ bottom: 0;
106
+ height: auto;
107
+ top: 0;
108
+ width: 1px;
109
+ }
110
+ }
111
+
100
112
  %hr {
101
113
  background: $colors--theme--border-default;
102
114
  border: 0;
@@ -0,0 +1,440 @@
1
+ @import 'settings';
2
+
3
+ /*
4
+ @classreference
5
+ in-page-navigation:
6
+ Root element:
7
+ .p-in-page-navigation:
8
+ in-page navigation in default variant.
9
+ "&.is-sticky":
10
+ Sticky version of in-page navigation.
11
+
12
+ Dropdown toggle:
13
+ .p-in-page-navigation__dropdown-toggle:
14
+ Button to toggle between horizontal and vertical dropdown navigation on small/medium screens.
15
+ .p-in-page-navigation__dropdown-toggle-icon:
16
+ Chevron icon inside the dropdown toggle button.
17
+
18
+ Items list heading:
19
+ .p-in-page-navigation__heading:
20
+ Heading for in-page navigation items group (text only).
21
+
22
+ Items list:
23
+ .p-in-page-navigation__list:
24
+ Group of in-page navigation items (usually a `<ul>` element).
25
+
26
+ Item element:
27
+ .p-in-page-navigation__item:
28
+ Single item in in-page navigation (usually a `<li>` element). May contain nested items lists.
29
+
30
+ Item link:
31
+ .p-in-page-navigation__link:
32
+ Single link in the in-page navigation.
33
+ */
34
+
35
+ // Shared text styles mixin
36
+ @mixin vf-in-page-navigation-text {
37
+ @include vf-in-page-navigation-spacing-left;
38
+
39
+ padding-bottom: $spv--x-small;
40
+ padding-right: 0;
41
+ padding-top: $spv--x-small;
42
+ }
43
+
44
+ // Shared link styles for large screens and expanded dropdown
45
+ @mixin vf-in-page-navigation-link-vertical {
46
+ -webkit-box-orient: vertical;
47
+ display: -webkit-box;
48
+ -webkit-line-clamp: 2;
49
+ line-clamp: 2;
50
+ overflow: hidden;
51
+ text-overflow: ellipsis;
52
+ }
53
+
54
+ @mixin vf-p-in-page-navigation {
55
+ // Use a consistent border width for left-highlight and list border
56
+ $in-page-navigation-border-width: 3px;
57
+
58
+ // Scroll margins for headings within the parent grid section
59
+ .grid-row:has(.p-in-page-navigation) {
60
+ /* stylelint-disable selector-max-type */
61
+ h2,
62
+ h3,
63
+ h4 {
64
+ /* stylelint-enable selector-max-type */
65
+ scroll-margin-top: 72px;
66
+
67
+ @media screen and (width >= $threshold-4-8-col) {
68
+ scroll-margin-top: 24px;
69
+ }
70
+
71
+ // When the page has a sticky main navigation, increase scroll margin
72
+ // to account for the fixed header height. This prevents headings from
73
+ // being hidden behind the navigation when scrolling to anchors.
74
+ @at-root body:has(.p-navigation.is-sticky) &,
75
+ body:has(.p-navigation--sliding.is-sticky) & {
76
+ scroll-margin-top: 120px;
77
+
78
+ @media screen and (width >= $threshold-4-8-col) {
79
+ scroll-margin-top: 80px;
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ // Sticky styles for large screens
86
+ .p-in-page-navigation {
87
+ margin-bottom: $spv--strip-regular;
88
+ max-height: 100vh;
89
+ overflow-y: auto;
90
+ position: sticky;
91
+ top: $spv--strip-shallow;
92
+ }
93
+
94
+ // We re-enable scrolling when the dropdown is expanded on small/medium screens
95
+ // `is-expanded` class is toggled via JS
96
+ .p-in-page-navigation.is-expanded {
97
+ overflow-y: auto;
98
+ }
99
+
100
+ // Custom tooltip styles for scrollable navigation
101
+ // We dynamically calculate the position using JS
102
+ .p-in-page-navigation .p-tooltip--right {
103
+ position: relative;
104
+
105
+ .p-tooltip__message {
106
+ // Position will be set via JS using CSS custom properties
107
+ left: var(--tooltip-left, auto);
108
+ // Use fixed positioning to escape overflow clipping
109
+ position: fixed;
110
+ top: var(--tooltip-top, auto);
111
+ z-index: 100;
112
+ }
113
+ }
114
+
115
+ // Left hand border
116
+ .p-in-page-navigation__container > .p-in-page-navigation__list {
117
+ @extend %vf-pseudo-border--left;
118
+
119
+ &::before {
120
+ background-color: $colors--theme--border-low-contrast;
121
+ width: $in-page-navigation-border-width;
122
+ }
123
+ }
124
+
125
+ .p-in-page-navigation__list {
126
+ @extend %vf-list;
127
+ color: $colors--theme--text-inactive;
128
+
129
+ &:last-of-type::after {
130
+ content: none;
131
+ }
132
+ }
133
+
134
+ .p-in-page-navigation__heading {
135
+ @extend %common-default-text-properties;
136
+ @extend %small-caps-text;
137
+
138
+ display: block;
139
+ font-size: map-get($base-font-sizes, base);
140
+ font-weight: $font-weight-bold;
141
+ margin: 0;
142
+ max-width: none;
143
+ padding: $spv--x-small 0 $spv--small;
144
+ }
145
+
146
+ .p-in-page-navigation__link {
147
+ @include vf-in-page-navigation-text;
148
+ @include vf-in-page-navigation-link-vertical;
149
+ @include vf-focus-themed;
150
+
151
+ color: $colors--theme--text-muted;
152
+
153
+ &:visited {
154
+ color: $colors--theme--text-muted;
155
+ }
156
+
157
+ &:hover {
158
+ color: $colors--theme--text-default;
159
+ text-decoration: none;
160
+ }
161
+
162
+ // vf-highlight-bar is rendered above focus outline
163
+ // so we need to hide it on focus
164
+ &:focus::before {
165
+ display: none;
166
+ }
167
+
168
+ // Display the highlight when focussing in modern browsers that support
169
+ // focus-visible.
170
+ &:focus:not(:focus-visible)::before {
171
+ display: block;
172
+ }
173
+
174
+ // Active state for current navigation item
175
+ &.is-active,
176
+ &[aria-current='true'] {
177
+ @extend %vf-pseudo-border--left;
178
+
179
+ color: $colors--theme--text-default;
180
+ cursor: default;
181
+
182
+ &::before {
183
+ background-color: $colors--theme--text-default;
184
+ width: $in-page-navigation-border-width;
185
+ }
186
+ }
187
+ }
188
+
189
+ // Dropdown toggle button
190
+ .p-in-page-navigation__dropdown-toggle {
191
+ background-color: $colors--theme--background-default;
192
+ border: 0;
193
+ display: none;
194
+ line-height: map-get($icon-sizes, default);
195
+ margin: 0;
196
+ padding: $spv--large $sph--x-large;
197
+
198
+ &:focus {
199
+ @include vf-focus;
200
+ }
201
+
202
+ &:hover {
203
+ background: $colors--theme--background-default;
204
+ }
205
+
206
+ .p-in-page-navigation__dropdown-toggle-icon {
207
+ height: map-get($icon-sizes, default);
208
+ width: map-get($icon-sizes, default);
209
+ }
210
+ }
211
+
212
+ .p-in-page-navigation__container {
213
+ position: relative;
214
+ }
215
+
216
+ // Nested 2nd level of navigation indentation
217
+ .p-in-page-navigation__item .p-in-page-navigation__list .p-in-page-navigation__link {
218
+ // @media screen and (width >= $threshold-4-8-col) {
219
+ @include vf-in-page-navigation-spacing-left($multiplier: 2);
220
+ // }
221
+ }
222
+
223
+ // Small/medium screens
224
+ @media screen and (width < $threshold-4-8-col) {
225
+ .p-in-page-navigation {
226
+ background-color: $colors--theme--background-default;
227
+ margin-bottom: $spv--strip-shallow;
228
+ margin-left: (-$spv--x-large);
229
+ margin-right: (-$spv--x-large);
230
+ overflow-y: unset;
231
+ }
232
+
233
+ // Sticky styles for parent column
234
+ // IMPORTANT: Pattern requires a grid-col as a parent
235
+ [class^='grid-col']:has(> .p-in-page-navigation) {
236
+ position: sticky;
237
+ top: 0;
238
+ z-index: 1;
239
+
240
+ > .p-in-page-navigation {
241
+ position: static;
242
+ }
243
+ }
244
+
245
+ // Top level navigation list
246
+ .p-in-page-navigation__container > .p-in-page-navigation__list {
247
+ background-color: $colors--theme--background-default;
248
+ padding-left: $spv--large;
249
+
250
+ &::before {
251
+ left: $spv--large;
252
+ }
253
+
254
+ > .p-in-page-navigation__item {
255
+ scroll-margin-left: $spv--large;
256
+ }
257
+ }
258
+
259
+ // Hide heading unless dropdown is expanded
260
+ .p-in-page-navigation__heading {
261
+ display: none;
262
+ }
263
+
264
+ // Link styles for horizontal layout
265
+ .p-in-page-navigation__link {
266
+ display: inline-block;
267
+ line-height: $spv--x-large;
268
+ max-width: 33.33vw; // Limit link width to 1/3 viewport
269
+ overflow: hidden;
270
+ padding: $spv--medium $spv--large;
271
+ text-overflow: ellipsis;
272
+ vertical-align: middle;
273
+
274
+ // Adjust active state pseudo-border for horizontal layout
275
+ &.is-active,
276
+ &[aria-current='true'] {
277
+ &::before {
278
+ height: 50%;
279
+ top: 25%;
280
+ }
281
+ }
282
+ }
283
+
284
+ // Nested link indentation when expanded
285
+ .p-in-page-navigation.is-expanded .p-in-page-navigation__item .p-in-page-navigation__list .p-in-page-navigation__link {
286
+ @include vf-in-page-navigation-spacing-left($multiplier: 2);
287
+ }
288
+
289
+ // Container styles for horizontal layout
290
+ .p-in-page-navigation__container {
291
+ align-items: center;
292
+ background-color: $colors--theme--background-default;
293
+ display: grid;
294
+ grid-template-columns: 1fr auto;
295
+
296
+ // Full-width bottom border
297
+ &::after {
298
+ background-color: $colors--theme--border-low-contrast;
299
+ bottom: 0;
300
+ content: '';
301
+ height: 1px;
302
+ left: 50%;
303
+ position: absolute;
304
+ transform: translateX(-50%);
305
+ width: 100%;
306
+ }
307
+ }
308
+
309
+ .p-in-page-navigation__dropdown-toggle {
310
+ display: block;
311
+ grid-column: 2;
312
+ justify-self: end;
313
+ }
314
+
315
+ // Collapsed dropdown styles
316
+ .p-in-page-navigation:not(.is-expanded) .p-in-page-navigation__container {
317
+ .p-in-page-navigation__list {
318
+ grid-column: 1;
319
+ margin-bottom: 0;
320
+ min-width: 0;
321
+ overflow-x: auto;
322
+ overflow-y: hidden;
323
+ scroll-behavior: smooth;
324
+ scrollbar-width: none; // Firefox
325
+ white-space: nowrap;
326
+
327
+ &::-webkit-scrollbar {
328
+ display: none; // Chrome/Safari/Opera
329
+ }
330
+
331
+ // Remove left side border
332
+ &::before {
333
+ content: none;
334
+ }
335
+
336
+ .p-in-page-navigation__item {
337
+ display: inline-block;
338
+ }
339
+ }
340
+
341
+ // Hide nested lists
342
+ .p-in-page-navigation__item .p-in-page-navigation__list {
343
+ display: none;
344
+ }
345
+ }
346
+
347
+ // Expanded dropdown styles
348
+ // Grid layout: [list content | toggle button]
349
+ // Row 1: heading * | toggle
350
+ // Row 2: nav list |
351
+ // * If present, otherwise nav list is in row 1
352
+ .p-in-page-navigation.is-expanded .p-in-page-navigation__container {
353
+ align-items: start;
354
+ background-color: $colors--theme--background-alt;
355
+ display: grid;
356
+ grid-template-columns: 1fr auto;
357
+
358
+ > .p-in-page-navigation__list {
359
+ background-color: $colors--theme--background-alt;
360
+ grid-column: 1 / 2;
361
+ grid-row: 2;
362
+ margin-bottom: $spv--large;
363
+ }
364
+
365
+ .p-in-page-navigation__item .p-in-page-navigation__list {
366
+ display: block;
367
+ }
368
+
369
+ .p-in-page-navigation__heading {
370
+ display: block;
371
+ grid-column: 1 / 2;
372
+ grid-row: 1;
373
+ padding: $spv--medium $spv--large;
374
+ }
375
+
376
+ // Override for when there is no heading
377
+ &:not(:has(> .p-in-page-navigation__heading)) > .p-in-page-navigation__list {
378
+ grid-row: 1;
379
+ margin-top: $spv--medium;
380
+ }
381
+
382
+ .p-in-page-navigation__dropdown-toggle {
383
+ background-color: $colors--theme--background-alt;
384
+ display: block;
385
+ grid-column: 2 / 3;
386
+ grid-row: 1;
387
+ justify-self: end;
388
+ }
389
+ }
390
+
391
+ .p-in-page-navigation.is-expanded .p-in-page-navigation__link {
392
+ @include vf-in-page-navigation-text;
393
+ @include vf-in-page-navigation-link-vertical;
394
+
395
+ max-width: unset;
396
+ padding-right: $spv--large;
397
+
398
+ &.is-active,
399
+ &[aria-current='true'] {
400
+ &::before {
401
+ height: auto;
402
+ top: 0;
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ // Small screens only
409
+ @media screen and (width < $threshold-4-small-4-med-col) {
410
+ .p-in-page-navigation {
411
+ margin-left: (-$spv--large);
412
+ margin-right: (-$spv--large);
413
+ }
414
+
415
+ .p-in-page-navigation__link {
416
+ max-width: 50vw; // Limit link width to 1/2 viewport
417
+ }
418
+
419
+ .p-in-page-navigation__dropdown-toggle {
420
+ padding: $spv--large;
421
+ }
422
+ }
423
+ }
424
+
425
+ // Helper
426
+
427
+ // elements in side nav should be aligned from left based on grid margin for given screen size
428
+ // additional offset is added when icons are used or for nested navigation levels
429
+ @mixin vf-in-page-navigation-spacing-left(
430
+ // property to adjust spacing, defaults to `padding-left`
431
+ $prop: padding-left,
432
+ // how many times grid margin width should be multiplied (for nested navigation levels)
433
+ $multiplier: 1
434
+ ) {
435
+ #{$prop}: $multiplier * map-get($grid-margin-widths, small);
436
+
437
+ @media screen and (width >= $threshold-4-small-4-med-col) {
438
+ #{$prop}: $multiplier * map-get($grid-margin-widths, default);
439
+ }
440
+ }
@@ -15,7 +15,7 @@ $tooltip-overlay-top-border-radius: 0% 0% 100% 100%;
15
15
 
16
16
  &:focus,
17
17
  &:hover {
18
- .p-tooltip__message {
18
+ > .p-tooltip__message:first-of-type {
19
19
  display: inline;
20
20
  text-decoration: initial;
21
21
  }
@@ -105,7 +105,7 @@ $tooltip-overlay-top-border-radius: 0% 0% 100% 100%;
105
105
  .p-tooltip--btm-center {
106
106
  @extend %tooltip;
107
107
 
108
- .p-tooltip__message {
108
+ > .p-tooltip__message {
109
109
  bottom: inherit;
110
110
  left: 50%;
111
111
  top: 100%;
@@ -54,6 +54,7 @@
54
54
  @import 'patterns_separator';
55
55
  @import 'patterns_side-navigation-expandable';
56
56
  @import 'patterns_side-navigation';
57
+ @import 'patterns_in-page-navigation';
57
58
  @import 'patterns_slider';
58
59
  @import 'patterns_status-label';
59
60
  @import 'patterns_strip';
@@ -156,6 +157,7 @@
156
157
  @include vf-p-separator;
157
158
  @include vf-p-side-navigation;
158
159
  @include vf-p-side-navigation-expandable;
160
+ @include vf-p-in-page-navigation;
159
161
  @include vf-p-slider;
160
162
  @include vf-p-status-label;
161
163
  @include vf-p-strip;
@@ -0,0 +1,93 @@
1
+ {#
2
+ Parameters:
3
+ - title (string) (optional): The text to be displayed as the title. Defauts to None and doesn"t render.
4
+ - navigation_items (list<object>) (optional): A list of navigation items to be included in the in-page navigation. Each item is an object with the following properties:
5
+ - id (string) (required): The ID of the section to link to.
6
+ - text (string) (required): The text to be displayed for the navigation item.
7
+ - scope (string) (optional): Rendering mode for navigation items.
8
+ - "manual" (default): Render from `navigation_items`.
9
+ - "full-page": Autogenerate from headings via JS.
10
+ - primary_heading (string) (optional, requires scope="full-page"): The heading level to treat as primary. Accepts "h2" or "h3".
11
+ - secondary_heading (string) (optional, requires primary_heading): The heading level to treat as secondary. Accepts "h3" or "h4"
12
+ - excludes (list<string>) (optional): A list of exclusions for headings (only applies when scope="full-page").
13
+ Supports two formats:
14
+ - CSS selector (e.g. "#some-id", ".some-class"): excludes headings matching the selector.
15
+ - Text match (e.g. "text:Newsletter signup"): excludes headings whose text content matches (case-insensitive).
16
+ #}
17
+
18
+ {%- macro vf_in_page_navigation(title=None, navigation_items=[], scope="full-page", primary_heading="h2", secondary_heading=None, excludes=[]) -%}
19
+ {%- set nav_id = "in-page-navigation-list" -%}
20
+ {%- set valid_primary_headings = ["h2", "h3"] %}
21
+ {%- set valid_secondary_headings = ["h3", "h4"] %}
22
+ {%- set valid_scopes = ["full-page", "manual"] %}
23
+ {%- if scope not in valid_scopes -%}
24
+ {%- set scope = "full-page" -%}
25
+ {%- endif -%}
26
+ {%- set excludes_queries = excludes | join(",") + ",.p-in-page-navigation__heading" -%}
27
+
28
+ <div class="p-in-page-navigation"
29
+ data-in-page-navigation-scope="{{ scope }}"
30
+ {%- if primary_heading and primary_heading in valid_primary_headings -%} data-in-page-navigation-primary="{{ primary_heading }}" {%- if secondary_heading and secondary_heading in valid_secondary_headings -%} data-in-page-navigation-secondary="{{ secondary_heading }}" {%- endif -%} {%- else -%} data-in-page-navigation-primary="h2" {% endif %}
31
+ data-in-page-navigation-excludes="{{ excludes_queries }}">
32
+
33
+ <nav class="p-in-page-navigation__container"
34
+ aria-label="{% if title %}{{ title }}{% else %}In-page navigation{% endif %}">
35
+
36
+ {%- if title -%}
37
+ <h3 class="p-in-page-navigation__heading">{{ title | safe }}</h3>
38
+ {%- endif -%}
39
+ <ul class="p-in-page-navigation__list js-in-page-nav-list"
40
+ id="{{ nav_id }}"
41
+ aria-expanded="false">
42
+ {%- if scope != "full-page" -%}
43
+ {%- for item in navigation_items -%}
44
+ <li class="p-in-page-navigation__item p-tooltip--right"
45
+ aria-describedby="{{ item.id }}-tooltip">
46
+ <a class="p-in-page-navigation__link{% if loop.first %} is-active{% endif %}"
47
+ href="#{{ item.id }}">{{ item.text }}</a>
48
+ <span class="p-tooltip__message u-hide"
49
+ role="tooltip"
50
+ id="{{ item.id }}-tooltip">{{ item.text }}</span>
51
+ {%- if item.children -%}
52
+ <ul class="p-in-page-navigation__list">
53
+ {%- for child in item.children -%}
54
+ <li class="p-in-page-navigation__item p-tooltip--right"
55
+ aria-describedby="{{ child.id }}-tooltip">
56
+ <a class="p-in-page-navigation__link" href="#{{ child.id }}">{{ child.text }}</a>
57
+ <span class="p-tooltip__message u-hide"
58
+ role="tooltip"
59
+ id="{{ child.id }}-tooltip">{{ child.text }}</span>
60
+ </li>
61
+ {%- endfor -%}
62
+ </ul>
63
+ {%- endif -%}
64
+ </li>
65
+ {%- endfor -%}
66
+ {%- endif -%}
67
+ </ul>
68
+
69
+ <button class="p-in-page-navigation__dropdown-toggle"
70
+ aria-expanded="false"
71
+ aria-controls="{{ nav_id }}">
72
+ <i class="p-icon--chevron-down p-in-page-navigation__dropdown-toggle-icon"></i>
73
+ <i class="p-icon--chevron-up p-in-page-navigation__dropdown-toggle-icon u-hide"></i>
74
+ </button>
75
+
76
+ </nav>
77
+ </div>
78
+
79
+ {# Templates for JS auto-generation (used when scope="full-page") #}
80
+ {%- if scope == "full-page" -%}
81
+ <template class="js-in-page-nav-template-item">
82
+ <li class="p-in-page-navigation__item p-tooltip--right"
83
+ aria-describedby="#">
84
+ <a class="p-in-page-navigation__link" href="#"></a>
85
+ <span class="p-tooltip__message u-hide" role="tooltip" id="#"></span>
86
+ </li>
87
+ </template>
88
+ <template class="js-in-page-nav-template-sublist">
89
+ <ul class="p-in-page-navigation__list">
90
+ </ul>
91
+ </template>
92
+ {%- endif -%}
93
+ {%- endmacro -%}