@cdevhub/ngx-tw 0.2.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/LICENSE +21 -0
- package/README.md +134 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
- package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
- package/package.json +265 -0
- package/theme/_base.css +394 -0
- package/theme/_dark.css +510 -0
- package/theme/_high-contrast.css +290 -0
- package/theme/_semantic.css +317 -0
- package/theme/_typography.css +9 -0
- package/theme/index.css +54 -0
- package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
- package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
- package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
- package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
- package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
- package/types/cdevhub-ngx-tw-button.d.ts +74 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
- package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
- package/types/cdevhub-ngx-tw-card.d.ts +48 -0
- package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
- package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
- package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
- package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
- package/types/cdevhub-ngx-tw-core.d.ts +719 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
- package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
- package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
- package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
- package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
- package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
- package/types/cdevhub-ngx-tw-input.d.ts +110 -0
- package/types/cdevhub-ngx-tw-item.d.ts +70 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
- package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
- package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
- package/types/cdevhub-ngx-tw-select.d.ts +384 -0
- package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
- package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
- package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
- package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
- package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
- package/types/cdevhub-ngx-tw-split.d.ts +265 -0
- package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
- package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
- package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
- package/types/cdevhub-ngx-tw-table.d.ts +479 -0
- package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
- package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
- package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
- package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
- package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
- package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
- package/types/cdevhub-ngx-tw.d.ts +49 -0
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, inject, ElementRef, signal, computed, ChangeDetectionStrategy, Component, model, output, DestroyRef, viewChild, contentChildren, afterNextRender, isDevMode, effect, Directive } from '@angular/core';
|
|
3
|
+
import { tv } from 'tailwind-variants';
|
|
4
|
+
|
|
5
|
+
// `CarouselComponent` is the canonical "structural-layout primitive" exception
|
|
6
|
+
// to CLAUDE.md's 5–6 input cap (precedent: `SplitComponent`). The 17 inputs
|
|
7
|
+
// here are each an independent geometry/behavior axis (orientation, slides
|
|
8
|
+
// math, gap, loop, autoplay knobs, drag, keyboard, snap, active index, ARIA
|
|
9
|
+
// label sources, i18n) — removing any would force consumers to coordinate two
|
|
10
|
+
// carousels or replicate logic externally. Documented in
|
|
11
|
+
// docs/requirements/carousel.requirements.md § 2.2 and § 12.
|
|
12
|
+
/** Default English labels used when consumers do not override via the `labels` input. */
|
|
13
|
+
const DEFAULT_CAROUSEL_LABELS = {
|
|
14
|
+
previous: 'Previous slide',
|
|
15
|
+
next: 'Next slide',
|
|
16
|
+
pauseAutoplay: 'Pause autoplay',
|
|
17
|
+
resumeAutoplay: 'Resume autoplay',
|
|
18
|
+
indicator: 'Go to slide {page}',
|
|
19
|
+
slideOfWithLabel: '{index} of {total}: {label}',
|
|
20
|
+
slideOf: '{index} of {total}',
|
|
21
|
+
};
|
|
22
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
23
|
+
/** Replaces `{key}` placeholders in `template` with values from `vars`. Ported from paginator. */
|
|
24
|
+
function formatLabel(template, vars) {
|
|
25
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match);
|
|
26
|
+
}
|
|
27
|
+
/** Pixel value of the inter-slide gap, used to compute the slide-basis CSS variable. */
|
|
28
|
+
const GAP_PX = {
|
|
29
|
+
xs: 8,
|
|
30
|
+
sm: 12,
|
|
31
|
+
md: 16,
|
|
32
|
+
lg: 24,
|
|
33
|
+
xl: 32,
|
|
34
|
+
};
|
|
35
|
+
// ── Static color × state class lookups ──
|
|
36
|
+
// Tailwind v4 only resolves statically-written class names. Per the stepper
|
|
37
|
+
// precedent, every color × active/inactive indicator combination is enumerated.
|
|
38
|
+
// Active indicators are also distinguishable by more than color (WCAG 1.4.1):
|
|
39
|
+
// `dots` scale 1.5×; `lines` widen to w-12; `numbers` gain a filled background.
|
|
40
|
+
const INDICATOR_ACTIVE_DOTS = {
|
|
41
|
+
primary: 'bg-primary-solid scale-150',
|
|
42
|
+
secondary: 'bg-secondary-solid scale-150',
|
|
43
|
+
accent: 'bg-accent-solid scale-150',
|
|
44
|
+
neutral: 'bg-neutral-solid scale-150',
|
|
45
|
+
info: 'bg-info-solid scale-150',
|
|
46
|
+
success: 'bg-success-solid scale-150',
|
|
47
|
+
warning: 'bg-warning-solid scale-150',
|
|
48
|
+
error: 'bg-error-solid scale-150',
|
|
49
|
+
};
|
|
50
|
+
const INDICATOR_ACTIVE_LINES = {
|
|
51
|
+
primary: 'bg-primary-solid w-12',
|
|
52
|
+
secondary: 'bg-secondary-solid w-12',
|
|
53
|
+
accent: 'bg-accent-solid w-12',
|
|
54
|
+
neutral: 'bg-neutral-solid w-12',
|
|
55
|
+
info: 'bg-info-solid w-12',
|
|
56
|
+
success: 'bg-success-solid w-12',
|
|
57
|
+
warning: 'bg-warning-solid w-12',
|
|
58
|
+
error: 'bg-error-solid w-12',
|
|
59
|
+
};
|
|
60
|
+
const INDICATOR_ACTIVE_NUMBERS = {
|
|
61
|
+
primary: 'bg-primary-solid text-primary-solid-fg',
|
|
62
|
+
secondary: 'bg-secondary-solid text-secondary-solid-fg',
|
|
63
|
+
accent: 'bg-accent-solid text-accent-solid-fg',
|
|
64
|
+
neutral: 'bg-neutral-solid text-neutral-solid-fg',
|
|
65
|
+
info: 'bg-info-solid text-info-solid-fg',
|
|
66
|
+
success: 'bg-success-solid text-success-solid-fg',
|
|
67
|
+
warning: 'bg-warning-solid text-warning-solid-fg',
|
|
68
|
+
error: 'bg-error-solid text-error-solid-fg',
|
|
69
|
+
};
|
|
70
|
+
const INDICATOR_INACTIVE_DOTS = 'bg-fg-muted opacity-50 hover:opacity-100';
|
|
71
|
+
const INDICATOR_INACTIVE_LINES = 'bg-fg-muted opacity-50 hover:opacity-100';
|
|
72
|
+
const INDICATOR_INACTIVE_NUMBERS = 'bg-surface-muted text-fg hover:bg-surface-sunken';
|
|
73
|
+
function resolveIndicatorActiveClasses(variant, color) {
|
|
74
|
+
switch (variant) {
|
|
75
|
+
case 'dots':
|
|
76
|
+
return INDICATOR_ACTIVE_DOTS[color];
|
|
77
|
+
case 'lines':
|
|
78
|
+
return INDICATOR_ACTIVE_LINES[color];
|
|
79
|
+
case 'numbers':
|
|
80
|
+
return INDICATOR_ACTIVE_NUMBERS[color];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function resolveIndicatorInactiveClasses(variant) {
|
|
84
|
+
switch (variant) {
|
|
85
|
+
case 'dots':
|
|
86
|
+
return INDICATOR_INACTIVE_DOTS;
|
|
87
|
+
case 'lines':
|
|
88
|
+
return INDICATOR_INACTIVE_LINES;
|
|
89
|
+
case 'numbers':
|
|
90
|
+
return INDICATOR_INACTIVE_NUMBERS;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── tv() config ───────────────────────────────────────────────────
|
|
94
|
+
const carouselVariants = tv({
|
|
95
|
+
slots: {
|
|
96
|
+
// `relative` anchors the absolutely-positioned pause control and the
|
|
97
|
+
// overlay-position indicators.
|
|
98
|
+
root: 'relative flex w-full',
|
|
99
|
+
// The scrollable viewport. `tw-scrollbar-none` is the existing utility in
|
|
100
|
+
// theme/_base.css (the requirements doc names this `.tw-scrollbar-hidden`
|
|
101
|
+
// but the codebase ships `.tw-scrollbar-none` — reusing it).
|
|
102
|
+
viewport: 'flex min-w-0 w-full snap-mandatory scroll-smooth motion-reduce:scroll-auto tw-scrollbar-none ' +
|
|
103
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
104
|
+
// Per-slide. `flex-basis` is applied via host style binding on
|
|
105
|
+
// `CarouselSlideComponent` (reads `carousel.slideBasis()`).
|
|
106
|
+
slide: 'flex-none snap-always min-w-0 transition-opacity duration-150 motion-reduce:transition-none',
|
|
107
|
+
// Component-rendered pause control. `bg-overlay-control` /
|
|
108
|
+
// `hover:bg-overlay-control-hover` resolve through
|
|
109
|
+
// `--color-overlay-control{,-hover}` in `theme/_semantic.css`: a fixed
|
|
110
|
+
// translucent dark capsule whose contrast contract is against the
|
|
111
|
+
// *consumer's slide content underneath*, not the surface palette. See
|
|
112
|
+
// requirements § 12.
|
|
113
|
+
pauseControl: 'absolute z-10 inline-flex items-center justify-center size-6 rounded-full bg-overlay-control ' +
|
|
114
|
+
'text-white hover:bg-overlay-control-hover transition-colors duration-200 motion-reduce:transition-none ' +
|
|
115
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
116
|
+
indicators: 'flex items-center justify-center',
|
|
117
|
+
indicator: 'inline-block transition-colors duration-200 motion-reduce:transition-none cursor-pointer ' +
|
|
118
|
+
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
119
|
+
},
|
|
120
|
+
variants: {
|
|
121
|
+
orientation: {
|
|
122
|
+
horizontal: {
|
|
123
|
+
root: 'flex-col',
|
|
124
|
+
viewport: 'flex-row overflow-x-auto overflow-y-hidden snap-x',
|
|
125
|
+
indicators: 'flex-row',
|
|
126
|
+
pauseControl: 'bottom-2 start-2',
|
|
127
|
+
},
|
|
128
|
+
vertical: {
|
|
129
|
+
root: 'flex-row',
|
|
130
|
+
viewport: 'flex-col overflow-y-auto overflow-x-hidden snap-y',
|
|
131
|
+
indicators: 'flex-col',
|
|
132
|
+
pauseControl: 'top-2 start-2',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
snapAlign: {
|
|
136
|
+
start: { slide: 'snap-start' },
|
|
137
|
+
center: { slide: 'snap-center' },
|
|
138
|
+
end: { slide: 'snap-end' },
|
|
139
|
+
},
|
|
140
|
+
gap: {
|
|
141
|
+
xs: {},
|
|
142
|
+
sm: {},
|
|
143
|
+
md: {},
|
|
144
|
+
lg: {},
|
|
145
|
+
xl: {},
|
|
146
|
+
},
|
|
147
|
+
variant: {
|
|
148
|
+
dots: { indicator: 'rounded-full' },
|
|
149
|
+
lines: { indicator: 'rounded-full' },
|
|
150
|
+
numbers: {
|
|
151
|
+
indicator: 'rounded-md inline-flex items-center justify-center font-medium',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
size: {
|
|
155
|
+
xs: {},
|
|
156
|
+
sm: {},
|
|
157
|
+
md: {},
|
|
158
|
+
lg: {},
|
|
159
|
+
xl: {},
|
|
160
|
+
},
|
|
161
|
+
position: {
|
|
162
|
+
below: {},
|
|
163
|
+
overlay: {
|
|
164
|
+
// `bg-overlay-control` capsule — same translucent dark token as the
|
|
165
|
+
// pause control. See the `pauseControl` slot above for the rationale.
|
|
166
|
+
indicators: 'absolute z-10 px-2 py-1 rounded-full bg-overlay-control',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
compoundVariants: [
|
|
171
|
+
// Indicator geometry per variant × size.
|
|
172
|
+
{ variant: 'dots', size: 'xs', class: { indicator: 'size-2' } },
|
|
173
|
+
{ variant: 'dots', size: 'sm', class: { indicator: 'size-2.5' } },
|
|
174
|
+
{ variant: 'dots', size: 'md', class: { indicator: 'size-3' } },
|
|
175
|
+
{ variant: 'dots', size: 'lg', class: { indicator: 'size-3' } },
|
|
176
|
+
{ variant: 'dots', size: 'xl', class: { indicator: 'size-3' } },
|
|
177
|
+
{ variant: 'lines', size: 'xs', class: { indicator: 'h-1 w-4' } },
|
|
178
|
+
{ variant: 'lines', size: 'sm', class: { indicator: 'h-1 w-5' } },
|
|
179
|
+
{ variant: 'lines', size: 'md', class: { indicator: 'h-1.5 w-6' } },
|
|
180
|
+
{ variant: 'lines', size: 'lg', class: { indicator: 'h-1.5 w-8' } },
|
|
181
|
+
{ variant: 'lines', size: 'xl', class: { indicator: 'h-2 w-10' } },
|
|
182
|
+
{ variant: 'numbers', size: 'xs', class: { indicator: 'size-5 text-2xs' } },
|
|
183
|
+
{ variant: 'numbers', size: 'sm', class: { indicator: 'size-6 text-xs' } },
|
|
184
|
+
{ variant: 'numbers', size: 'md', class: { indicator: 'size-7 text-xs' } },
|
|
185
|
+
{ variant: 'numbers', size: 'lg', class: { indicator: 'size-8 text-sm' } },
|
|
186
|
+
{ variant: 'numbers', size: 'xl', class: { indicator: 'size-9 text-sm' } },
|
|
187
|
+
// Indicator gap per size — flex-direction is set by the orientation
|
|
188
|
+
// variant so plain `gap-*` works for both row and column.
|
|
189
|
+
{ size: 'xs', class: { indicators: 'gap-1' } },
|
|
190
|
+
{ size: 'sm', class: { indicators: 'gap-1.5' } },
|
|
191
|
+
{ size: 'md', class: { indicators: 'gap-2' } },
|
|
192
|
+
{ size: 'lg', class: { indicators: 'gap-2' } },
|
|
193
|
+
{ size: 'xl', class: { indicators: 'gap-3' } },
|
|
194
|
+
// "below" position spacing for indicators relative to the viewport.
|
|
195
|
+
{ orientation: 'horizontal', position: 'below', class: { indicators: 'mt-3' } },
|
|
196
|
+
{ orientation: 'vertical', position: 'below', class: { indicators: 'ms-3' } },
|
|
197
|
+
// Overlay placement — centered along the axis, offset from the edge.
|
|
198
|
+
{
|
|
199
|
+
orientation: 'horizontal',
|
|
200
|
+
position: 'overlay',
|
|
201
|
+
class: { indicators: 'bottom-3 start-1/2 -translate-x-1/2' },
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
orientation: 'vertical',
|
|
205
|
+
position: 'overlay',
|
|
206
|
+
class: { indicators: 'top-1/2 -translate-y-1/2 end-3' },
|
|
207
|
+
},
|
|
208
|
+
// Per-orientation flex gap on the viewport (slide spacing).
|
|
209
|
+
{ orientation: 'horizontal', gap: 'xs', class: { viewport: 'gap-x-2' } },
|
|
210
|
+
{ orientation: 'horizontal', gap: 'sm', class: { viewport: 'gap-x-3' } },
|
|
211
|
+
{ orientation: 'horizontal', gap: 'md', class: { viewport: 'gap-x-4' } },
|
|
212
|
+
{ orientation: 'horizontal', gap: 'lg', class: { viewport: 'gap-x-6' } },
|
|
213
|
+
{ orientation: 'horizontal', gap: 'xl', class: { viewport: 'gap-x-8' } },
|
|
214
|
+
{ orientation: 'vertical', gap: 'xs', class: { viewport: 'gap-y-2' } },
|
|
215
|
+
{ orientation: 'vertical', gap: 'sm', class: { viewport: 'gap-y-3' } },
|
|
216
|
+
{ orientation: 'vertical', gap: 'md', class: { viewport: 'gap-y-4' } },
|
|
217
|
+
{ orientation: 'vertical', gap: 'lg', class: { viewport: 'gap-y-6' } },
|
|
218
|
+
{ orientation: 'vertical', gap: 'xl', class: { viewport: 'gap-y-8' } },
|
|
219
|
+
],
|
|
220
|
+
defaultVariants: {
|
|
221
|
+
orientation: 'horizontal',
|
|
222
|
+
snapAlign: 'start',
|
|
223
|
+
gap: 'md',
|
|
224
|
+
variant: 'dots',
|
|
225
|
+
size: 'md',
|
|
226
|
+
position: 'below',
|
|
227
|
+
},
|
|
228
|
+
}, { twMerge: true });
|
|
229
|
+
// ── CarouselSlideComponent ────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* A single slide inside a `<tw-carousel>`. Projects arbitrary content via its
|
|
232
|
+
* default slot. Reports its visibility back to the carousel container via a
|
|
233
|
+
* shared `IntersectionObserver`; hidden slides receive `aria-hidden="true"` and
|
|
234
|
+
* the `inert` attribute so their focusable descendants are removed from the tab
|
|
235
|
+
* order.
|
|
236
|
+
*/
|
|
237
|
+
class CarouselSlideComponent {
|
|
238
|
+
/** Optional human-readable label for the slide. When provided, used in the slide's `aria-label` as `"{index + 1} of {total}: {label}"`. When `null`, uses `"{index + 1} of {total}"` only. Defaults to `null`. */
|
|
239
|
+
label = input(null, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
240
|
+
/** When `true`, the slide is rendered but skipped by Prev/Next, Indicators, keyboard nav, and autoplay. Programmatic `scrollTo` still lands on it. Visually muted (`opacity-50`, `cursor-not-allowed`). Defaults to `false`. */
|
|
241
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
242
|
+
/** @internal Direct parent carousel. A `tw-carousel-slide` outside a `tw-carousel` is a programmer error — the DI failure throws naturally. */
|
|
243
|
+
carousel = inject(CarouselComponent);
|
|
244
|
+
/** @internal Host element reference used by the carousel's `IntersectionObserver`. */
|
|
245
|
+
_hostEl = inject((ElementRef)).nativeElement;
|
|
246
|
+
/** @internal Visibility flag set by the carousel's IntersectionObserver. Initial value is `true` so the first paint does not flash `inert` on every slide. */
|
|
247
|
+
_isVisible = signal(true, ...(ngDevMode ? [{ debugName: "_isVisible" }] : /* istanbul ignore next */ []));
|
|
248
|
+
_isHidden = computed(() => !this._isVisible(), ...(ngDevMode ? [{ debugName: "_isHidden" }] : /* istanbul ignore next */ []));
|
|
249
|
+
/** @internal 0-based index in the parent carousel's slide content. -1 until contentChildren materialises. */
|
|
250
|
+
index = computed(() => this.carousel.slides().indexOf(this), ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
|
|
251
|
+
/** @internal Resolved `aria-label` per the `slideOf` / `slideOfWithLabel` template. */
|
|
252
|
+
accessibleName = computed(() => {
|
|
253
|
+
const labels = this.carousel.resolvedLabels();
|
|
254
|
+
const idx = this.index();
|
|
255
|
+
const total = this.carousel.slides().length;
|
|
256
|
+
const oneBasedIndex = idx < 0 ? 1 : idx + 1;
|
|
257
|
+
const label = this.label();
|
|
258
|
+
if (label !== null && label !== '') {
|
|
259
|
+
return formatLabel(labels.slideOfWithLabel, {
|
|
260
|
+
index: oneBasedIndex,
|
|
261
|
+
total,
|
|
262
|
+
label,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return formatLabel(labels.slideOf, {
|
|
266
|
+
index: oneBasedIndex,
|
|
267
|
+
total,
|
|
268
|
+
});
|
|
269
|
+
}, ...(ngDevMode ? [{ debugName: "accessibleName" }] : /* istanbul ignore next */ []));
|
|
270
|
+
slideClasses = computed(() => {
|
|
271
|
+
const base = this.carousel._slotClasses().slide();
|
|
272
|
+
return this.disabled() ? `${base} opacity-50 cursor-not-allowed` : base;
|
|
273
|
+
}, ...(ngDevMode ? [{ debugName: "slideClasses" }] : /* istanbul ignore next */ []));
|
|
274
|
+
/** @internal Per-slide `flex-basis`. Reads from the carousel so each slide sizes along the viewport's main axis (width for horizontal, height for vertical via `flex-direction`). */
|
|
275
|
+
_basis = computed(() => this.carousel.slideBasis(), ...(ngDevMode ? [{ debugName: "_basis" }] : /* istanbul ignore next */ []));
|
|
276
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselSlideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
277
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: CarouselSlideComponent, isStandalone: true, selector: "tw-carousel-slide", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-roledescription": "\"slide\"", "attr.aria-label": "accessibleName()", "attr.aria-hidden": "_isHidden() ? \"true\" : null", "attr.inert": "_isHidden() ? \"\" : null", "class": "slideClasses()", "style.flex-basis": "_basis()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
278
|
+
}
|
|
279
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselSlideComponent, decorators: [{
|
|
280
|
+
type: Component,
|
|
281
|
+
args: [{
|
|
282
|
+
selector: 'tw-carousel-slide',
|
|
283
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
284
|
+
host: {
|
|
285
|
+
role: 'group',
|
|
286
|
+
'[attr.aria-roledescription]': '"slide"',
|
|
287
|
+
'[attr.aria-label]': 'accessibleName()',
|
|
288
|
+
'[attr.aria-hidden]': '_isHidden() ? "true" : null',
|
|
289
|
+
'[attr.inert]': '_isHidden() ? "" : null',
|
|
290
|
+
'[class]': 'slideClasses()',
|
|
291
|
+
'[style.flex-basis]': '_basis()',
|
|
292
|
+
},
|
|
293
|
+
template: `<ng-content />`,
|
|
294
|
+
}]
|
|
295
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
296
|
+
// ── CarouselComponent ─────────────────────────────────────────────
|
|
297
|
+
/**
|
|
298
|
+
* Slide / swipe gallery primitive. Renders projected `<tw-carousel-slide>`
|
|
299
|
+
* children in a horizontally (or vertically) scrolling viewport with native
|
|
300
|
+
* CSS scroll-snap, optional autoplay, mouse-pointer drag, keyboard navigation,
|
|
301
|
+
* prev/next directive hosts, and an `<tw-carousel-indicators>` companion.
|
|
302
|
+
* Implements the W3C APG carousel pattern: `role="region"` +
|
|
303
|
+
* `aria-roledescription="carousel"` on the host; the inner viewport carries
|
|
304
|
+
* `tabindex="0"` and a polite `aria-live` region when autoplay is off.
|
|
305
|
+
*/
|
|
306
|
+
class CarouselComponent {
|
|
307
|
+
// ── Inputs (17, cap-exempt: structural-layout primitive — precedent SplitComponent) ──
|
|
308
|
+
/** Axis along which slides flow. `'horizontal'` is the canonical case; `'vertical'` is supported for tickers and feature lists. Defaults to `'horizontal'`. */
|
|
309
|
+
orientation = input('horizontal', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
|
|
310
|
+
/** Number of slides visible in the viewport. May be fractional (e.g. `1.2` for a "peek" of the next slide). Values below `0.5` are clamped to `0.5`; values above the slide count are clamped to the slide count. Defaults to `1`. */
|
|
311
|
+
slidesPerView = input(1, ...(ngDevMode ? [{ debugName: "slidesPerView" }] : /* istanbul ignore next */ []));
|
|
312
|
+
/** Number of slides advanced per navigation action (button click, keyboard, indicator). Non-integer values are floored. Defaults to `1`. */
|
|
313
|
+
slidesToScroll = input(1, ...(ngDevMode ? [{ debugName: "slidesToScroll" }] : /* istanbul ignore next */ []));
|
|
314
|
+
/** Inter-slide gap on the scroll axis. Mapped via the canonical spacing scale. Defaults to `'md'`. */
|
|
315
|
+
gap = input('md', ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
|
|
316
|
+
/** When `true`, navigation wraps around at the boundaries. Prev at slide 0 jumps to the last page; Next at the last page jumps to slide 0. Implementation is jumpless via a brief opacity mask. Defaults to `false`. */
|
|
317
|
+
loop = input(false, ...(ngDevMode ? [{ debugName: "loop" }] : /* istanbul ignore next */ []));
|
|
318
|
+
/** When `true`, the carousel auto-advances by `slidesToScroll` every `autoplayInterval` ms. Pauses on hover, focus-in, drag, document hidden, or user interaction. Defaults to `false`. */
|
|
319
|
+
autoplay = input(false, ...(ngDevMode ? [{ debugName: "autoplay" }] : /* istanbul ignore next */ []));
|
|
320
|
+
/** Milliseconds between autoplay advances. Values below `1000` are clamped to `1000` per WCAG 2.2.2. Defaults to `5000`. */
|
|
321
|
+
autoplayInterval = input(5000, ...(ngDevMode ? [{ debugName: "autoplayInterval" }] : /* istanbul ignore next */ []));
|
|
322
|
+
/** Pauses autoplay while the pointer is over the container. Defaults to `true` because losing autoplay on hover is the expected gallery behavior — opt-out is the special case. */
|
|
323
|
+
pauseOnHover = input(true, ...(ngDevMode ? [{ debugName: "pauseOnHover" }] : /* istanbul ignore next */ []));
|
|
324
|
+
/** Pauses autoplay while keyboard focus is anywhere inside the container. Defaults to `true` for WCAG 2.2.2 compliance — opt-out is the special case. */
|
|
325
|
+
pauseOnFocusIn = input(true, ...(ngDevMode ? [{ debugName: "pauseOnFocusIn" }] : /* istanbul ignore next */ []));
|
|
326
|
+
/** When `true`, the user may pan the slides via mouse pointer drag; touch is left to native scroll. Pointer events are intercepted only when the drag exceeds a 6-pixel threshold so clicks inside slides still work. Defaults to `true` because galleries are draggable by user expectation — opt-out is the special case. */
|
|
327
|
+
draggable = input(true, ...(ngDevMode ? [{ debugName: "draggable" }] : /* istanbul ignore next */ []));
|
|
328
|
+
/** When `true`, the viewport responds to Arrow / Home / End / PageUp / PageDown when focus is inside it. Defaults to `true` for keyboard accessibility — opt-out is the special case. */
|
|
329
|
+
keyboard = input(true, ...(ngDevMode ? [{ debugName: "keyboard" }] : /* istanbul ignore next */ []));
|
|
330
|
+
/** CSS `scroll-snap-align` value applied to each slide. `'start'` is the standard gallery behavior; `'center'` is used for peek/preview layouts. Defaults to `'start'`. */
|
|
331
|
+
snapAlign = input('start', ...(ngDevMode ? [{ debugName: "snapAlign" }] : /* istanbul ignore next */ []));
|
|
332
|
+
/** Two-way bound 0-based index of the first visible slide in the current page. Setting from the parent scrolls the viewport smoothly to align that slide; reading reflects user-driven scroll position after `scrollend`. Defaults to `0`. */
|
|
333
|
+
activeIndex = model(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
334
|
+
/** Accessible name for the carousel region. If both this and `ariaLabelledBy` are `null`, a one-time dev-mode `console.warn` is logged (production builds never log). Defaults to `null`. */
|
|
335
|
+
ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
336
|
+
/** ID of an element labeling the carousel. Either `ariaLabel` or `ariaLabelledBy` SHOULD be provided. Defaults to `null`. */
|
|
337
|
+
ariaLabelledBy = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledBy" }] : /* istanbul ignore next */ []));
|
|
338
|
+
/** Localizable strings for prev/next/pause/resume/indicator/slide-of templates. Unset keys fall back to the English defaults in `DEFAULT_CAROUSEL_LABELS`. Defaults to `{}`. */
|
|
339
|
+
labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
|
|
340
|
+
// ── Outputs ───────────────────────────────────────────────────
|
|
341
|
+
/** Fires when the active index changes. Payload identifies the previous and new index plus the trigger source. */
|
|
342
|
+
slideChange = output();
|
|
343
|
+
/** Fires when autoplay transitions from running to paused. Payload is the pause reason. */
|
|
344
|
+
autoplayPaused = output();
|
|
345
|
+
/** Fires when autoplay transitions from paused to running. */
|
|
346
|
+
autoplayResumed = output();
|
|
347
|
+
// ── Internal references ────────────────────────────────────────
|
|
348
|
+
_destroyRef = inject(DestroyRef);
|
|
349
|
+
/** @internal The scrollable viewport element. */
|
|
350
|
+
_viewportRef = viewChild.required('viewport');
|
|
351
|
+
/** @internal Projected slides in DOM order. */
|
|
352
|
+
slides = contentChildren(CarouselSlideComponent, { ...(ngDevMode ? { debugName: "slides" } : /* istanbul ignore next */ {}), descendants: false });
|
|
353
|
+
// ── Resolved configuration ─────────────────────────────────────
|
|
354
|
+
/** @internal Resolved label record with English defaults filled in. */
|
|
355
|
+
resolvedLabels = computed(() => ({
|
|
356
|
+
...DEFAULT_CAROUSEL_LABELS,
|
|
357
|
+
...this.labels(),
|
|
358
|
+
}), ...(ngDevMode ? [{ debugName: "resolvedLabels" }] : /* istanbul ignore next */ []));
|
|
359
|
+
/** @internal Effective `slidesPerView` clamped to `[0.5, slideCount]`. */
|
|
360
|
+
_effectiveSlidesPerView = computed(() => {
|
|
361
|
+
const count = this.slides().length;
|
|
362
|
+
const raw = this.slidesPerView();
|
|
363
|
+
if (count === 0)
|
|
364
|
+
return Math.max(0.5, raw);
|
|
365
|
+
return Math.max(0.5, Math.min(raw, count));
|
|
366
|
+
}, ...(ngDevMode ? [{ debugName: "_effectiveSlidesPerView" }] : /* istanbul ignore next */ []));
|
|
367
|
+
/** @internal Effective `slidesToScroll` floored to a positive integer. Read by child indicators directive (`CarouselIndicatorsComponent._onClick`) as a signal. */
|
|
368
|
+
_effectiveSlidesToScroll = computed(() => {
|
|
369
|
+
return Math.max(1, Math.floor(this.slidesToScroll()));
|
|
370
|
+
}, ...(ngDevMode ? [{ debugName: "_effectiveSlidesToScroll" }] : /* istanbul ignore next */ []));
|
|
371
|
+
/** @internal Autoplay interval clamped to ≥ 1000 ms per WCAG 2.2.2. */
|
|
372
|
+
_effectiveAutoplayInterval = computed(() => {
|
|
373
|
+
return Math.max(1000, this.autoplayInterval());
|
|
374
|
+
}, ...(ngDevMode ? [{ debugName: "_effectiveAutoplayInterval" }] : /* istanbul ignore next */ []));
|
|
375
|
+
/** Number of distinct pages (groups of `slidesToScroll` slides) the carousel can land on. Reactive. */
|
|
376
|
+
pageCount = computed(() => {
|
|
377
|
+
const count = this.slides().length;
|
|
378
|
+
if (count === 0)
|
|
379
|
+
return 1;
|
|
380
|
+
const perView = this._effectiveSlidesPerView();
|
|
381
|
+
const toScroll = this._effectiveSlidesToScroll();
|
|
382
|
+
return Math.max(1, Math.ceil((count - perView) / toScroll) + 1);
|
|
383
|
+
}, ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
|
|
384
|
+
/** 0-based page index that contains `activeIndex`. Reactive. */
|
|
385
|
+
activePage = computed(() => {
|
|
386
|
+
return Math.floor(this.activeIndex() / this._effectiveSlidesToScroll());
|
|
387
|
+
}, ...(ngDevMode ? [{ debugName: "activePage" }] : /* istanbul ignore next */ []));
|
|
388
|
+
/** The highest slide index the carousel can land on given the current `slidesPerView`. Equals `max(0, slideCount - ceil(effectiveSlidesPerView))`. Reactive. Every site that asks "where is the last legal landing position?" goes through this signal. */
|
|
389
|
+
lastReachableIndex = computed(() => {
|
|
390
|
+
const count = this.slides().length;
|
|
391
|
+
if (count === 0)
|
|
392
|
+
return 0;
|
|
393
|
+
return Math.max(0, count - Math.ceil(this._effectiveSlidesPerView()));
|
|
394
|
+
}, ...(ngDevMode ? [{ debugName: "lastReachableIndex" }] : /* istanbul ignore next */ []));
|
|
395
|
+
/** @internal `true` when the carousel is at slide 0 (drives prev directive `disabled`). */
|
|
396
|
+
isAtStart = computed(() => this.activeIndex() <= 0, ...(ngDevMode ? [{ debugName: "isAtStart" }] : /* istanbul ignore next */ []));
|
|
397
|
+
/** @internal `true` when the carousel is at the last reachable slide (drives next directive `disabled`). */
|
|
398
|
+
isAtEnd = computed(() => this.activeIndex() >= this.lastReachableIndex(), ...(ngDevMode ? [{ debugName: "isAtEnd" }] : /* istanbul ignore next */ []));
|
|
399
|
+
/** @internal Per-slide `flex-basis`. Read by each `CarouselSlideComponent` host style binding to size slides along the viewport's main axis. Reactive. */
|
|
400
|
+
slideBasis = computed(() => {
|
|
401
|
+
const perView = this._effectiveSlidesPerView();
|
|
402
|
+
const gapPx = GAP_PX[this.gap()];
|
|
403
|
+
// calc((100% - ((perView - 1) * gap-px)) / perView)
|
|
404
|
+
return `calc((100% - ${(perView - 1) * gapPx}px) / ${perView})`;
|
|
405
|
+
}, ...(ngDevMode ? [{ debugName: "slideBasis" }] : /* istanbul ignore next */ []));
|
|
406
|
+
// ── tv() resolution ────────────────────────────────────────────
|
|
407
|
+
_variantResult = computed(() => carouselVariants({
|
|
408
|
+
orientation: this.orientation(),
|
|
409
|
+
snapAlign: this.snapAlign(),
|
|
410
|
+
gap: this.gap(),
|
|
411
|
+
}), ...(ngDevMode ? [{ debugName: "_variantResult" }] : /* istanbul ignore next */ []));
|
|
412
|
+
/** @internal Slot-class accessor used by child slides and indicators. */
|
|
413
|
+
_slotClasses = computed(() => this._variantResult(), ...(ngDevMode ? [{ debugName: "_slotClasses" }] : /* istanbul ignore next */ []));
|
|
414
|
+
rootClasses = computed(() => this._variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
415
|
+
viewportClasses = computed(() => this._variantResult().viewport(), ...(ngDevMode ? [{ debugName: "viewportClasses" }] : /* istanbul ignore next */ []));
|
|
416
|
+
pauseControlClasses = computed(() => this._variantResult().pauseControl(), ...(ngDevMode ? [{ debugName: "pauseControlClasses" }] : /* istanbul ignore next */ []));
|
|
417
|
+
// ── State signals ──────────────────────────────────────────────
|
|
418
|
+
/** @internal Last interaction source — written before any navigation triggers scroll, read by the settle handler. */
|
|
419
|
+
_lastInteractionSource = signal(null, ...(ngDevMode ? [{ debugName: "_lastInteractionSource" }] : /* istanbul ignore next */ []));
|
|
420
|
+
/** @internal Suppresses the scroll-end recompute during a loop-jump. */
|
|
421
|
+
_isLoopJumping = signal(false, ...(ngDevMode ? [{ debugName: "_isLoopJumping" }] : /* istanbul ignore next */ []));
|
|
422
|
+
_isHovered = signal(false, ...(ngDevMode ? [{ debugName: "_isHovered" }] : /* istanbul ignore next */ []));
|
|
423
|
+
_hasFocusInside = signal(false, ...(ngDevMode ? [{ debugName: "_hasFocusInside" }] : /* istanbul ignore next */ []));
|
|
424
|
+
_isTabVisible = signal(true, ...(ngDevMode ? [{ debugName: "_isTabVisible" }] : /* istanbul ignore next */ []));
|
|
425
|
+
_isDragging = signal(false, ...(ngDevMode ? [{ debugName: "_isDragging" }] : /* istanbul ignore next */ []));
|
|
426
|
+
_isManuallyPaused = signal(false, ...(ngDevMode ? [{ debugName: "_isManuallyPaused" }] : /* istanbul ignore next */ []));
|
|
427
|
+
_postInteractionPauseUntil = signal(null, ...(ngDevMode ? [{ debugName: "_postInteractionPauseUntil" }] : /* istanbul ignore next */ []));
|
|
428
|
+
/** @internal RTL sign factor applied to scroll-position math. `+1` LTR, `-1` RTL. */
|
|
429
|
+
_rtlSign = signal(1, ...(ngDevMode ? [{ debugName: "_rtlSign" }] : /* istanbul ignore next */ []));
|
|
430
|
+
// ── Resolved pause-control labels ──────────────────────────────
|
|
431
|
+
_pauseControlAriaLabel = computed(() => {
|
|
432
|
+
const labels = this.resolvedLabels();
|
|
433
|
+
return this._isManuallyPaused() ? labels.resumeAutoplay : labels.pauseAutoplay;
|
|
434
|
+
}, ...(ngDevMode ? [{ debugName: "_pauseControlAriaLabel" }] : /* istanbul ignore next */ []));
|
|
435
|
+
/** @internal Public accessor for the template. */
|
|
436
|
+
_isManuallyPausedView = this._isManuallyPaused.asReadonly();
|
|
437
|
+
// ── Internal API used by child directives / indicators ─────────
|
|
438
|
+
/**
|
|
439
|
+
* @internal Sets the "last interaction source" signal so the upcoming
|
|
440
|
+
* navigation call emits `slideChange` with the correct trigger. Called by
|
|
441
|
+
* `[twCarouselPrev]`, `[twCarouselNext]`, and the indicators before they
|
|
442
|
+
* invoke `next()` / `prev()` / `scrollTo()`.
|
|
443
|
+
*/
|
|
444
|
+
_setLastInteractionSource(source) {
|
|
445
|
+
this._lastInteractionSource.set(source);
|
|
446
|
+
}
|
|
447
|
+
// ── Lifecycle ──────────────────────────────────────────────────
|
|
448
|
+
constructor() {
|
|
449
|
+
// One-time dev warnings.
|
|
450
|
+
afterNextRender(() => {
|
|
451
|
+
this._setupViewport();
|
|
452
|
+
});
|
|
453
|
+
// Dev-mode warnings. Each one is deduped per component instance so they
|
|
454
|
+
// re-evaluate when consumer inputs change but never spam the console. The
|
|
455
|
+
// earlier implementation used `afterNextRender` which fires once at
|
|
456
|
+
// construction — if a consumer later set `slidesPerView = 0.1`, the
|
|
457
|
+
// threshold warning never surfaced (audit (i)).
|
|
458
|
+
if (isDevMode()) {
|
|
459
|
+
let warnedMissingLabel = false;
|
|
460
|
+
let warnedLowInterval = false;
|
|
461
|
+
let warnedLowPerView = false;
|
|
462
|
+
effect(() => {
|
|
463
|
+
if (!warnedMissingLabel &&
|
|
464
|
+
this.ariaLabel() === null &&
|
|
465
|
+
this.ariaLabelledBy() === null) {
|
|
466
|
+
warnedMissingLabel = true;
|
|
467
|
+
console.warn('[tw-carousel] Provide either `ariaLabel` or `ariaLabelledBy` so the carousel region has an accessible name (WCAG 4.1.2).');
|
|
468
|
+
}
|
|
469
|
+
if (!warnedLowInterval && this.autoplayInterval() < 1000) {
|
|
470
|
+
warnedLowInterval = true;
|
|
471
|
+
console.warn(`[tw-carousel] autoplayInterval=${this.autoplayInterval()} is below the 1000 ms WCAG 2.2.2 floor; clamped to 1000.`);
|
|
472
|
+
}
|
|
473
|
+
if (!warnedLowPerView && this.slidesPerView() < 0.5) {
|
|
474
|
+
warnedLowPerView = true;
|
|
475
|
+
console.warn(`[tw-carousel] slidesPerView=${this.slidesPerView()} is below the 0.5 floor; clamped.`);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// Autoplay state machine — start/stop the interval based on inputs + pause state.
|
|
480
|
+
effect((onCleanup) => {
|
|
481
|
+
const autoplay = this.autoplay();
|
|
482
|
+
const paused = this._isPaused();
|
|
483
|
+
const interval = this._effectiveAutoplayInterval();
|
|
484
|
+
if (!autoplay || paused)
|
|
485
|
+
return;
|
|
486
|
+
const handle = setInterval(() => {
|
|
487
|
+
this._lastInteractionSource.set('autoplay');
|
|
488
|
+
this.next();
|
|
489
|
+
}, interval);
|
|
490
|
+
onCleanup(() => clearInterval(handle));
|
|
491
|
+
});
|
|
492
|
+
// Pause/resume transition emissions. Track previous state across effect
|
|
493
|
+
// invocations and emit on actual transitions while autoplay is on.
|
|
494
|
+
// The initial `false` is correct ONLY because `autoplay` defaults to
|
|
495
|
+
// `false`: the first effect run sees `autoplay=false` and short-circuits
|
|
496
|
+
// without emitting, regardless of the pause state. If the default ever
|
|
497
|
+
// flips to `true`, this needs rethinking (audit (f)).
|
|
498
|
+
let prevPaused = false;
|
|
499
|
+
effect(() => {
|
|
500
|
+
const autoplay = this.autoplay();
|
|
501
|
+
const paused = this._isPaused();
|
|
502
|
+
if (autoplay) {
|
|
503
|
+
if (paused && !prevPaused) {
|
|
504
|
+
const reason = this._currentPauseReason();
|
|
505
|
+
if (reason !== null)
|
|
506
|
+
this.autoplayPaused.emit(reason);
|
|
507
|
+
}
|
|
508
|
+
else if (!paused && prevPaused) {
|
|
509
|
+
this.autoplayResumed.emit();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
prevPaused = paused;
|
|
513
|
+
});
|
|
514
|
+
// Slide-set changes: re-observe with IntersectionObserver, clamp activeIndex.
|
|
515
|
+
effect(() => {
|
|
516
|
+
const slides = this.slides();
|
|
517
|
+
this._reobserveSlides(slides);
|
|
518
|
+
const count = slides.length;
|
|
519
|
+
if (count > 0 && this.activeIndex() > count - 1) {
|
|
520
|
+
this.activeIndex.set(count - 1);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
this._destroyRef.onDestroy(() => {
|
|
524
|
+
this._teardownViewport();
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
// ── Pause state aggregation ────────────────────────────────────
|
|
528
|
+
_isPaused = computed(() => {
|
|
529
|
+
if (this.pauseOnHover() && this._isHovered())
|
|
530
|
+
return true;
|
|
531
|
+
if (this.pauseOnFocusIn() && this._hasFocusInside())
|
|
532
|
+
return true;
|
|
533
|
+
if (!this._isTabVisible())
|
|
534
|
+
return true;
|
|
535
|
+
if (this._isDragging())
|
|
536
|
+
return true;
|
|
537
|
+
if (this._isManuallyPaused())
|
|
538
|
+
return true;
|
|
539
|
+
const until = this._postInteractionPauseUntil();
|
|
540
|
+
if (until !== null && Date.now() < until)
|
|
541
|
+
return true;
|
|
542
|
+
return false;
|
|
543
|
+
}, ...(ngDevMode ? [{ debugName: "_isPaused" }] : /* istanbul ignore next */ []));
|
|
544
|
+
_currentPauseReason() {
|
|
545
|
+
if (this._isManuallyPaused())
|
|
546
|
+
return 'manual';
|
|
547
|
+
if (!this._isTabVisible())
|
|
548
|
+
return 'visibility';
|
|
549
|
+
if (this._isDragging())
|
|
550
|
+
return 'interaction';
|
|
551
|
+
const until = this._postInteractionPauseUntil();
|
|
552
|
+
if (until !== null && Date.now() < until)
|
|
553
|
+
return 'interaction';
|
|
554
|
+
if (this.pauseOnFocusIn() && this._hasFocusInside())
|
|
555
|
+
return 'focus';
|
|
556
|
+
if (this.pauseOnHover() && this._isHovered())
|
|
557
|
+
return 'hover';
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
// ── Viewport / observer setup ───────────────────────────────────
|
|
561
|
+
_intersectionObserver = null;
|
|
562
|
+
_resizeObserver = null;
|
|
563
|
+
_onScrollEndBound = null;
|
|
564
|
+
_onScrollDebouncedBound = null;
|
|
565
|
+
_scrollDebounceTimer = null;
|
|
566
|
+
_onVisibilityChangeBound = null;
|
|
567
|
+
_setupViewport() {
|
|
568
|
+
const viewport = this._viewportRef().nativeElement;
|
|
569
|
+
// RTL detection (read once; consumers needing dynamic dir changes can
|
|
570
|
+
// re-render the host).
|
|
571
|
+
try {
|
|
572
|
+
const direction = getComputedStyle(viewport).direction;
|
|
573
|
+
this._rtlSign.set(direction === 'rtl' ? -1 : 1);
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
this._rtlSign.set(1);
|
|
577
|
+
}
|
|
578
|
+
// Active-index detection: scrollend if available, debounced scroll fallback.
|
|
579
|
+
const hasScrollEnd = 'onscrollend' in window;
|
|
580
|
+
if (hasScrollEnd) {
|
|
581
|
+
this._onScrollEndBound = () => this._handleScrollSettled();
|
|
582
|
+
viewport.addEventListener('scrollend', this._onScrollEndBound, { passive: true });
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
this._onScrollDebouncedBound = () => {
|
|
586
|
+
if (this._scrollDebounceTimer !== null) {
|
|
587
|
+
clearTimeout(this._scrollDebounceTimer);
|
|
588
|
+
}
|
|
589
|
+
this._scrollDebounceTimer = setTimeout(() => {
|
|
590
|
+
this._scrollDebounceTimer = null;
|
|
591
|
+
this._handleScrollSettled();
|
|
592
|
+
}, 150);
|
|
593
|
+
};
|
|
594
|
+
viewport.addEventListener('scroll', this._onScrollDebouncedBound, {
|
|
595
|
+
passive: true,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
// Tab visibility for autoplay gating.
|
|
599
|
+
if (typeof document !== 'undefined') {
|
|
600
|
+
this._isTabVisible.set(document.visibilityState !== 'hidden');
|
|
601
|
+
this._onVisibilityChangeBound = () => {
|
|
602
|
+
this._isTabVisible.set(document.visibilityState !== 'hidden');
|
|
603
|
+
};
|
|
604
|
+
document.addEventListener('visibilitychange', this._onVisibilityChangeBound, {
|
|
605
|
+
passive: true,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
// Observers for slide visibility and viewport-size changes.
|
|
609
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
610
|
+
this._intersectionObserver = new IntersectionObserver((entries) => {
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
const slide = entry.target
|
|
613
|
+
._twSlide;
|
|
614
|
+
if (slide) {
|
|
615
|
+
slide._isVisible.set(entry.isIntersecting && entry.intersectionRatio >= 0.5);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}, { root: viewport, threshold: 0.5 });
|
|
619
|
+
this._reobserveSlides(this.slides());
|
|
620
|
+
}
|
|
621
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
622
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
623
|
+
// Reflow: keep activeIndex aligned to its slide position.
|
|
624
|
+
this._scrollToIndex(this.activeIndex(), 'instant');
|
|
625
|
+
});
|
|
626
|
+
this._resizeObserver.observe(viewport);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
_teardownViewport() {
|
|
630
|
+
const viewport = this._viewportRef()?.nativeElement;
|
|
631
|
+
if (viewport && this._onScrollEndBound) {
|
|
632
|
+
viewport.removeEventListener('scrollend', this._onScrollEndBound);
|
|
633
|
+
}
|
|
634
|
+
if (viewport && this._onScrollDebouncedBound) {
|
|
635
|
+
viewport.removeEventListener('scroll', this._onScrollDebouncedBound);
|
|
636
|
+
}
|
|
637
|
+
if (this._scrollDebounceTimer !== null) {
|
|
638
|
+
clearTimeout(this._scrollDebounceTimer);
|
|
639
|
+
this._scrollDebounceTimer = null;
|
|
640
|
+
}
|
|
641
|
+
if (typeof document !== 'undefined' && this._onVisibilityChangeBound) {
|
|
642
|
+
document.removeEventListener('visibilitychange', this._onVisibilityChangeBound);
|
|
643
|
+
}
|
|
644
|
+
this._intersectionObserver?.disconnect();
|
|
645
|
+
this._intersectionObserver = null;
|
|
646
|
+
this._resizeObserver?.disconnect();
|
|
647
|
+
this._resizeObserver = null;
|
|
648
|
+
if (this._loopJumpHandle !== null) {
|
|
649
|
+
clearTimeout(this._loopJumpHandle);
|
|
650
|
+
this._loopJumpHandle = null;
|
|
651
|
+
}
|
|
652
|
+
if (this._loopJumpSafetyHandle !== null) {
|
|
653
|
+
clearTimeout(this._loopJumpSafetyHandle);
|
|
654
|
+
this._loopJumpSafetyHandle = null;
|
|
655
|
+
}
|
|
656
|
+
if (this._dragCleanup !== null) {
|
|
657
|
+
this._dragCleanup();
|
|
658
|
+
this._dragCleanup = null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
_reobserveSlides(slides) {
|
|
662
|
+
const observer = this._intersectionObserver;
|
|
663
|
+
if (!observer)
|
|
664
|
+
return;
|
|
665
|
+
observer.disconnect();
|
|
666
|
+
for (const slide of slides) {
|
|
667
|
+
// Locate the slide host element via document inspection. The host element
|
|
668
|
+
// is the first ancestor with the `tw-carousel-slide` tag — but with
|
|
669
|
+
// `contentChildren` we don't have direct ElementRef access on each slide.
|
|
670
|
+
// Use the public `_hostEl` set in the slide constructor.
|
|
671
|
+
const el = slide._hostEl;
|
|
672
|
+
if (el) {
|
|
673
|
+
el._twSlide = slide;
|
|
674
|
+
observer.observe(el);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// ── Scroll/active-index logic ───────────────────────────────────
|
|
679
|
+
// The scroll-math contract: both reads (`_handleScrollSettled`) and writes
|
|
680
|
+
// (`_scrollToIndex`) consult the actually-rendered slide geometry via each
|
|
681
|
+
// slide host's `offsetLeft` / `offsetTop`. The synthetic
|
|
682
|
+
// `step = (dimension + gap) / perView` from earlier revisions has been
|
|
683
|
+
// removed — fractional `slidesPerView`, container resize, or sub-pixel gap
|
|
684
|
+
// rounding could cause that step to land between snap points and the
|
|
685
|
+
// browser would snap back to the previously-visible tile. Offsets are
|
|
686
|
+
// authoritative.
|
|
687
|
+
//
|
|
688
|
+
// RTL: in modern browsers (negative-scroll model) `viewport.scrollLeft` is
|
|
689
|
+
// 0 at the logical start and decreases toward `-(scrollWidth - clientWidth)`
|
|
690
|
+
// as the user scrolls toward the logical end. Slide DOM order is preserved
|
|
691
|
+
// but layout is mirrored: `slide[0].offsetLeft` is the largest, not zero.
|
|
692
|
+
// We normalize by transforming each slide's offset into a "logical offset"
|
|
693
|
+
// that increases monotonically along the scroll axis, and read the current
|
|
694
|
+
// scroll as `Math.abs(scrollLeft)`. `_rtlSign()` is applied **at most once**
|
|
695
|
+
// — on writes (`viewport.scrollTo({ left: -logicalOffset })`).
|
|
696
|
+
_logicalOffset(slideEl) {
|
|
697
|
+
const viewport = this._viewportRef().nativeElement;
|
|
698
|
+
if (this.orientation() === 'horizontal') {
|
|
699
|
+
const isRtl = this._rtlSign() === -1;
|
|
700
|
+
return isRtl
|
|
701
|
+
? viewport.scrollWidth - slideEl.offsetLeft - slideEl.offsetWidth
|
|
702
|
+
: slideEl.offsetLeft;
|
|
703
|
+
}
|
|
704
|
+
return slideEl.offsetTop;
|
|
705
|
+
}
|
|
706
|
+
_currentLogicalScroll() {
|
|
707
|
+
const viewport = this._viewportRef().nativeElement;
|
|
708
|
+
if (this.orientation() === 'horizontal') {
|
|
709
|
+
// `Math.abs` is the single sign normalization on reads. Works for both
|
|
710
|
+
// LTR (scrollLeft ≥ 0) and RTL negative-scroll (scrollLeft ≤ 0).
|
|
711
|
+
return Math.abs(viewport.scrollLeft);
|
|
712
|
+
}
|
|
713
|
+
return viewport.scrollTop;
|
|
714
|
+
}
|
|
715
|
+
_findClosestSlideIndex(targetLogicalScroll) {
|
|
716
|
+
const slides = this.slides();
|
|
717
|
+
if (slides.length === 0)
|
|
718
|
+
return 0;
|
|
719
|
+
let bestIndex = 0;
|
|
720
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
721
|
+
for (let i = 0; i < slides.length; i++) {
|
|
722
|
+
const el = slides[i]._hostEl;
|
|
723
|
+
if (!el)
|
|
724
|
+
continue;
|
|
725
|
+
const distance = Math.abs(this._logicalOffset(el) - targetLogicalScroll);
|
|
726
|
+
if (distance < bestDistance) {
|
|
727
|
+
bestDistance = distance;
|
|
728
|
+
bestIndex = i;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return bestIndex;
|
|
732
|
+
}
|
|
733
|
+
_handleScrollSettled() {
|
|
734
|
+
if (this._isLoopJumping())
|
|
735
|
+
return;
|
|
736
|
+
if (this.slides().length === 0) {
|
|
737
|
+
this._lastInteractionSource.set(null);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const closest = this._findClosestSlideIndex(this._currentLogicalScroll());
|
|
741
|
+
const clamped = Math.max(0, Math.min(closest, this.lastReachableIndex()));
|
|
742
|
+
const from = this.activeIndex();
|
|
743
|
+
if (clamped !== from) {
|
|
744
|
+
this._emitSlideChange(from, clamped, this._lastInteractionSource() ?? 'pointer');
|
|
745
|
+
this.activeIndex.set(clamped);
|
|
746
|
+
}
|
|
747
|
+
this._lastInteractionSource.set(null);
|
|
748
|
+
}
|
|
749
|
+
_emitSlideChange(from, to, trigger) {
|
|
750
|
+
this.slideChange.emit({ from, to, trigger });
|
|
751
|
+
}
|
|
752
|
+
// ── Navigation methods (imperative API) ─────────────────────────
|
|
753
|
+
/** Advance by `slidesToScroll`. Wraps if `loop` is `true`; no-op at the last page when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally. */
|
|
754
|
+
next() {
|
|
755
|
+
const trigger = this._lastInteractionSource() ?? 'programmatic';
|
|
756
|
+
this._lastInteractionSource.set(trigger);
|
|
757
|
+
const count = this.slides().length;
|
|
758
|
+
if (count === 0)
|
|
759
|
+
return;
|
|
760
|
+
const toScroll = this._effectiveSlidesToScroll();
|
|
761
|
+
const lastReachable = this.lastReachableIndex();
|
|
762
|
+
let target = this._findNextEnabled(this.activeIndex() + toScroll, +1);
|
|
763
|
+
if (target > lastReachable) {
|
|
764
|
+
// Past the last reachable index.
|
|
765
|
+
if (this.loop()) {
|
|
766
|
+
this._loopJumpTo(0);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
target = lastReachable;
|
|
770
|
+
if (target === this.activeIndex()) {
|
|
771
|
+
this._lastInteractionSource.set(null);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
this._scrollToIndex(target);
|
|
776
|
+
}
|
|
777
|
+
/** Retreat by `slidesToScroll`. Wraps if `loop` is `true`; no-op at slide 0 when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally. */
|
|
778
|
+
prev() {
|
|
779
|
+
const trigger = this._lastInteractionSource() ?? 'programmatic';
|
|
780
|
+
this._lastInteractionSource.set(trigger);
|
|
781
|
+
const count = this.slides().length;
|
|
782
|
+
if (count === 0)
|
|
783
|
+
return;
|
|
784
|
+
const toScroll = this._effectiveSlidesToScroll();
|
|
785
|
+
let target = this._findNextEnabled(this.activeIndex() - toScroll, -1);
|
|
786
|
+
if (target < 0) {
|
|
787
|
+
if (this.loop()) {
|
|
788
|
+
this._loopJumpTo(this.lastReachableIndex());
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
target = 0;
|
|
792
|
+
if (target === this.activeIndex()) {
|
|
793
|
+
this._lastInteractionSource.set(null);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
this._scrollToIndex(target);
|
|
798
|
+
}
|
|
799
|
+
/** Jump to a specific 0-based slide index. `opts.behavior` is `'smooth' | 'instant'`; default is `'smooth'` unless `prefers-reduced-motion: reduce` is set, in which case the default is `'instant'`. */
|
|
800
|
+
scrollTo(index, opts) {
|
|
801
|
+
const trigger = this._lastInteractionSource() ?? 'programmatic';
|
|
802
|
+
this._lastInteractionSource.set(trigger);
|
|
803
|
+
this._scrollToIndex(index, this._resolveBehavior(opts));
|
|
804
|
+
}
|
|
805
|
+
/** Pause autoplay. `reason` defaults to `'manual'`. */
|
|
806
|
+
pause(_reason = 'manual') {
|
|
807
|
+
this._isManuallyPaused.set(true);
|
|
808
|
+
}
|
|
809
|
+
/** Resume autoplay if `autoplay` input is `true`. */
|
|
810
|
+
resume() {
|
|
811
|
+
this._isManuallyPaused.set(false);
|
|
812
|
+
}
|
|
813
|
+
_findNextEnabled(start, direction) {
|
|
814
|
+
const slides = this.slides();
|
|
815
|
+
const count = slides.length;
|
|
816
|
+
if (count === 0)
|
|
817
|
+
return 0;
|
|
818
|
+
let idx = start;
|
|
819
|
+
while (idx >= 0 && idx < count) {
|
|
820
|
+
if (!slides[idx].disabled())
|
|
821
|
+
return idx;
|
|
822
|
+
idx += direction;
|
|
823
|
+
}
|
|
824
|
+
return start;
|
|
825
|
+
}
|
|
826
|
+
_scrollToIndex(index, behavior = 'smooth') {
|
|
827
|
+
const viewport = this._viewportRef()?.nativeElement;
|
|
828
|
+
if (!viewport)
|
|
829
|
+
return;
|
|
830
|
+
const slides = this.slides();
|
|
831
|
+
const count = slides.length;
|
|
832
|
+
if (count === 0)
|
|
833
|
+
return;
|
|
834
|
+
const clamped = Math.max(0, Math.min(index, count - 1));
|
|
835
|
+
const from = this.activeIndex();
|
|
836
|
+
const targetEl = slides[clamped]
|
|
837
|
+
._hostEl;
|
|
838
|
+
const horizontal = this.orientation() === 'horizontal';
|
|
839
|
+
// Map our public `'instant'` to the DOM-standard `'auto'` literal.
|
|
840
|
+
const domBehavior = behavior === 'instant' ? 'auto' : 'smooth';
|
|
841
|
+
if (targetEl) {
|
|
842
|
+
const logicalOffset = this._logicalOffset(targetEl);
|
|
843
|
+
if (typeof viewport.scrollTo === 'function') {
|
|
844
|
+
// RTL writes apply `_rtlSign()` exactly once — no double-multiplication
|
|
845
|
+
// anywhere on the read/write paths (see audit (a)).
|
|
846
|
+
const opts = horizontal
|
|
847
|
+
? { left: logicalOffset * this._rtlSign(), behavior: domBehavior }
|
|
848
|
+
: { top: logicalOffset, behavior: domBehavior };
|
|
849
|
+
viewport.scrollTo(opts);
|
|
850
|
+
}
|
|
851
|
+
else if (horizontal) {
|
|
852
|
+
viewport.scrollLeft = logicalOffset * this._rtlSign();
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
viewport.scrollTop = logicalOffset;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (clamped !== from) {
|
|
859
|
+
this._emitSlideChange(from, clamped, this._lastInteractionSource() ?? 'programmatic');
|
|
860
|
+
this.activeIndex.set(clamped);
|
|
861
|
+
}
|
|
862
|
+
// Clear the source — `_handleScrollSettled` will not re-emit because the
|
|
863
|
+
// index already matches.
|
|
864
|
+
this._lastInteractionSource.set(null);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* @internal Resolves the effective scroll behavior. Explicit caller-supplied
|
|
868
|
+
* values are honoured verbatim; otherwise checks
|
|
869
|
+
* `matchMedia('(prefers-reduced-motion: reduce)')` and returns `'instant'`
|
|
870
|
+
* when the user has reduced-motion enabled, `'smooth'` otherwise. See B(d).
|
|
871
|
+
*/
|
|
872
|
+
_resolveBehavior(opts) {
|
|
873
|
+
if (opts?.behavior)
|
|
874
|
+
return opts.behavior;
|
|
875
|
+
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
|
876
|
+
try {
|
|
877
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
878
|
+
return 'instant';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
// matchMedia may throw in non-browser environments; fall through.
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return 'smooth';
|
|
886
|
+
}
|
|
887
|
+
_loopJumpHandle = null;
|
|
888
|
+
_loopJumpSafetyHandle = null;
|
|
889
|
+
_loopJumpTo(index) {
|
|
890
|
+
this._isLoopJumping.set(true);
|
|
891
|
+
const viewport = this._viewportRef().nativeElement;
|
|
892
|
+
viewport.classList.add('tw-carousel-loop-jump');
|
|
893
|
+
// If a previous loop-jump is still pending, drop its timers — the new
|
|
894
|
+
// jump supersedes it.
|
|
895
|
+
if (this._loopJumpHandle !== null)
|
|
896
|
+
clearTimeout(this._loopJumpHandle);
|
|
897
|
+
if (this._loopJumpSafetyHandle !== null)
|
|
898
|
+
clearTimeout(this._loopJumpSafetyHandle);
|
|
899
|
+
// Sequencing the opacity mask. The keyframe in `theme/_base.css`
|
|
900
|
+
// (`tw-carousel-loop-jump`) runs 0% → 40% → 100% over 200ms with the
|
|
901
|
+
// opacity nadir (0.55) at 40% — i.e. 80ms in. We defer the instant scroll
|
|
902
|
+
// until ~80ms after class-add so the swap happens at peak occlusion,
|
|
903
|
+
// *under* the mask, rather than before the dip starts (audit (e)). Under
|
|
904
|
+
// `prefers-reduced-motion: reduce`, `_base.css:361` zeroes the animation
|
|
905
|
+
// duration; the scroll still fires at +80ms, which is a negligible delay
|
|
906
|
+
// and avoids a code branch.
|
|
907
|
+
this._loopJumpHandle = setTimeout(() => {
|
|
908
|
+
this._loopJumpHandle = null;
|
|
909
|
+
// _scrollToIndex handles the slideChange emission and the activeIndex
|
|
910
|
+
// update; we just need the loop-jump flag set so the scrollend
|
|
911
|
+
// recompute does not re-emit.
|
|
912
|
+
this._scrollToIndex(index, 'instant');
|
|
913
|
+
}, 80);
|
|
914
|
+
const cleanup = () => {
|
|
915
|
+
viewport.classList.remove('tw-carousel-loop-jump');
|
|
916
|
+
this._isLoopJumping.set(false);
|
|
917
|
+
viewport.removeEventListener('animationend', cleanup);
|
|
918
|
+
if (this._loopJumpSafetyHandle !== null) {
|
|
919
|
+
clearTimeout(this._loopJumpSafetyHandle);
|
|
920
|
+
this._loopJumpSafetyHandle = null;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
viewport.addEventListener('animationend', cleanup, { once: true });
|
|
924
|
+
// Safety net — if the animation never fires (reduced-motion override,
|
|
925
|
+
// jsdom environment), clear after the keyframe duration.
|
|
926
|
+
this._loopJumpSafetyHandle = setTimeout(cleanup, 220);
|
|
927
|
+
}
|
|
928
|
+
// ── Host event handlers ─────────────────────────────────────────
|
|
929
|
+
_onPointerEnter() {
|
|
930
|
+
this._isHovered.set(true);
|
|
931
|
+
}
|
|
932
|
+
_onPointerLeave() {
|
|
933
|
+
this._isHovered.set(false);
|
|
934
|
+
}
|
|
935
|
+
_onFocusIn() {
|
|
936
|
+
this._hasFocusInside.set(true);
|
|
937
|
+
}
|
|
938
|
+
_onFocusOut(event) {
|
|
939
|
+
const root = event.currentTarget ?? null;
|
|
940
|
+
const next = event.relatedTarget;
|
|
941
|
+
if (root && next && root.contains(next)) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
this._hasFocusInside.set(false);
|
|
945
|
+
}
|
|
946
|
+
// ── Pointer drag handler ───────────────────────────────────────
|
|
947
|
+
_dragStart = null;
|
|
948
|
+
// Per-drag cleanup closure. Set in `_onPointerDown`, invoked from
|
|
949
|
+
// `_onPointerUp` AND from `_teardownViewport` (the DestroyRef hook) — that
|
|
950
|
+
// covers the destroy-mid-drag race where the component is torn down before
|
|
951
|
+
// the `pointerup`/`pointercancel` listeners fire (audit (g)). The closure
|
|
952
|
+
// nulls the field itself so a second invocation is a no-op.
|
|
953
|
+
_dragCleanup = null;
|
|
954
|
+
_onPointerDown(event) {
|
|
955
|
+
if (!this.draggable())
|
|
956
|
+
return;
|
|
957
|
+
if (!event.isPrimary || event.pointerType === 'touch')
|
|
958
|
+
return;
|
|
959
|
+
const viewport = this._viewportRef().nativeElement;
|
|
960
|
+
this._dragStart = {
|
|
961
|
+
x: event.clientX,
|
|
962
|
+
y: event.clientY,
|
|
963
|
+
scrollLeft: viewport.scrollLeft,
|
|
964
|
+
scrollTop: viewport.scrollTop,
|
|
965
|
+
pointerId: event.pointerId,
|
|
966
|
+
};
|
|
967
|
+
const moveHandler = (e) => this._onPointerMove(e);
|
|
968
|
+
const upHandler = (e) => {
|
|
969
|
+
// Invoke the unified cleanup so the destroy path and the natural-up
|
|
970
|
+
// path go through the same code.
|
|
971
|
+
if (this._dragCleanup !== null)
|
|
972
|
+
this._dragCleanup();
|
|
973
|
+
this._onPointerUp(e);
|
|
974
|
+
};
|
|
975
|
+
window.addEventListener('pointermove', moveHandler);
|
|
976
|
+
window.addEventListener('pointerup', upHandler);
|
|
977
|
+
window.addEventListener('pointercancel', upHandler);
|
|
978
|
+
this._dragCleanup = () => {
|
|
979
|
+
window.removeEventListener('pointermove', moveHandler);
|
|
980
|
+
window.removeEventListener('pointerup', upHandler);
|
|
981
|
+
window.removeEventListener('pointercancel', upHandler);
|
|
982
|
+
this._dragCleanup = null;
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
_onPointerMove(event) {
|
|
986
|
+
const start = this._dragStart;
|
|
987
|
+
if (!start)
|
|
988
|
+
return;
|
|
989
|
+
const horizontal = this.orientation() === 'horizontal';
|
|
990
|
+
const delta = horizontal ? event.clientX - start.x : event.clientY - start.y;
|
|
991
|
+
if (!this._isDragging() && Math.abs(delta) <= 6)
|
|
992
|
+
return;
|
|
993
|
+
const viewport = this._viewportRef().nativeElement;
|
|
994
|
+
if (!this._isDragging()) {
|
|
995
|
+
this._isDragging.set(true);
|
|
996
|
+
viewport.style.scrollSnapType = 'none';
|
|
997
|
+
viewport.classList.add('cursor-grabbing');
|
|
998
|
+
try {
|
|
999
|
+
viewport.setPointerCapture(start.pointerId);
|
|
1000
|
+
}
|
|
1001
|
+
catch {
|
|
1002
|
+
// setPointerCapture may throw if the element isn't focusable in some browsers.
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (horizontal) {
|
|
1006
|
+
viewport.scrollLeft = start.scrollLeft - delta * this._rtlSign();
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
viewport.scrollTop = start.scrollTop - delta;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
_onPointerUp(_event) {
|
|
1013
|
+
// Capture `_dragStart` into a local *before* nulling the field so the
|
|
1014
|
+
// `releasePointerCapture` call below still has the pointerId. Order
|
|
1015
|
+
// matters: a re-entrant pointer event (some browsers fire `pointercancel`
|
|
1016
|
+
// synchronously inside `releasePointerCapture`) must see `_dragStart === null`
|
|
1017
|
+
// and bail at the `!this._isDragging()` guard or via `_dragCleanup` no-op.
|
|
1018
|
+
const start = this._dragStart;
|
|
1019
|
+
this._dragStart = null;
|
|
1020
|
+
if (!this._isDragging())
|
|
1021
|
+
return;
|
|
1022
|
+
const viewport = this._viewportRef().nativeElement;
|
|
1023
|
+
viewport.style.scrollSnapType = '';
|
|
1024
|
+
viewport.classList.remove('cursor-grabbing');
|
|
1025
|
+
try {
|
|
1026
|
+
if (start)
|
|
1027
|
+
viewport.releasePointerCapture(start.pointerId);
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// ignore
|
|
1031
|
+
}
|
|
1032
|
+
this._isDragging.set(false);
|
|
1033
|
+
this._lastInteractionSource.set('pointer');
|
|
1034
|
+
// Suppress the click that immediately follows a drag-release.
|
|
1035
|
+
const swallowClick = (e) => {
|
|
1036
|
+
e.stopPropagation();
|
|
1037
|
+
e.preventDefault();
|
|
1038
|
+
viewport.removeEventListener('click', swallowClick, true);
|
|
1039
|
+
};
|
|
1040
|
+
viewport.addEventListener('click', swallowClick, true);
|
|
1041
|
+
queueMicrotask(() => {
|
|
1042
|
+
viewport.removeEventListener('click', swallowClick, true);
|
|
1043
|
+
});
|
|
1044
|
+
// Post-interaction autoplay pause.
|
|
1045
|
+
this._postInteractionPauseUntil.set(Date.now() + this._effectiveAutoplayInterval() * 2);
|
|
1046
|
+
setTimeout(() => {
|
|
1047
|
+
if (this._postInteractionPauseUntil() !== null &&
|
|
1048
|
+
Date.now() >= (this._postInteractionPauseUntil() ?? 0)) {
|
|
1049
|
+
this._postInteractionPauseUntil.set(null);
|
|
1050
|
+
}
|
|
1051
|
+
}, this._effectiveAutoplayInterval() * 2 + 16);
|
|
1052
|
+
}
|
|
1053
|
+
// ── Keyboard handler ───────────────────────────────────────────
|
|
1054
|
+
_onKeydown(event) {
|
|
1055
|
+
if (!this.keyboard())
|
|
1056
|
+
return;
|
|
1057
|
+
const horizontal = this.orientation() === 'horizontal';
|
|
1058
|
+
const rtl = this._rtlSign() === -1;
|
|
1059
|
+
let handled = false;
|
|
1060
|
+
switch (event.key) {
|
|
1061
|
+
case 'ArrowLeft':
|
|
1062
|
+
if (horizontal) {
|
|
1063
|
+
this._lastInteractionSource.set('keyboard');
|
|
1064
|
+
rtl ? this.next() : this.prev();
|
|
1065
|
+
handled = true;
|
|
1066
|
+
}
|
|
1067
|
+
break;
|
|
1068
|
+
case 'ArrowRight':
|
|
1069
|
+
if (horizontal) {
|
|
1070
|
+
this._lastInteractionSource.set('keyboard');
|
|
1071
|
+
rtl ? this.prev() : this.next();
|
|
1072
|
+
handled = true;
|
|
1073
|
+
}
|
|
1074
|
+
break;
|
|
1075
|
+
case 'ArrowUp':
|
|
1076
|
+
this._lastInteractionSource.set('keyboard');
|
|
1077
|
+
this.prev();
|
|
1078
|
+
handled = true;
|
|
1079
|
+
break;
|
|
1080
|
+
case 'ArrowDown':
|
|
1081
|
+
this._lastInteractionSource.set('keyboard');
|
|
1082
|
+
this.next();
|
|
1083
|
+
handled = true;
|
|
1084
|
+
break;
|
|
1085
|
+
case 'Home':
|
|
1086
|
+
this._lastInteractionSource.set('keyboard');
|
|
1087
|
+
this.scrollTo(0);
|
|
1088
|
+
handled = true;
|
|
1089
|
+
break;
|
|
1090
|
+
case 'End': {
|
|
1091
|
+
this._lastInteractionSource.set('keyboard');
|
|
1092
|
+
const lastIndex = Math.max(0, this.slides().length - 1);
|
|
1093
|
+
this.scrollTo(lastIndex);
|
|
1094
|
+
handled = true;
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
case 'PageUp':
|
|
1098
|
+
this._lastInteractionSource.set('keyboard');
|
|
1099
|
+
this.prev();
|
|
1100
|
+
handled = true;
|
|
1101
|
+
break;
|
|
1102
|
+
case 'PageDown':
|
|
1103
|
+
this._lastInteractionSource.set('keyboard');
|
|
1104
|
+
this.next();
|
|
1105
|
+
handled = true;
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
if (handled) {
|
|
1109
|
+
event.preventDefault();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// ── Pause control toggle ───────────────────────────────────────
|
|
1113
|
+
_togglePauseControl() {
|
|
1114
|
+
const next = !this._isManuallyPaused();
|
|
1115
|
+
this._isManuallyPaused.set(next);
|
|
1116
|
+
}
|
|
1117
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1118
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CarouselComponent, isStandalone: true, selector: "tw-carousel", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, slidesPerView: { classPropertyName: "slidesPerView", publicName: "slidesPerView", isSignal: true, isRequired: false, transformFunction: null }, slidesToScroll: { classPropertyName: "slidesToScroll", publicName: "slidesToScroll", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, autoplayInterval: { classPropertyName: "autoplayInterval", publicName: "autoplayInterval", isSignal: true, isRequired: false, transformFunction: null }, pauseOnHover: { classPropertyName: "pauseOnHover", publicName: "pauseOnHover", isSignal: true, isRequired: false, transformFunction: null }, pauseOnFocusIn: { classPropertyName: "pauseOnFocusIn", publicName: "pauseOnFocusIn", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null }, keyboard: { classPropertyName: "keyboard", publicName: "keyboard", isSignal: true, isRequired: false, transformFunction: null }, snapAlign: { classPropertyName: "snapAlign", publicName: "snapAlign", isSignal: true, isRequired: false, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeIndex: "activeIndexChange", slideChange: "slideChange", autoplayPaused: "autoplayPaused", autoplayResumed: "autoplayResumed" }, host: { attributes: { "role": "region" }, listeners: { "pointerenter": "_onPointerEnter()", "pointerleave": "_onPointerLeave()", "focusin": "_onFocusIn()", "focusout": "_onFocusOut($event)" }, properties: { "attr.aria-roledescription": "\"carousel\"", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledBy()", "class": "rootClasses()" } }, queries: [{ propertyName: "slides", predicate: CarouselSlideComponent, isSignal: true }], viewQueries: [{ propertyName: "_viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1119
|
+
<div
|
|
1120
|
+
#viewport
|
|
1121
|
+
data-tw-carousel-viewport
|
|
1122
|
+
tabindex="0"
|
|
1123
|
+
[class]="viewportClasses()"
|
|
1124
|
+
[attr.aria-live]="autoplay() ? 'off' : 'polite'"
|
|
1125
|
+
(pointerdown)="_onPointerDown($event)"
|
|
1126
|
+
(keydown)="_onKeydown($event)"
|
|
1127
|
+
>
|
|
1128
|
+
<ng-content select="tw-carousel-slide" />
|
|
1129
|
+
</div>
|
|
1130
|
+
|
|
1131
|
+
<!--
|
|
1132
|
+
Pause/play glyphs are inline SVG (not <tw-icon>) to avoid coupling the
|
|
1133
|
+
carousel entry point to ngx-tw/icon's registry. The carousel must work
|
|
1134
|
+
without consumer icon registration; two structural primitives should
|
|
1135
|
+
not couple through an icon registry (audit (h)).
|
|
1136
|
+
-->
|
|
1137
|
+
@if (autoplay()) {
|
|
1138
|
+
<button
|
|
1139
|
+
type="button"
|
|
1140
|
+
[class]="pauseControlClasses()"
|
|
1141
|
+
[attr.aria-label]="_pauseControlAriaLabel()"
|
|
1142
|
+
(click)="_togglePauseControl()"
|
|
1143
|
+
>
|
|
1144
|
+
@if (_isManuallyPausedView()) {
|
|
1145
|
+
<svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
|
|
1146
|
+
<path d="M8 5v14l11-7L8 5z" fill="currentColor" />
|
|
1147
|
+
</svg>
|
|
1148
|
+
} @else {
|
|
1149
|
+
<svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
|
|
1150
|
+
<path d="M6 5h4v14H6V5zm8 0h4v14h-4V5z" fill="currentColor" />
|
|
1151
|
+
</svg>
|
|
1152
|
+
}
|
|
1153
|
+
</button>
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
<ng-content select="[twCarouselPrev], [twCarouselNext], tw-carousel-indicators" />
|
|
1157
|
+
<!-- Default slot: catches arbitrary footer content (custom control rows,
|
|
1158
|
+
captions, secondary buttons). Render after the named-selector slot so
|
|
1159
|
+
dedicated controls keep their canonical position. -->
|
|
1160
|
+
<ng-content />
|
|
1161
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1162
|
+
}
|
|
1163
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselComponent, decorators: [{
|
|
1164
|
+
type: Component,
|
|
1165
|
+
args: [{
|
|
1166
|
+
selector: 'tw-carousel',
|
|
1167
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1168
|
+
host: {
|
|
1169
|
+
role: 'region',
|
|
1170
|
+
'[attr.aria-roledescription]': '"carousel"',
|
|
1171
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
1172
|
+
'[attr.aria-labelledby]': 'ariaLabelledBy()',
|
|
1173
|
+
'[class]': 'rootClasses()',
|
|
1174
|
+
'(pointerenter)': '_onPointerEnter()',
|
|
1175
|
+
'(pointerleave)': '_onPointerLeave()',
|
|
1176
|
+
'(focusin)': '_onFocusIn()',
|
|
1177
|
+
'(focusout)': '_onFocusOut($event)',
|
|
1178
|
+
},
|
|
1179
|
+
template: `
|
|
1180
|
+
<div
|
|
1181
|
+
#viewport
|
|
1182
|
+
data-tw-carousel-viewport
|
|
1183
|
+
tabindex="0"
|
|
1184
|
+
[class]="viewportClasses()"
|
|
1185
|
+
[attr.aria-live]="autoplay() ? 'off' : 'polite'"
|
|
1186
|
+
(pointerdown)="_onPointerDown($event)"
|
|
1187
|
+
(keydown)="_onKeydown($event)"
|
|
1188
|
+
>
|
|
1189
|
+
<ng-content select="tw-carousel-slide" />
|
|
1190
|
+
</div>
|
|
1191
|
+
|
|
1192
|
+
<!--
|
|
1193
|
+
Pause/play glyphs are inline SVG (not <tw-icon>) to avoid coupling the
|
|
1194
|
+
carousel entry point to ngx-tw/icon's registry. The carousel must work
|
|
1195
|
+
without consumer icon registration; two structural primitives should
|
|
1196
|
+
not couple through an icon registry (audit (h)).
|
|
1197
|
+
-->
|
|
1198
|
+
@if (autoplay()) {
|
|
1199
|
+
<button
|
|
1200
|
+
type="button"
|
|
1201
|
+
[class]="pauseControlClasses()"
|
|
1202
|
+
[attr.aria-label]="_pauseControlAriaLabel()"
|
|
1203
|
+
(click)="_togglePauseControl()"
|
|
1204
|
+
>
|
|
1205
|
+
@if (_isManuallyPausedView()) {
|
|
1206
|
+
<svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
|
|
1207
|
+
<path d="M8 5v14l11-7L8 5z" fill="currentColor" />
|
|
1208
|
+
</svg>
|
|
1209
|
+
} @else {
|
|
1210
|
+
<svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
|
|
1211
|
+
<path d="M6 5h4v14H6V5zm8 0h4v14h-4V5z" fill="currentColor" />
|
|
1212
|
+
</svg>
|
|
1213
|
+
}
|
|
1214
|
+
</button>
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
<ng-content select="[twCarouselPrev], [twCarouselNext], tw-carousel-indicators" />
|
|
1218
|
+
<!-- Default slot: catches arbitrary footer content (custom control rows,
|
|
1219
|
+
captions, secondary buttons). Render after the named-selector slot so
|
|
1220
|
+
dedicated controls keep their canonical position. -->
|
|
1221
|
+
<ng-content />
|
|
1222
|
+
`,
|
|
1223
|
+
}]
|
|
1224
|
+
}], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], slidesPerView: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesPerView", required: false }] }], slidesToScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesToScroll", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], autoplayInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplayInterval", required: false }] }], pauseOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseOnHover", required: false }] }], pauseOnFocusIn: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseOnFocusIn", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], keyboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboard", required: false }] }], snapAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapAlign", required: false }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }, { type: i0.Output, args: ["activeIndexChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], slideChange: [{ type: i0.Output, args: ["slideChange"] }], autoplayPaused: [{ type: i0.Output, args: ["autoplayPaused"] }], autoplayResumed: [{ type: i0.Output, args: ["autoplayResumed"] }], _viewportRef: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CarouselSlideComponent), { ...{ descendants: false }, isSignal: true }] }] } });
|
|
1225
|
+
// ── CarouselIndicatorsComponent ───────────────────────────────────
|
|
1226
|
+
/**
|
|
1227
|
+
* Renders one button per **page** (not per slide) inside a `<tw-carousel>`.
|
|
1228
|
+
* The active button is marked with `aria-current="true"` and a distinguishing
|
|
1229
|
+
* scale / width / fill so it is identifiable beyond color alone (WCAG 1.4.1).
|
|
1230
|
+
*/
|
|
1231
|
+
class CarouselIndicatorsComponent {
|
|
1232
|
+
/** Visual style. `'dots'` = small filled circles; `'lines'` = short horizontal/vertical bars; `'numbers'` = text 1, 2, 3 inside small pills. Defaults to `'dots'`. */
|
|
1233
|
+
variant = input('dots', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
1234
|
+
/** Color of the active indicator. Inactive indicators use neutral `fg-muted` tokens. Defaults to `'primary'`. */
|
|
1235
|
+
color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
1236
|
+
/** Indicator size (diameter/length and gap between indicators). Defaults to `'md'`. */
|
|
1237
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
1238
|
+
/** When `'overlay'`, the indicators float absolutely-positioned over the carousel with a `bg-overlay-control` translucent capsule backdrop for contrast. When `'below'`, they sit below the carousel as a normal block. Defaults to `'below'`. */
|
|
1239
|
+
position = input('below', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
1240
|
+
/** @internal Direct parent carousel. */
|
|
1241
|
+
carousel = inject(CarouselComponent);
|
|
1242
|
+
_variantResult = computed(() => carouselVariants({
|
|
1243
|
+
orientation: this.carousel.orientation(),
|
|
1244
|
+
variant: this.variant(),
|
|
1245
|
+
size: this.size(),
|
|
1246
|
+
position: this.position(),
|
|
1247
|
+
gap: this.carousel.gap(),
|
|
1248
|
+
snapAlign: this.carousel.snapAlign(),
|
|
1249
|
+
}), ...(ngDevMode ? [{ debugName: "_variantResult" }] : /* istanbul ignore next */ []));
|
|
1250
|
+
rootClasses = computed(() => this._variantResult().indicators(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
1251
|
+
/**
|
|
1252
|
+
* @internal Sequence of page indices to render. Returns `[]` when the
|
|
1253
|
+
* parent carousel's `slidesPerView` is not `1` — a single dot can never
|
|
1254
|
+
* match a viewport that shows two or more slides, so the indicator UX is
|
|
1255
|
+
* unambiguously broken at multi-item display (issue A1). The host element
|
|
1256
|
+
* stays mounted so layout doesn't reflow if the consumer toggles
|
|
1257
|
+
* `slidesPerView` back to `1` — the buttons reappear at that point without
|
|
1258
|
+
* a remount.
|
|
1259
|
+
*/
|
|
1260
|
+
_pages = computed(() => {
|
|
1261
|
+
if (this.carousel.slidesPerView() !== 1)
|
|
1262
|
+
return [];
|
|
1263
|
+
const count = this.carousel.pageCount();
|
|
1264
|
+
const out = new Array(count);
|
|
1265
|
+
for (let i = 0; i < count; i++)
|
|
1266
|
+
out[i] = i;
|
|
1267
|
+
return out;
|
|
1268
|
+
}, ...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
|
|
1269
|
+
constructor() {
|
|
1270
|
+
if (isDevMode()) {
|
|
1271
|
+
let warned = false;
|
|
1272
|
+
effect(() => {
|
|
1273
|
+
if (!warned && this.carousel.slidesPerView() !== 1) {
|
|
1274
|
+
warned = true;
|
|
1275
|
+
console.warn(`[tw-carousel-indicators] Indicators are only shown when slidesPerView === 1. Got slidesPerView=${this.carousel.slidesPerView()}; indicators are hidden until slidesPerView returns to 1.`);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
_buttonClasses(page) {
|
|
1281
|
+
const base = this._variantResult().indicator();
|
|
1282
|
+
const active = page === this.carousel.activePage();
|
|
1283
|
+
const stateClasses = active
|
|
1284
|
+
? resolveIndicatorActiveClasses(this.variant(), this.color())
|
|
1285
|
+
: resolveIndicatorInactiveClasses(this.variant());
|
|
1286
|
+
return `${base} ${stateClasses}`;
|
|
1287
|
+
}
|
|
1288
|
+
_buttonLabel(page) {
|
|
1289
|
+
return formatLabel(this.carousel.resolvedLabels().indicator, {
|
|
1290
|
+
page: page + 1,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
_onClick(page) {
|
|
1294
|
+
const toScroll = this.carousel._effectiveSlidesToScroll();
|
|
1295
|
+
const targetIndex = page * toScroll;
|
|
1296
|
+
this.carousel._setLastInteractionSource('indicator');
|
|
1297
|
+
this.carousel.scrollTo(targetIndex);
|
|
1298
|
+
}
|
|
1299
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselIndicatorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1300
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CarouselIndicatorsComponent, isStandalone: true, selector: "tw-carousel-indicators", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "class": "rootClasses()" } }, ngImport: i0, template: `
|
|
1301
|
+
@for (page of _pages(); track page) {
|
|
1302
|
+
<button
|
|
1303
|
+
type="button"
|
|
1304
|
+
[class]="_buttonClasses(page)"
|
|
1305
|
+
[attr.aria-label]="_buttonLabel(page)"
|
|
1306
|
+
[attr.aria-current]="page === carousel.activePage() ? 'true' : null"
|
|
1307
|
+
(click)="_onClick(page)"
|
|
1308
|
+
>
|
|
1309
|
+
@if (variant() === 'numbers') {
|
|
1310
|
+
{{ page + 1 }}
|
|
1311
|
+
}
|
|
1312
|
+
</button>
|
|
1313
|
+
}
|
|
1314
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1315
|
+
}
|
|
1316
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselIndicatorsComponent, decorators: [{
|
|
1317
|
+
type: Component,
|
|
1318
|
+
args: [{
|
|
1319
|
+
selector: 'tw-carousel-indicators',
|
|
1320
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1321
|
+
host: {
|
|
1322
|
+
role: 'group',
|
|
1323
|
+
'[class]': 'rootClasses()',
|
|
1324
|
+
},
|
|
1325
|
+
template: `
|
|
1326
|
+
@for (page of _pages(); track page) {
|
|
1327
|
+
<button
|
|
1328
|
+
type="button"
|
|
1329
|
+
[class]="_buttonClasses(page)"
|
|
1330
|
+
[attr.aria-label]="_buttonLabel(page)"
|
|
1331
|
+
[attr.aria-current]="page === carousel.activePage() ? 'true' : null"
|
|
1332
|
+
(click)="_onClick(page)"
|
|
1333
|
+
>
|
|
1334
|
+
@if (variant() === 'numbers') {
|
|
1335
|
+
{{ page + 1 }}
|
|
1336
|
+
}
|
|
1337
|
+
</button>
|
|
1338
|
+
}
|
|
1339
|
+
`,
|
|
1340
|
+
}]
|
|
1341
|
+
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
|
|
1342
|
+
// ── Prev / Next directives ────────────────────────────────────────
|
|
1343
|
+
/**
|
|
1344
|
+
* Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>`
|
|
1345
|
+
* to navigate to the previous page. Auto-disables at the first slide when the
|
|
1346
|
+
* carousel is not looping. Sets `aria-label` to `labels.previous` unless the
|
|
1347
|
+
* host already carries `aria-label` or `aria-labelledby`.
|
|
1348
|
+
*/
|
|
1349
|
+
class CarouselPrevDirective {
|
|
1350
|
+
/** @internal Parent carousel. */
|
|
1351
|
+
carousel = inject(CarouselComponent);
|
|
1352
|
+
_hostEl = inject((ElementRef)).nativeElement;
|
|
1353
|
+
_consumerLabel = this._hostEl.getAttribute('aria-label');
|
|
1354
|
+
_hasLabelledBy = this._hostEl.hasAttribute('aria-labelledby');
|
|
1355
|
+
_isButton = this._hostEl.tagName === 'BUTTON';
|
|
1356
|
+
/** @internal True when prev is unavailable (at start and !loop). */
|
|
1357
|
+
_isDisabled = computed(() => !this.carousel.loop() && this.carousel.isAtStart(), ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
|
|
1358
|
+
_ariaLabel() {
|
|
1359
|
+
// Consumer-provided aria-label wins outright. Consumer-provided
|
|
1360
|
+
// aria-labelledby suppresses our binding so the two labels don't compete.
|
|
1361
|
+
if (this._consumerLabel !== null)
|
|
1362
|
+
return this._consumerLabel;
|
|
1363
|
+
if (this._hasLabelledBy)
|
|
1364
|
+
return null;
|
|
1365
|
+
return this.carousel.resolvedLabels().previous;
|
|
1366
|
+
}
|
|
1367
|
+
_isButtonDisabled() {
|
|
1368
|
+
return this._isButton && this._isDisabled() ? '' : null;
|
|
1369
|
+
}
|
|
1370
|
+
_isTabindex() {
|
|
1371
|
+
return !this._isButton && this._isDisabled() ? '-1' : null;
|
|
1372
|
+
}
|
|
1373
|
+
_onClick(event) {
|
|
1374
|
+
if (this._isDisabled()) {
|
|
1375
|
+
event.preventDefault();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
this.carousel._setLastInteractionSource('button');
|
|
1379
|
+
this.carousel.prev();
|
|
1380
|
+
}
|
|
1381
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselPrevDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1382
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CarouselPrevDirective, isStandalone: true, selector: "[twCarouselPrev]", host: { listeners: { "click": "_onClick($event)" }, properties: { "attr.aria-label": "_ariaLabel()", "attr.disabled": "_isButtonDisabled()", "attr.aria-disabled": "_isDisabled() ? \"true\" : null", "attr.tabindex": "_isTabindex()" } }, ngImport: i0 });
|
|
1383
|
+
}
|
|
1384
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselPrevDirective, decorators: [{
|
|
1385
|
+
type: Directive,
|
|
1386
|
+
args: [{
|
|
1387
|
+
selector: '[twCarouselPrev]',
|
|
1388
|
+
host: {
|
|
1389
|
+
'(click)': '_onClick($event)',
|
|
1390
|
+
'[attr.aria-label]': '_ariaLabel()',
|
|
1391
|
+
'[attr.disabled]': '_isButtonDisabled()',
|
|
1392
|
+
'[attr.aria-disabled]': '_isDisabled() ? "true" : null',
|
|
1393
|
+
'[attr.tabindex]': '_isTabindex()',
|
|
1394
|
+
},
|
|
1395
|
+
}]
|
|
1396
|
+
}] });
|
|
1397
|
+
/**
|
|
1398
|
+
* Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>`
|
|
1399
|
+
* to navigate to the next page. Auto-disables at the last page when the
|
|
1400
|
+
* carousel is not looping. Sets `aria-label` to `labels.next` unless the host
|
|
1401
|
+
* already carries `aria-label` or `aria-labelledby`.
|
|
1402
|
+
*/
|
|
1403
|
+
class CarouselNextDirective {
|
|
1404
|
+
/** @internal Parent carousel. */
|
|
1405
|
+
carousel = inject(CarouselComponent);
|
|
1406
|
+
_hostEl = inject((ElementRef)).nativeElement;
|
|
1407
|
+
_consumerLabel = this._hostEl.getAttribute('aria-label');
|
|
1408
|
+
_hasLabelledBy = this._hostEl.hasAttribute('aria-labelledby');
|
|
1409
|
+
_isButton = this._hostEl.tagName === 'BUTTON';
|
|
1410
|
+
_isDisabled = computed(() => !this.carousel.loop() && this.carousel.isAtEnd(), ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
|
|
1411
|
+
_ariaLabel() {
|
|
1412
|
+
if (this._consumerLabel !== null)
|
|
1413
|
+
return this._consumerLabel;
|
|
1414
|
+
if (this._hasLabelledBy)
|
|
1415
|
+
return null;
|
|
1416
|
+
return this.carousel.resolvedLabels().next;
|
|
1417
|
+
}
|
|
1418
|
+
_isButtonDisabled() {
|
|
1419
|
+
return this._isButton && this._isDisabled() ? '' : null;
|
|
1420
|
+
}
|
|
1421
|
+
_isTabindex() {
|
|
1422
|
+
return !this._isButton && this._isDisabled() ? '-1' : null;
|
|
1423
|
+
}
|
|
1424
|
+
_onClick(event) {
|
|
1425
|
+
if (this._isDisabled()) {
|
|
1426
|
+
event.preventDefault();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
this.carousel._setLastInteractionSource('button');
|
|
1430
|
+
this.carousel.next();
|
|
1431
|
+
}
|
|
1432
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselNextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1433
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CarouselNextDirective, isStandalone: true, selector: "[twCarouselNext]", host: { listeners: { "click": "_onClick($event)" }, properties: { "attr.aria-label": "_ariaLabel()", "attr.disabled": "_isButtonDisabled()", "attr.aria-disabled": "_isDisabled() ? \"true\" : null", "attr.tabindex": "_isTabindex()" } }, ngImport: i0 });
|
|
1434
|
+
}
|
|
1435
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselNextDirective, decorators: [{
|
|
1436
|
+
type: Directive,
|
|
1437
|
+
args: [{
|
|
1438
|
+
selector: '[twCarouselNext]',
|
|
1439
|
+
host: {
|
|
1440
|
+
'(click)': '_onClick($event)',
|
|
1441
|
+
'[attr.aria-label]': '_ariaLabel()',
|
|
1442
|
+
'[attr.disabled]': '_isButtonDisabled()',
|
|
1443
|
+
'[attr.aria-disabled]': '_isDisabled() ? "true" : null',
|
|
1444
|
+
'[attr.tabindex]': '_isTabindex()',
|
|
1445
|
+
},
|
|
1446
|
+
}]
|
|
1447
|
+
}] });
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Generated bundle index. Do not edit.
|
|
1451
|
+
*/
|
|
1452
|
+
|
|
1453
|
+
export { CarouselComponent, CarouselIndicatorsComponent, CarouselNextDirective, CarouselPrevDirective, CarouselSlideComponent, DEFAULT_CAROUSEL_LABELS };
|
|
1454
|
+
//# sourceMappingURL=cdevhub-ngx-tw-carousel.mjs.map
|