@almoamendev/ngx-md3 0.2.1 → 0.3.4
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, booleanAttribute, effect, Directive, inject, signal, computed, Injectable, InjectionToken, ViewChild, Component, ViewContainerRef, Injector, viewChild, numberAttribute, HostListener, model, contentChild, contentChildren, output, ElementRef, HostBinding, TemplateRef } from '@angular/core';
|
|
3
|
-
import { DOCUMENT, NgClass, NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import { input, booleanAttribute, effect, Directive, inject, signal, computed, Injectable, InjectionToken, ViewChild, Component, ViewContainerRef, Injector, viewChild, numberAttribute, HostListener, model, contentChild, contentChildren, PLATFORM_ID, afterNextRender, output, ElementRef, HostBinding, TemplateRef } from '@angular/core';
|
|
3
|
+
import { DOCUMENT, isPlatformBrowser, NgClass, NgTemplateOutlet } from '@angular/common';
|
|
4
4
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
5
5
|
import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
6
|
import * as i1 from '@angular/router';
|
|
@@ -9,10 +9,11 @@ import { fromEvent, startWith, map, filter, Subscription, Subject, merge, take }
|
|
|
9
9
|
import { ComponentPortal, CdkPortalOutlet } from '@angular/cdk/portal';
|
|
10
10
|
import * as i2 from '@angular/cdk/scrolling';
|
|
11
11
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
|
12
|
+
import { Directionality } from '@angular/cdk/bidi';
|
|
12
13
|
import { FormControlName } from '@angular/forms';
|
|
13
14
|
import { CdkDialogContainer, Dialog as Dialog$1 } from '@angular/cdk/dialog';
|
|
14
15
|
import { hasModifierKey } from '@angular/cdk/keycodes';
|
|
15
|
-
import { Overlay, OverlayConfig } from '@angular/cdk/overlay';
|
|
16
|
+
import { Overlay, OverlayContainer, OverlayConfig } from '@angular/cdk/overlay';
|
|
16
17
|
|
|
17
18
|
class TypeDisplay {
|
|
18
19
|
el;
|
|
@@ -1553,6 +1554,947 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
|
|
|
1553
1554
|
], template: "<ng-content></ng-content>\n", styles: [":host{--background-color: rgb(var(--md-scheme-surface-container-low));--foreground-color: rgb(var(--md-scheme-on-surface));--state-color: var(--md-scheme-on-surface);--border-radius: var(--md-border-radius-medium);--border-size: 0em;--outline-color: rgb(var(--md-scheme-secondary));--outline-width: .1875em;--outline-offset: -.125em;font-size:1rem;border-radius:var(--md-border-radius-medium);display:block;width:100%;text-align:start;padding:0;border:none;text-decoration:none;background-color:var(--background-color);color:var(--foreground-color);box-shadow:var(--md-shadow-1dp);transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing)}:host.md3-interactive:hover{box-shadow:var(--md-shadow-3dp)}:host.md3-interactive:active,:host.md3-interactive:focus{box-shadow:var(--md-shadow-1dp)}:host.md3-filled{--background-color: rgb(var(--md-scheme-surface-container-highest));box-shadow:var(--md-shadow-0dp)}:host.md3-filled.md3-interactive:hover{box-shadow:var(--md-shadow-1dp)}:host.md3-filled.md3-interactive:active,:host.md3-filled.md3-interactive:focus{box-shadow:var(--md-shadow-0dp)}:host.md3-outlined{--background-color: rgb(var(--md-scheme-surface));--border-size: .0625em;box-shadow:var(--md-shadow-0dp);border:var(--border-size) solid rgb(var(--md-scheme-outline-variant))}:host.md3-outlined.md3-interactive:hover{box-shadow:var(--md-shadow-1dp)}:host.md3-outlined.md3-interactive:active{box-shadow:var(--md-shadow-0dp)}:host.md3-outlined.md3-interactive:focus{box-shadow:var(--md-shadow-0dp);border-color:rgb(var(--md-scheme-on-surface))}\n"] }]
|
|
1554
1555
|
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: StateComponent }], propDecorators: { cardType: [{ type: i0.Input, args: [{ isSignal: true, alias: "card-type", required: false }] }], isInteractive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }] } });
|
|
1555
1556
|
|
|
1557
|
+
/** Fraction of its own size a medium item may flex by to protect the large item's target size. */
|
|
1558
|
+
const MEDIUM_FLEX = 0.1;
|
|
1559
|
+
function clamp(value, min, max) {
|
|
1560
|
+
return Math.min(Math.max(value, min), max);
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Solves for the large item size given that medium items are always the mean of large and small.
|
|
1564
|
+
*
|
|
1565
|
+
* containerSize = largeSize * largeCount
|
|
1566
|
+
* + ((largeSize + smallSize) / 2) * mediumCount
|
|
1567
|
+
* + smallSize * smallCount
|
|
1568
|
+
*/
|
|
1569
|
+
function solveLargeSize(containerSize, smallSize, smallCount, mediumCount, largeCount) {
|
|
1570
|
+
const smallContribution = (smallCount + mediumCount / 2) * smallSize;
|
|
1571
|
+
return (containerSize - smallContribution) / (largeCount + mediumCount / 2);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Shrinks or grows a candidate until it exactly fills the container, disturbing the large items
|
|
1575
|
+
* as little as possible: small items absorb the difference first, then medium items flex, and
|
|
1576
|
+
* only what is left over changes the large size.
|
|
1577
|
+
*/
|
|
1578
|
+
function fit(containerSize, targetLargeSize, smallSizeMin, smallSizeMax, smallCount, mediumCount, largeCount, targetSmallSize, priority) {
|
|
1579
|
+
let smallSize = clamp(targetSmallSize, smallSizeMin, smallSizeMax);
|
|
1580
|
+
let largeSize = targetLargeSize;
|
|
1581
|
+
let mediumSize = (targetLargeSize + smallSize) / 2;
|
|
1582
|
+
const occupied = largeSize * largeCount + mediumSize * mediumCount + smallSize * smallCount;
|
|
1583
|
+
const delta = containerSize - occupied;
|
|
1584
|
+
if (smallCount > 0 && delta > 0) {
|
|
1585
|
+
smallSize += Math.min(delta / smallCount, smallSizeMax - smallSize);
|
|
1586
|
+
}
|
|
1587
|
+
else if (smallCount > 0 && delta < 0) {
|
|
1588
|
+
smallSize += Math.max(delta / smallCount, smallSizeMin - smallSize);
|
|
1589
|
+
}
|
|
1590
|
+
if (smallCount === 0) {
|
|
1591
|
+
smallSize = 0;
|
|
1592
|
+
}
|
|
1593
|
+
largeSize = solveLargeSize(containerSize, smallSize, smallCount, mediumCount, largeCount);
|
|
1594
|
+
mediumSize = (largeSize + smallSize) / 2;
|
|
1595
|
+
// Give the large items back as much of their target size as the medium items can spare.
|
|
1596
|
+
if (mediumCount > 0 && largeSize !== targetLargeSize) {
|
|
1597
|
+
const targetAdjustment = (targetLargeSize - largeSize) * largeCount;
|
|
1598
|
+
const availableFlex = mediumSize * MEDIUM_FLEX * mediumCount;
|
|
1599
|
+
const distribute = Math.min(Math.abs(targetAdjustment), availableFlex);
|
|
1600
|
+
const direction = targetAdjustment > 0 ? 1 : -1;
|
|
1601
|
+
mediumSize -= (direction * distribute) / mediumCount;
|
|
1602
|
+
largeSize += (direction * distribute) / largeCount;
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
largeSize,
|
|
1606
|
+
largeCount,
|
|
1607
|
+
mediumSize,
|
|
1608
|
+
mediumCount,
|
|
1609
|
+
smallSize,
|
|
1610
|
+
smallCount,
|
|
1611
|
+
cost: cost(targetLargeSize, largeSize, mediumSize, smallSize, largeCount, mediumCount, smallCount, priority),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function isValid(largeSize, mediumSize, smallSize, largeCount, mediumCount, smallCount) {
|
|
1615
|
+
if (largeSize <= 0) {
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
if (largeCount > 0 && smallCount > 0 && mediumCount > 0) {
|
|
1619
|
+
return largeSize > mediumSize && mediumSize > smallSize;
|
|
1620
|
+
}
|
|
1621
|
+
if (largeCount > 0 && smallCount > 0) {
|
|
1622
|
+
return largeSize > smallSize;
|
|
1623
|
+
}
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
function cost(targetLargeSize, largeSize, mediumSize, smallSize, largeCount, mediumCount, smallCount, priority) {
|
|
1627
|
+
if (!isValid(largeSize, mediumSize, smallSize, largeCount, mediumCount, smallCount)) {
|
|
1628
|
+
return Number.POSITIVE_INFINITY;
|
|
1629
|
+
}
|
|
1630
|
+
// Prefer arrangements that appear earlier in the priority order and that leave the large
|
|
1631
|
+
// item closest to the size the consumer asked for.
|
|
1632
|
+
return Math.abs(targetLargeSize - largeSize) * priority;
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Fits every permutation of the supplied counts and returns the cheapest.
|
|
1636
|
+
*
|
|
1637
|
+
* Permutations are generated in priority order, so a zero-cost candidate is provably optimal
|
|
1638
|
+
* and ends the search early.
|
|
1639
|
+
*/
|
|
1640
|
+
function findLowestCostArrangement(containerSize, targetLargeSize, targetSmallSize, smallSizeMin, smallSizeMax, smallCounts, mediumCounts, largeCounts) {
|
|
1641
|
+
let best;
|
|
1642
|
+
let priority = 1;
|
|
1643
|
+
for (const largeCount of largeCounts) {
|
|
1644
|
+
for (const mediumCount of mediumCounts) {
|
|
1645
|
+
for (const smallCount of smallCounts) {
|
|
1646
|
+
const candidate = fit(containerSize, targetLargeSize, smallSizeMin, smallSizeMax, smallCount, mediumCount, largeCount, targetSmallSize, priority);
|
|
1647
|
+
if (!best || candidate.cost < best.cost) {
|
|
1648
|
+
best = candidate;
|
|
1649
|
+
if (best.cost === 0) {
|
|
1650
|
+
return best;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
priority++;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// Every permutation was invalid; fall back to a single full-width item.
|
|
1658
|
+
return best ?? {
|
|
1659
|
+
largeSize: containerSize,
|
|
1660
|
+
largeCount: 1,
|
|
1661
|
+
mediumSize: 0,
|
|
1662
|
+
mediumCount: 0,
|
|
1663
|
+
smallSize: 0,
|
|
1664
|
+
smallCount: 0,
|
|
1665
|
+
cost: Number.POSITIVE_INFINITY,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Trims keylines until there are no more of them than there are items, so a short list never
|
|
1670
|
+
* leaves empty slots. Small items go first, then medium — never large, since large items are
|
|
1671
|
+
* already fully unmasked.
|
|
1672
|
+
*
|
|
1673
|
+
* Returns true when the counts changed and the arrangement needs re-solving.
|
|
1674
|
+
*/
|
|
1675
|
+
function trimToItemCount(candidate, itemCount) {
|
|
1676
|
+
let surplus = candidate.smallCount + candidate.mediumCount + candidate.largeCount - itemCount;
|
|
1677
|
+
let changed = false;
|
|
1678
|
+
while (surplus > 0 && (candidate.smallCount > 0 || candidate.mediumCount > 1)) {
|
|
1679
|
+
if (candidate.smallCount > 0) {
|
|
1680
|
+
candidate.smallCount--;
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
candidate.mediumCount--;
|
|
1684
|
+
}
|
|
1685
|
+
changed = true;
|
|
1686
|
+
surplus--;
|
|
1687
|
+
}
|
|
1688
|
+
return changed;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* The Material Design 3 multi-browse layout: a run of large items followed by a medium and a
|
|
1692
|
+
* small item, sized so the whole arrangement fills the container exactly.
|
|
1693
|
+
*
|
|
1694
|
+
* Ported from `MultiBrowseCarouselStrategy` and `Arrangement` in Material Components for
|
|
1695
|
+
* Android so the two implementations agree on sizing.
|
|
1696
|
+
*/
|
|
1697
|
+
const multiBrowseStrategy = {
|
|
1698
|
+
// Multi-browse resizes items, so it only reads correctly with items resting on keylines.
|
|
1699
|
+
snap: true,
|
|
1700
|
+
arrange(context) {
|
|
1701
|
+
const { containerSize, itemSize, smallSizeMin, smallSizeMax, itemCount, alignment } = context;
|
|
1702
|
+
const maxSmallSize = Math.max(smallSizeMax, smallSizeMin);
|
|
1703
|
+
const targetLargeSize = Math.min(itemSize, containerSize);
|
|
1704
|
+
// A small item ideally reads as a third of a large one, held within its allowed range.
|
|
1705
|
+
const targetSmallSize = clamp(itemSize / 3, smallSizeMin, maxSmallSize);
|
|
1706
|
+
const targetMediumSize = (targetLargeSize + targetSmallSize) / 2;
|
|
1707
|
+
let smallCounts = containerSize <= smallSizeMin * 2 ? [0] : [1];
|
|
1708
|
+
let mediumCounts = [1, 0];
|
|
1709
|
+
// A centred focal range needs matching keylines on both sides of it.
|
|
1710
|
+
if (alignment === 'center') {
|
|
1711
|
+
smallCounts = smallCounts.map((count) => count * 2);
|
|
1712
|
+
mediumCounts = mediumCounts.map((count) => count * 2);
|
|
1713
|
+
}
|
|
1714
|
+
const minLargeSpace = containerSize
|
|
1715
|
+
- targetMediumSize * Math.max(...mediumCounts)
|
|
1716
|
+
- maxSmallSize * Math.max(...smallCounts);
|
|
1717
|
+
const largeCountMin = Math.max(1, Math.floor(minLargeSpace / targetLargeSize));
|
|
1718
|
+
const largeCountMax = Math.max(largeCountMin, Math.ceil(containerSize / targetLargeSize));
|
|
1719
|
+
const largeCounts = [];
|
|
1720
|
+
for (let count = largeCountMax; count >= largeCountMin; count--) {
|
|
1721
|
+
largeCounts.push(count);
|
|
1722
|
+
}
|
|
1723
|
+
let arrangement = findLowestCostArrangement(containerSize, targetLargeSize, targetSmallSize, smallSizeMin, maxSmallSize, smallCounts, mediumCounts, largeCounts);
|
|
1724
|
+
let resolve = trimToItemCount(arrangement, itemCount);
|
|
1725
|
+
// An arrangement of nothing but large items has no visual hint that the list continues,
|
|
1726
|
+
// so force a small item back in whenever there is room for one.
|
|
1727
|
+
if (arrangement.mediumCount === 0 && arrangement.smallCount === 0 && containerSize > 2 * smallSizeMin) {
|
|
1728
|
+
arrangement.smallCount = 1;
|
|
1729
|
+
resolve = true;
|
|
1730
|
+
}
|
|
1731
|
+
if (resolve) {
|
|
1732
|
+
arrangement = findLowestCostArrangement(containerSize, targetLargeSize, targetSmallSize, smallSizeMin, maxSmallSize, [arrangement.smallCount], [arrangement.mediumCount], [arrangement.largeCount]);
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
largeSize: arrangement.largeSize,
|
|
1736
|
+
largeCount: arrangement.largeCount,
|
|
1737
|
+
mediumSize: arrangement.mediumSize,
|
|
1738
|
+
mediumCount: arrangement.mediumCount,
|
|
1739
|
+
smallSize: arrangement.smallSize,
|
|
1740
|
+
smallCount: arrangement.smallCount,
|
|
1741
|
+
};
|
|
1742
|
+
},
|
|
1743
|
+
};
|
|
1744
|
+
/**
|
|
1745
|
+
* Registry of the available carousel layouts.
|
|
1746
|
+
*
|
|
1747
|
+
* Adding a layout means implementing {@link CarouselStrategy}, widening `CarouselLayout` and
|
|
1748
|
+
* adding the entry here — no changes to the component itself.
|
|
1749
|
+
*/
|
|
1750
|
+
const CAROUSEL_STRATEGIES = {
|
|
1751
|
+
'multi-browse': multiBrowseStrategy,
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
/** Anchor size fallback, as a fraction of the large item, when an arrangement has no small items. */
|
|
1755
|
+
const ANCHOR_FALLBACK_RATIO = 0.2;
|
|
1756
|
+
function lerp(from, to, progress) {
|
|
1757
|
+
return from + (to - from) * progress;
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Lays out one shift step.
|
|
1761
|
+
*
|
|
1762
|
+
* Keylines before the focal range ascend in size towards it and keylines after it descend away
|
|
1763
|
+
* from it, which is what keeps the arrangement looking balanced as it shifts. An off-screen
|
|
1764
|
+
* anchor sits at each end so items have somewhere to shrink into rather than popping out.
|
|
1765
|
+
*/
|
|
1766
|
+
function buildState(step, arrangement, anchorSize) {
|
|
1767
|
+
const { largeSize, largeCount, mediumSize, mediumCount, smallSize, smallCount } = arrangement;
|
|
1768
|
+
const before = [
|
|
1769
|
+
...new Array(step.smallBefore).fill(smallSize),
|
|
1770
|
+
...new Array(step.mediumBefore).fill(mediumSize),
|
|
1771
|
+
];
|
|
1772
|
+
const after = [
|
|
1773
|
+
...new Array(mediumCount - step.mediumBefore).fill(mediumSize),
|
|
1774
|
+
...new Array(smallCount - step.smallBefore).fill(smallSize),
|
|
1775
|
+
];
|
|
1776
|
+
const sizes = [
|
|
1777
|
+
anchorSize,
|
|
1778
|
+
...before,
|
|
1779
|
+
...new Array(largeCount).fill(largeSize),
|
|
1780
|
+
...after,
|
|
1781
|
+
anchorSize,
|
|
1782
|
+
];
|
|
1783
|
+
const firstFocalIndex = 1 + before.length;
|
|
1784
|
+
const lastFocalIndex = firstFocalIndex + largeCount - 1;
|
|
1785
|
+
// The leading anchor occupies [-anchorSize, 0] so the first real keyline starts at 0.
|
|
1786
|
+
let cursor = -anchorSize;
|
|
1787
|
+
const screenLocs = sizes.map((size) => {
|
|
1788
|
+
const loc = cursor + size / 2;
|
|
1789
|
+
cursor += size;
|
|
1790
|
+
return loc;
|
|
1791
|
+
});
|
|
1792
|
+
// In scroll space every item occupies exactly `largeSize`, so keylines are evenly spaced.
|
|
1793
|
+
// Anchoring that spacing to the first focal keyline keeps screen and scroll space aligned
|
|
1794
|
+
// wherever an item is fully unmasked.
|
|
1795
|
+
const focalLoc = screenLocs[firstFocalIndex];
|
|
1796
|
+
const keylines = sizes.map((size, index) => ({
|
|
1797
|
+
scrollLoc: focalLoc + (index - firstFocalIndex) * largeSize,
|
|
1798
|
+
screenLoc: screenLocs[index],
|
|
1799
|
+
maskedSize: size,
|
|
1800
|
+
isFocal: index >= firstFocalIndex && index <= lastFocalIndex,
|
|
1801
|
+
isAnchor: index === 0 || index === sizes.length - 1,
|
|
1802
|
+
}));
|
|
1803
|
+
return {
|
|
1804
|
+
keylines,
|
|
1805
|
+
focalStart: focalLoc - largeSize / 2,
|
|
1806
|
+
firstFocalIndex,
|
|
1807
|
+
lastFocalIndex,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Enumerates every shift step from "focal range at the start of the container" through to
|
|
1812
|
+
* "focal range at the end", ordered by ascending `focalStart`.
|
|
1813
|
+
*
|
|
1814
|
+
* Each step moves exactly one keyline across the focal range, which is what guarantees every
|
|
1815
|
+
* item passes through the focal range as the carousel scrolls. Smaller keylines migrate first,
|
|
1816
|
+
* so items pile up smallest-outermost at whichever edge they are collecting against.
|
|
1817
|
+
*/
|
|
1818
|
+
function buildSteps(arrangement, alignment) {
|
|
1819
|
+
const { smallCount, mediumCount } = arrangement;
|
|
1820
|
+
const restingStep = alignment === 'center'
|
|
1821
|
+
? { smallBefore: Math.floor(smallCount / 2), mediumBefore: Math.floor(mediumCount / 2) }
|
|
1822
|
+
: { smallBefore: 0, mediumBefore: 0 };
|
|
1823
|
+
// Walk backwards from the resting step, retiring the smallest leading keyline each time.
|
|
1824
|
+
const towardsStart = [restingStep];
|
|
1825
|
+
let { smallBefore, mediumBefore } = restingStep;
|
|
1826
|
+
while (smallBefore > 0 || mediumBefore > 0) {
|
|
1827
|
+
if (smallBefore > 0) {
|
|
1828
|
+
smallBefore--;
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
mediumBefore--;
|
|
1832
|
+
}
|
|
1833
|
+
towardsStart.push({ smallBefore, mediumBefore });
|
|
1834
|
+
}
|
|
1835
|
+
// Walk forwards, promoting the smallest trailing keyline each time.
|
|
1836
|
+
const towardsEnd = [restingStep];
|
|
1837
|
+
({ smallBefore, mediumBefore } = restingStep);
|
|
1838
|
+
while (smallBefore + mediumBefore < smallCount + mediumCount) {
|
|
1839
|
+
if (smallBefore < smallCount) {
|
|
1840
|
+
smallBefore++;
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
mediumBefore++;
|
|
1844
|
+
}
|
|
1845
|
+
towardsEnd.push({ smallBefore, mediumBefore });
|
|
1846
|
+
}
|
|
1847
|
+
const ordered = [...towardsStart.slice(1).reverse(), ...towardsEnd];
|
|
1848
|
+
const anchorSize = arrangement.smallSize > 0
|
|
1849
|
+
? arrangement.smallSize
|
|
1850
|
+
: arrangement.largeSize * ANCHOR_FALLBACK_RATIO;
|
|
1851
|
+
return {
|
|
1852
|
+
steps: ordered.map((step) => buildState(step, arrangement, anchorSize)),
|
|
1853
|
+
defaultStep: towardsStart.length - 1,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Turns a solved arrangement into everything the component needs to place items.
|
|
1858
|
+
*/
|
|
1859
|
+
function buildGeometry(arrangement, alignment, itemCount, containerSize) {
|
|
1860
|
+
const { steps, defaultStep } = buildSteps(arrangement, alignment);
|
|
1861
|
+
const resting = steps[defaultStep];
|
|
1862
|
+
const startShiftRange = resting.focalStart - steps[0].focalStart;
|
|
1863
|
+
const endShiftRange = steps[steps.length - 1].focalStart - resting.focalStart;
|
|
1864
|
+
// Each item owns exactly `largeSize` of scroll range, and the final `largeCount` items share
|
|
1865
|
+
// the focal region, so they need no scroll range of their own.
|
|
1866
|
+
const lastIndex = Math.max(0, itemCount - arrangement.largeCount);
|
|
1867
|
+
const maxScroll = lastIndex * arrangement.largeSize;
|
|
1868
|
+
return {
|
|
1869
|
+
arrangement,
|
|
1870
|
+
steps,
|
|
1871
|
+
defaultStep,
|
|
1872
|
+
itemSize: arrangement.largeSize,
|
|
1873
|
+
startShiftRange,
|
|
1874
|
+
endShiftRange,
|
|
1875
|
+
maxScroll,
|
|
1876
|
+
lastIndex,
|
|
1877
|
+
scrollSize: maxScroll + containerSize,
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Where the focal range sits for a given scroll offset.
|
|
1882
|
+
*
|
|
1883
|
+
* It rests at its default position through the middle of the list and slides towards whichever
|
|
1884
|
+
* edge is being approached, so the first and last items can reach the focal range without
|
|
1885
|
+
* detaching from the container edges.
|
|
1886
|
+
*/
|
|
1887
|
+
function focalStartFor(geometry, scrollOffset) {
|
|
1888
|
+
const resting = geometry.steps[geometry.defaultStep].focalStart;
|
|
1889
|
+
const first = geometry.steps[0].focalStart;
|
|
1890
|
+
const last = geometry.steps[geometry.steps.length - 1].focalStart;
|
|
1891
|
+
const startShift = Math.max(0, geometry.startShiftRange - scrollOffset);
|
|
1892
|
+
const endShift = Math.max(0, scrollOffset - (geometry.maxScroll - geometry.endShiftRange));
|
|
1893
|
+
return Math.min(Math.max(resting - startShift + endShift, first), last);
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Interpolates the two shift steps bracketing `focalStart` into a single state.
|
|
1897
|
+
*
|
|
1898
|
+
* Every step holds the same number of keylines, so this is a straight index-by-index blend.
|
|
1899
|
+
*/
|
|
1900
|
+
function stateAt(geometry, focalStart) {
|
|
1901
|
+
const { steps } = geometry;
|
|
1902
|
+
let upper = 1;
|
|
1903
|
+
while (upper < steps.length && steps[upper].focalStart < focalStart) {
|
|
1904
|
+
upper++;
|
|
1905
|
+
}
|
|
1906
|
+
if (upper >= steps.length) {
|
|
1907
|
+
return steps[steps.length - 1];
|
|
1908
|
+
}
|
|
1909
|
+
const from = steps[upper - 1];
|
|
1910
|
+
const to = steps[upper];
|
|
1911
|
+
const span = to.focalStart - from.focalStart;
|
|
1912
|
+
if (span <= 0) {
|
|
1913
|
+
return from;
|
|
1914
|
+
}
|
|
1915
|
+
const progress = Math.min(Math.max((focalStart - from.focalStart) / span, 0), 1);
|
|
1916
|
+
return {
|
|
1917
|
+
focalStart,
|
|
1918
|
+
firstFocalIndex: progress < 0.5 ? from.firstFocalIndex : to.firstFocalIndex,
|
|
1919
|
+
lastFocalIndex: progress < 0.5 ? from.lastFocalIndex : to.lastFocalIndex,
|
|
1920
|
+
keylines: from.keylines.map((keyline, index) => {
|
|
1921
|
+
const target = to.keylines[index];
|
|
1922
|
+
return {
|
|
1923
|
+
scrollLoc: lerp(keyline.scrollLoc, target.scrollLoc, progress),
|
|
1924
|
+
screenLoc: lerp(keyline.screenLoc, target.screenLoc, progress),
|
|
1925
|
+
maskedSize: lerp(keyline.maskedSize, target.maskedSize, progress),
|
|
1926
|
+
isFocal: progress < 0.5 ? keyline.isFocal : target.isFocal,
|
|
1927
|
+
isAnchor: keyline.isAnchor && target.isAnchor,
|
|
1928
|
+
};
|
|
1929
|
+
}),
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Resolves the keyline arrangement in effect at a given scroll offset.
|
|
1934
|
+
*/
|
|
1935
|
+
function resolveState(geometry, scrollOffset) {
|
|
1936
|
+
return stateAt(geometry, focalStartFor(geometry, scrollOffset));
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Finds the keylines either side of a scroll-space position, and how far between them it sits.
|
|
1940
|
+
*/
|
|
1941
|
+
function keylineRange(keylines, scrollCenter) {
|
|
1942
|
+
let upper = 1;
|
|
1943
|
+
while (upper < keylines.length - 1 && keylines[upper].scrollLoc < scrollCenter) {
|
|
1944
|
+
upper++;
|
|
1945
|
+
}
|
|
1946
|
+
const from = keylines[upper - 1];
|
|
1947
|
+
const to = keylines[upper];
|
|
1948
|
+
const span = to.scrollLoc - from.scrollLoc;
|
|
1949
|
+
const progress = span <= 0 ? 0 : Math.min(Math.max((scrollCenter - from.scrollLoc) / span, 0), 1);
|
|
1950
|
+
return [from, to, progress];
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Classifies a rendered size against the arrangement's three sizes.
|
|
1954
|
+
*
|
|
1955
|
+
* Items resize continuously, so this snaps to whichever size the item currently reads as,
|
|
1956
|
+
* flipping at the midpoint between one size and the next.
|
|
1957
|
+
*/
|
|
1958
|
+
function sizeBandFor(maskedSize, geometry) {
|
|
1959
|
+
const { largeSize, mediumSize, smallSize } = geometry.arrangement;
|
|
1960
|
+
if (maskedSize >= (largeSize + mediumSize) / 2) {
|
|
1961
|
+
return 'large';
|
|
1962
|
+
}
|
|
1963
|
+
if (maskedSize >= (mediumSize + smallSize) / 2) {
|
|
1964
|
+
return 'medium';
|
|
1965
|
+
}
|
|
1966
|
+
return 'small';
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Places one item.
|
|
1970
|
+
*
|
|
1971
|
+
* The item's centre is a plain linear function of the scroll offset in scroll space; looking
|
|
1972
|
+
* that position up against the current keylines is what converts it into a rendered size and
|
|
1973
|
+
* an on-screen position.
|
|
1974
|
+
*/
|
|
1975
|
+
function resolveItemGeometry(geometry, state, index, scrollOffset) {
|
|
1976
|
+
const { itemSize } = geometry;
|
|
1977
|
+
const { keylines } = state;
|
|
1978
|
+
const scrollCenter = index * itemSize + itemSize / 2 - scrollOffset + state.focalStart;
|
|
1979
|
+
const first = keylines[0];
|
|
1980
|
+
const last = keylines[keylines.length - 1];
|
|
1981
|
+
if (scrollCenter < first.scrollLoc || scrollCenter > last.scrollLoc) {
|
|
1982
|
+
return {
|
|
1983
|
+
offset: 0,
|
|
1984
|
+
maskedSize: 0,
|
|
1985
|
+
size: 'small',
|
|
1986
|
+
maskRatio: 1,
|
|
1987
|
+
isFocal: false,
|
|
1988
|
+
isVisible: false,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
const [from, to, progress] = keylineRange(keylines, scrollCenter);
|
|
1992
|
+
const maskedSize = lerp(from.maskedSize, to.maskedSize, progress);
|
|
1993
|
+
const screenCenter = lerp(from.screenLoc, to.screenLoc, progress);
|
|
1994
|
+
return {
|
|
1995
|
+
offset: screenCenter - maskedSize / 2,
|
|
1996
|
+
maskedSize,
|
|
1997
|
+
size: sizeBandFor(maskedSize, geometry),
|
|
1998
|
+
maskRatio: itemSize <= 0 ? 1 : Math.min(Math.max(1 - maskedSize / itemSize, 0), 1),
|
|
1999
|
+
isFocal: (from.isFocal && progress < 0.5) || (to.isFocal && progress >= 0.5),
|
|
2000
|
+
isVisible: maskedSize > 0.5,
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Clamps an index to one the carousel can actually come to rest on.
|
|
2005
|
+
*/
|
|
2006
|
+
function clampIndex(geometry, index) {
|
|
2007
|
+
return Math.min(Math.max(Math.round(index), 0), geometry.lastIndex);
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Scroll offset at which `index` leads the focal range.
|
|
2011
|
+
*
|
|
2012
|
+
* The shifting focal range cancels out here, so this stays a simple multiple regardless of
|
|
2013
|
+
* alignment or how close to either end of the list the item is.
|
|
2014
|
+
*/
|
|
2015
|
+
function scrollOffsetForIndex(geometry, index) {
|
|
2016
|
+
return clampIndex(geometry, index) * geometry.itemSize;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* The item leading the focal range at a given scroll offset.
|
|
2020
|
+
*/
|
|
2021
|
+
function indexForScrollOffset(geometry, scrollOffset) {
|
|
2022
|
+
if (geometry.itemSize <= 0) {
|
|
2023
|
+
return 0;
|
|
2024
|
+
}
|
|
2025
|
+
return clampIndex(geometry, scrollOffset / geometry.itemSize);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* A single item within an `md3-carousel`.
|
|
2030
|
+
*
|
|
2031
|
+
* The item is a clipping box whose content stays at full size, so scrolling crops the item
|
|
2032
|
+
* towards its centre rather than squashing it. Content should be full-bleed — an image or a
|
|
2033
|
+
* background that reaches the edges — or the crop will reveal empty space.
|
|
2034
|
+
*
|
|
2035
|
+
* Position and size are written straight to CSS custom properties by the parent carousel, so
|
|
2036
|
+
* styling never waits on change detection. The same values are mirrored onto signals and onto
|
|
2037
|
+
* `md3-large` / `md3-medium` / `md3-small` classes, so content can react to the item's size
|
|
2038
|
+
* either declaratively or from a stylesheet.
|
|
2039
|
+
*/
|
|
2040
|
+
class CarouselItem {
|
|
2041
|
+
el;
|
|
2042
|
+
/** 0 while the item is fully unmasked, approaching 1 as it crops away. */
|
|
2043
|
+
maskRatio = signal(0, /* @ts-ignore */
|
|
2044
|
+
...(ngDevMode ? [{ debugName: "maskRatio" }] : /* istanbul ignore next */ []));
|
|
2045
|
+
/** Current rendered size of the item, in pixels. */
|
|
2046
|
+
maskedSize = signal(0, /* @ts-ignore */
|
|
2047
|
+
...(ngDevMode ? [{ debugName: "maskedSize" }] : /* istanbul ignore next */ []));
|
|
2048
|
+
/** True while the item rests in the carousel's focal range. */
|
|
2049
|
+
isFocal = signal(false, /* @ts-ignore */
|
|
2050
|
+
...(ngDevMode ? [{ debugName: "isFocal" }] : /* istanbul ignore next */ []));
|
|
2051
|
+
/**
|
|
2052
|
+
* Which of the arrangement's three sizes the item currently reads as.
|
|
2053
|
+
*
|
|
2054
|
+
* Mirrored onto the element as `md3-large`, `md3-medium` or `md3-small`.
|
|
2055
|
+
*/
|
|
2056
|
+
size = signal('large', /* @ts-ignore */
|
|
2057
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
2058
|
+
constructor(el) {
|
|
2059
|
+
this.el = el;
|
|
2060
|
+
effect((onCleanup) => {
|
|
2061
|
+
const size = 'md3-' + this.size();
|
|
2062
|
+
this.element.classList.add(size);
|
|
2063
|
+
onCleanup(() => {
|
|
2064
|
+
this.element.classList.remove(size);
|
|
2065
|
+
});
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
get element() {
|
|
2069
|
+
return this.el.nativeElement;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Applies a resolved placement.
|
|
2073
|
+
*
|
|
2074
|
+
* Called from the carousel's scroll handler on every frame, so this writes to the DOM
|
|
2075
|
+
* directly and only touches signals when a value actually changes.
|
|
2076
|
+
*/
|
|
2077
|
+
applyGeometry(geometry, itemSize) {
|
|
2078
|
+
const style = this.element.style;
|
|
2079
|
+
if (!geometry.isVisible) {
|
|
2080
|
+
this.element.setAttribute('hidden', '');
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
this.element.removeAttribute('hidden');
|
|
2084
|
+
style.setProperty('--md3-carousel-item-size', `${geometry.maskedSize}px`);
|
|
2085
|
+
style.setProperty('--md3-carousel-item-full-size', `${itemSize}px`);
|
|
2086
|
+
style.setProperty('--md3-carousel-item-offset', `${geometry.offset}px`);
|
|
2087
|
+
style.setProperty('--md3-carousel-item-mask-ratio', `${geometry.maskRatio}`);
|
|
2088
|
+
if (this.maskedSize() !== geometry.maskedSize) {
|
|
2089
|
+
this.maskedSize.set(geometry.maskedSize);
|
|
2090
|
+
}
|
|
2091
|
+
if (this.maskRatio() !== geometry.maskRatio) {
|
|
2092
|
+
this.maskRatio.set(geometry.maskRatio);
|
|
2093
|
+
}
|
|
2094
|
+
if (this.size() !== geometry.size) {
|
|
2095
|
+
this.size.set(geometry.size);
|
|
2096
|
+
}
|
|
2097
|
+
if (this.isFocal() !== geometry.isFocal) {
|
|
2098
|
+
this.isFocal.set(geometry.isFocal);
|
|
2099
|
+
this.element.classList.toggle('md3-focal', geometry.isFocal);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CarouselItem, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2103
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.0", type: CarouselItem, isStandalone: true, selector: "md3-carousel-item", host: { attributes: { "role": "group", "aria-roledescription": "slide" } }, ngImport: i0, template: "<div class=\"md3-carousel-item-content\">\n <ng-content></ng-content>\n</div>\n", styles: [":host{--md3-carousel-item-size: 0px;--md3-carousel-item-full-size: 0px;--md3-carousel-item-offset: 0px;--md3-carousel-item-mask-ratio: 0;--background-color: rgb(var(--md-scheme-surface-container-high));--border-radius: var(--md-border-radius-xlarge);--md3-carousel-item-gap: var(--md3-carousel-gap, 0px);--md3-carousel-item-visible-size: max( 0px, calc(var(--md3-carousel-item-size) - var(--md3-carousel-item-gap)) );--md3-carousel-item-visible-full-size: max( 0px, calc(var(--md3-carousel-item-full-size) - var(--md3-carousel-item-gap)) );position:absolute;inset-block:0;inset-inline-start:calc(var(--md3-carousel-item-offset) + var(--md3-carousel-item-gap) / 2);inline-size:var(--md3-carousel-item-visible-size);overflow:hidden;border-radius:var(--border-radius);background-color:var(--background-color);contain:paint}:host([hidden]){display:none}.md3-carousel-item-content{position:absolute;inset-block:0;inset-inline-start:50%;inline-size:var(--md3-carousel-item-visible-full-size);margin-inline-start:calc(var(--md3-carousel-item-visible-full-size) / -2)}:host ::ng-deep .md3-carousel-item-content>img,:host ::ng-deep .md3-carousel-item-content>video,:host ::ng-deep .md3-carousel-item-content>picture{display:block;inline-size:100%;block-size:100%;object-fit:cover}\n"] });
|
|
2104
|
+
}
|
|
2105
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CarouselItem, decorators: [{
|
|
2106
|
+
type: Component,
|
|
2107
|
+
args: [{ selector: 'md3-carousel-item', host: {
|
|
2108
|
+
'role': 'group',
|
|
2109
|
+
'aria-roledescription': 'slide',
|
|
2110
|
+
}, template: "<div class=\"md3-carousel-item-content\">\n <ng-content></ng-content>\n</div>\n", styles: [":host{--md3-carousel-item-size: 0px;--md3-carousel-item-full-size: 0px;--md3-carousel-item-offset: 0px;--md3-carousel-item-mask-ratio: 0;--background-color: rgb(var(--md-scheme-surface-container-high));--border-radius: var(--md-border-radius-xlarge);--md3-carousel-item-gap: var(--md3-carousel-gap, 0px);--md3-carousel-item-visible-size: max( 0px, calc(var(--md3-carousel-item-size) - var(--md3-carousel-item-gap)) );--md3-carousel-item-visible-full-size: max( 0px, calc(var(--md3-carousel-item-full-size) - var(--md3-carousel-item-gap)) );position:absolute;inset-block:0;inset-inline-start:calc(var(--md3-carousel-item-offset) + var(--md3-carousel-item-gap) / 2);inline-size:var(--md3-carousel-item-visible-size);overflow:hidden;border-radius:var(--border-radius);background-color:var(--background-color);contain:paint}:host([hidden]){display:none}.md3-carousel-item-content{position:absolute;inset-block:0;inset-inline-start:50%;inline-size:var(--md3-carousel-item-visible-full-size);margin-inline-start:calc(var(--md3-carousel-item-visible-full-size) / -2)}:host ::ng-deep .md3-carousel-item-content>img,:host ::ng-deep .md3-carousel-item-content>video,:host ::ng-deep .md3-carousel-item-content>picture{display:block;inline-size:100%;block-size:100%;object-fit:cover}\n"] }]
|
|
2111
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }] });
|
|
2112
|
+
|
|
2113
|
+
/** Grace period after a programmatic scroll before the index is synced back from the DOM. */
|
|
2114
|
+
const SCROLL_END_FALLBACK = 120;
|
|
2115
|
+
/**
|
|
2116
|
+
* A Material Design 3 carousel.
|
|
2117
|
+
*
|
|
2118
|
+
* Items are laid out against a set of keylines — large, medium, small and an off-screen anchor —
|
|
2119
|
+
* and adopt the size of whichever keyline they are passing through, so they grow into the focal
|
|
2120
|
+
* range and crop away as they leave it.
|
|
2121
|
+
*
|
|
2122
|
+
* Scrolling is native, which keeps momentum, touch, snapping and the scrollbar intact. Only the
|
|
2123
|
+
* visual sizing is derived in TypeScript, and it is written straight to CSS custom properties so
|
|
2124
|
+
* scrolling never triggers change detection.
|
|
2125
|
+
*
|
|
2126
|
+
* All size inputs are in pixels.
|
|
2127
|
+
*
|
|
2128
|
+
* ```html
|
|
2129
|
+
* <md3-carousel [(index)]="selected" [item-size]="200">
|
|
2130
|
+
* @for (photo of photos(); track photo.id) {
|
|
2131
|
+
* <md3-carousel-item>
|
|
2132
|
+
* <img [src]="photo.url" [alt]="photo.alt" />
|
|
2133
|
+
* </md3-carousel-item>
|
|
2134
|
+
* }
|
|
2135
|
+
* </md3-carousel>
|
|
2136
|
+
* ```
|
|
2137
|
+
*/
|
|
2138
|
+
class Carousel {
|
|
2139
|
+
el;
|
|
2140
|
+
/** Arrangement strategy. Additional Material Design layouts will widen this type. */
|
|
2141
|
+
carouselLayout = input('multi-browse', { ...(ngDevMode ? { debugName: "carouselLayout" } : /* istanbul ignore next */ {}), alias: 'carousel-layout' });
|
|
2142
|
+
/** Where the focal (large) items sit within the container. */
|
|
2143
|
+
alignment = input('start', { ...(ngDevMode ? { debugName: "alignment" } : /* istanbul ignore next */ {}), alias: 'alignment' });
|
|
2144
|
+
/** Scroll axis. Only `horizontal` is implemented today. */
|
|
2145
|
+
orientation = input('horizontal', { ...(ngDevMode ? { debugName: "orientation" } : /* istanbul ignore next */ {}), alias: 'orientation' });
|
|
2146
|
+
/** Preferred size of a fully unmasked item, in pixels. */
|
|
2147
|
+
itemSize = input(200, { ...(ngDevMode ? { debugName: "itemSize" } : /* istanbul ignore next */ {}), alias: 'item-size',
|
|
2148
|
+
transform: numberAttribute });
|
|
2149
|
+
/** Smallest a small item may shrink to, in pixels. */
|
|
2150
|
+
smallItemSizeMin = input(40, { ...(ngDevMode ? { debugName: "smallItemSizeMin" } : /* istanbul ignore next */ {}), alias: 'small-item-size-min',
|
|
2151
|
+
transform: numberAttribute });
|
|
2152
|
+
/** Largest a small item may grow to, in pixels. */
|
|
2153
|
+
smallItemSizeMax = input(56, { ...(ngDevMode ? { debugName: "smallItemSizeMax" } : /* istanbul ignore next */ {}), alias: 'small-item-size-max',
|
|
2154
|
+
transform: numberAttribute });
|
|
2155
|
+
/** Space between items, in pixels. */
|
|
2156
|
+
gap = input(8, { ...(ngDevMode ? { debugName: "gap" } : /* istanbul ignore next */ {}), alias: 'gap',
|
|
2157
|
+
transform: numberAttribute });
|
|
2158
|
+
/**
|
|
2159
|
+
* Index of the item leading the focal range. Two-way bindable.
|
|
2160
|
+
*
|
|
2161
|
+
* Clamped to {@link lastIndex}: the final items share the focal range and are all visible at
|
|
2162
|
+
* once, so there is no scroll position where any of them leads on its own. Setting a higher
|
|
2163
|
+
* value scrolls to the end and reads back as `lastIndex`.
|
|
2164
|
+
*/
|
|
2165
|
+
index = model(0, /* @ts-ignore */
|
|
2166
|
+
...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
|
|
2167
|
+
/** Items projected into the carousel, in document order. */
|
|
2168
|
+
items = contentChildren(CarouselItem, { ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), descendants: true });
|
|
2169
|
+
/**
|
|
2170
|
+
* Whether the current layout brings items to rest on keylines.
|
|
2171
|
+
*
|
|
2172
|
+
* Decided by the layout, not by the consumer: an arrangement that resizes items only reads
|
|
2173
|
+
* correctly when they are sitting on keylines.
|
|
2174
|
+
*/
|
|
2175
|
+
snap = computed(() => CAROUSEL_STRATEGIES[this.carouselLayout()].snap, /* @ts-ignore */
|
|
2176
|
+
...(ngDevMode ? [{ debugName: "snap" }] : /* istanbul ignore next */ []));
|
|
2177
|
+
scroller = viewChild.required('scroller', /* @ts-ignore */
|
|
2178
|
+
...(ngDevMode ? [{ debugName: "scroller" }] : /* istanbul ignore next */ []));
|
|
2179
|
+
containerSize = signal(0, /* @ts-ignore */
|
|
2180
|
+
...(ngDevMode ? [{ debugName: "containerSize" }] : /* istanbul ignore next */ []));
|
|
2181
|
+
directionality = inject(Directionality, { optional: true });
|
|
2182
|
+
/**
|
|
2183
|
+
* Sizes handed to the solver.
|
|
2184
|
+
*
|
|
2185
|
+
* Gaps are folded into item sizes so the solver only has one quantity to balance, then
|
|
2186
|
+
* removed again visually by insetting each item half a gap on both sides.
|
|
2187
|
+
*/
|
|
2188
|
+
metrics = computed(() => {
|
|
2189
|
+
const gap = this.gap();
|
|
2190
|
+
return {
|
|
2191
|
+
gap,
|
|
2192
|
+
itemSize: this.itemSize() + gap,
|
|
2193
|
+
smallSizeMin: this.smallItemSizeMin() + gap,
|
|
2194
|
+
smallSizeMax: this.smallItemSizeMax() + gap,
|
|
2195
|
+
};
|
|
2196
|
+
}, /* @ts-ignore */
|
|
2197
|
+
...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
|
|
2198
|
+
/** The solved distribution of large, medium and small items for the current container. */
|
|
2199
|
+
arrangement = computed(() => {
|
|
2200
|
+
const containerSize = this.containerSize();
|
|
2201
|
+
const itemCount = this.items().length;
|
|
2202
|
+
const { itemSize, smallSizeMin, smallSizeMax } = this.metrics();
|
|
2203
|
+
if (containerSize <= 0 || itemCount === 0 || itemSize <= 0) {
|
|
2204
|
+
return undefined;
|
|
2205
|
+
}
|
|
2206
|
+
return CAROUSEL_STRATEGIES[this.carouselLayout()].arrange({
|
|
2207
|
+
containerSize,
|
|
2208
|
+
itemSize,
|
|
2209
|
+
smallSizeMin,
|
|
2210
|
+
smallSizeMax,
|
|
2211
|
+
itemCount,
|
|
2212
|
+
alignment: this.alignment(),
|
|
2213
|
+
});
|
|
2214
|
+
}, /* @ts-ignore */
|
|
2215
|
+
...(ngDevMode ? [{ debugName: "arrangement" }] : /* istanbul ignore next */ []));
|
|
2216
|
+
/** Keyline geometry derived from the arrangement. */
|
|
2217
|
+
geometry = computed(() => {
|
|
2218
|
+
const arrangement = this.arrangement();
|
|
2219
|
+
if (!arrangement) {
|
|
2220
|
+
return undefined;
|
|
2221
|
+
}
|
|
2222
|
+
return buildGeometry(arrangement, this.alignment(), this.items().length, this.containerSize());
|
|
2223
|
+
}, /* @ts-ignore */
|
|
2224
|
+
...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
|
|
2225
|
+
/** Scroll offsets that items snap to, one per item. */
|
|
2226
|
+
snapPoints = computed(() => {
|
|
2227
|
+
const geometry = this.geometry();
|
|
2228
|
+
if (!geometry) {
|
|
2229
|
+
return [];
|
|
2230
|
+
}
|
|
2231
|
+
return this.items().map((_, index) => scrollOffsetForIndex(geometry, index));
|
|
2232
|
+
}, /* @ts-ignore */
|
|
2233
|
+
...(ngDevMode ? [{ debugName: "snapPoints" }] : /* istanbul ignore next */ []));
|
|
2234
|
+
/**
|
|
2235
|
+
* Highest index the carousel can rest on.
|
|
2236
|
+
*
|
|
2237
|
+
* Lower than the last item's index, because the trailing items share the focal range once
|
|
2238
|
+
* the carousel is scrolled to the end. It moves as the container is resized, since a wider
|
|
2239
|
+
* container fits more items in the focal range.
|
|
2240
|
+
*/
|
|
2241
|
+
lastIndex = computed(() => this.geometry()?.lastIndex ?? 0, /* @ts-ignore */
|
|
2242
|
+
...(ngDevMode ? [{ debugName: "lastIndex" }] : /* istanbul ignore next */ []));
|
|
2243
|
+
atStart = computed(() => this.index() <= 0, /* @ts-ignore */
|
|
2244
|
+
...(ngDevMode ? [{ debugName: "atStart" }] : /* istanbul ignore next */ []));
|
|
2245
|
+
/** True when the carousel cannot scroll any further towards the end. */
|
|
2246
|
+
atEnd = computed(() => this.index() >= this.lastIndex(), /* @ts-ignore */
|
|
2247
|
+
...(ngDevMode ? [{ debugName: "atEnd" }] : /* istanbul ignore next */ []));
|
|
2248
|
+
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
2249
|
+
scrollEndTimer;
|
|
2250
|
+
frame = 0;
|
|
2251
|
+
/**
|
|
2252
|
+
* Target of an in-flight programmatic scroll.
|
|
2253
|
+
*
|
|
2254
|
+
* Smooth scrolling reports intermediate offsets, and reading the index back from those would
|
|
2255
|
+
* fight the animation, so index syncing pauses until the target is reached or the user takes
|
|
2256
|
+
* over.
|
|
2257
|
+
*/
|
|
2258
|
+
pendingScroll;
|
|
2259
|
+
constructor(el) {
|
|
2260
|
+
this.el = el;
|
|
2261
|
+
// ResizeObserver alone would leave the carousel unmeasured until its first callback, so
|
|
2262
|
+
// take an initial measurement as soon as the view exists.
|
|
2263
|
+
afterNextRender(() => this.measure());
|
|
2264
|
+
effect((onCleanup) => {
|
|
2265
|
+
if (!this.isBrowser) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const observer = new ResizeObserver(() => this.measure());
|
|
2269
|
+
observer.observe(this.element);
|
|
2270
|
+
onCleanup(() => observer.disconnect());
|
|
2271
|
+
});
|
|
2272
|
+
effect((onCleanup) => {
|
|
2273
|
+
if (!this.isBrowser) {
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
const scroller = this.scroller().nativeElement;
|
|
2277
|
+
const onScroll = () => this.scheduleGeometry();
|
|
2278
|
+
const onScrollEnd = () => this.syncIndexFromScroll();
|
|
2279
|
+
const onInteract = () => {
|
|
2280
|
+
this.pendingScroll = undefined;
|
|
2281
|
+
};
|
|
2282
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
2283
|
+
scroller.addEventListener('scrollend', onScrollEnd);
|
|
2284
|
+
scroller.addEventListener('pointerdown', onInteract, { passive: true });
|
|
2285
|
+
scroller.addEventListener('wheel', onInteract, { passive: true });
|
|
2286
|
+
scroller.addEventListener('touchstart', onInteract, { passive: true });
|
|
2287
|
+
onCleanup(() => {
|
|
2288
|
+
clearTimeout(this.scrollEndTimer);
|
|
2289
|
+
cancelAnimationFrame(this.frame);
|
|
2290
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
2291
|
+
scroller.removeEventListener('scrollend', onScrollEnd);
|
|
2292
|
+
scroller.removeEventListener('pointerdown', onInteract);
|
|
2293
|
+
scroller.removeEventListener('wheel', onInteract);
|
|
2294
|
+
scroller.removeEventListener('touchstart', onInteract);
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
// Re-place every item whenever the arrangement or the item list changes. Reading the
|
|
2298
|
+
// signals here is what registers the dependency.
|
|
2299
|
+
effect(() => {
|
|
2300
|
+
this.geometry();
|
|
2301
|
+
this.items();
|
|
2302
|
+
this.applyGeometry();
|
|
2303
|
+
});
|
|
2304
|
+
// Follow the index when it is set from outside, but not while the user is scrolling —
|
|
2305
|
+
// `syncIndexFromScroll` only writes a value the DOM already agrees with.
|
|
2306
|
+
effect(() => {
|
|
2307
|
+
const index = this.index();
|
|
2308
|
+
const geometry = this.geometry();
|
|
2309
|
+
if (!geometry || !this.isBrowser) {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
// A resize changes how many items share the focal range, so an index that was
|
|
2313
|
+
// reachable before may no longer be.
|
|
2314
|
+
const clamped = clampIndex(geometry, index);
|
|
2315
|
+
if (clamped !== index) {
|
|
2316
|
+
this.index.set(clamped);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const target = scrollOffsetForIndex(geometry, clamped);
|
|
2320
|
+
if (Math.abs(this.scrollOffset() - target) > 1) {
|
|
2321
|
+
this.scrollTo(target, 'smooth');
|
|
2322
|
+
}
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
get element() {
|
|
2326
|
+
return this.el.nativeElement;
|
|
2327
|
+
}
|
|
2328
|
+
/** True when laid out right-to-left. */
|
|
2329
|
+
get isRtl() {
|
|
2330
|
+
return this.directionality?.value === 'rtl'
|
|
2331
|
+
|| getComputedStyle(this.element).direction === 'rtl';
|
|
2332
|
+
}
|
|
2333
|
+
onKeydown(event) {
|
|
2334
|
+
// Arrow keys are direction-agnostic here: ArrowRight always moves towards the visual
|
|
2335
|
+
// right, which under RTL is the previous item.
|
|
2336
|
+
const forward = this.isRtl ? 'ArrowLeft' : 'ArrowRight';
|
|
2337
|
+
const backward = this.isRtl ? 'ArrowRight' : 'ArrowLeft';
|
|
2338
|
+
switch (event.key) {
|
|
2339
|
+
case forward:
|
|
2340
|
+
this.next();
|
|
2341
|
+
break;
|
|
2342
|
+
case backward:
|
|
2343
|
+
this.previous();
|
|
2344
|
+
break;
|
|
2345
|
+
case 'Home':
|
|
2346
|
+
this.scrollToIndex(0);
|
|
2347
|
+
break;
|
|
2348
|
+
case 'End':
|
|
2349
|
+
this.scrollToIndex(this.lastIndex());
|
|
2350
|
+
break;
|
|
2351
|
+
default:
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
event.preventDefault();
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Keeps a focused item in view.
|
|
2358
|
+
*
|
|
2359
|
+
* Items are absolutely positioned, so the browser cannot scroll them into view on its own.
|
|
2360
|
+
* Without this, tabbing to a link inside a cropped item would leave it invisible.
|
|
2361
|
+
*/
|
|
2362
|
+
onFocusIn(event) {
|
|
2363
|
+
const target = event.target;
|
|
2364
|
+
if (!(target instanceof Node)) {
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
const index = this.items().findIndex((item) => item.element.contains(target));
|
|
2368
|
+
if (index >= 0 && index !== this.index()) {
|
|
2369
|
+
this.scrollToIndex(index);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
/** Scrolls until `index` leads the focal range, clamping to {@link lastIndex}. */
|
|
2373
|
+
scrollToIndex(index, behavior = 'smooth') {
|
|
2374
|
+
const geometry = this.geometry();
|
|
2375
|
+
if (!geometry) {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const clamped = clampIndex(geometry, index);
|
|
2379
|
+
this.index.set(clamped);
|
|
2380
|
+
this.scrollTo(scrollOffsetForIndex(geometry, clamped), behavior);
|
|
2381
|
+
}
|
|
2382
|
+
next() {
|
|
2383
|
+
this.scrollToIndex(this.index() + 1);
|
|
2384
|
+
}
|
|
2385
|
+
previous() {
|
|
2386
|
+
this.scrollToIndex(this.index() - 1);
|
|
2387
|
+
}
|
|
2388
|
+
measure() {
|
|
2389
|
+
if (!this.isBrowser) {
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
this.containerSize.set(this.scroller().nativeElement.clientWidth);
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Current scroll position as a positive offset from the logical start edge.
|
|
2396
|
+
*
|
|
2397
|
+
* Right-to-left scroll containers report `scrollLeft` as zero at the start and negative
|
|
2398
|
+
* moving away from it, so the magnitude is the logical offset in both directions.
|
|
2399
|
+
*/
|
|
2400
|
+
scrollOffset() {
|
|
2401
|
+
return Math.abs(this.scroller().nativeElement.scrollLeft);
|
|
2402
|
+
}
|
|
2403
|
+
scrollTo(offset, behavior) {
|
|
2404
|
+
if (!this.isBrowser) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
2408
|
+
this.pendingScroll = offset;
|
|
2409
|
+
this.scroller().nativeElement.scrollTo({
|
|
2410
|
+
left: this.isRtl ? -offset : offset,
|
|
2411
|
+
behavior: reduceMotion ? 'auto' : behavior,
|
|
2412
|
+
});
|
|
2413
|
+
this.scheduleIndexSync();
|
|
2414
|
+
}
|
|
2415
|
+
scheduleGeometry() {
|
|
2416
|
+
if (this.frame) {
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
this.frame = requestAnimationFrame(() => {
|
|
2420
|
+
this.frame = 0;
|
|
2421
|
+
this.applyGeometry();
|
|
2422
|
+
});
|
|
2423
|
+
this.scheduleIndexSync();
|
|
2424
|
+
}
|
|
2425
|
+
/** `scrollend` is not universally supported, so back it with a timer. */
|
|
2426
|
+
scheduleIndexSync() {
|
|
2427
|
+
clearTimeout(this.scrollEndTimer);
|
|
2428
|
+
this.scrollEndTimer = setTimeout(() => this.syncIndexFromScroll(), SCROLL_END_FALLBACK);
|
|
2429
|
+
}
|
|
2430
|
+
syncIndexFromScroll() {
|
|
2431
|
+
const geometry = this.geometry();
|
|
2432
|
+
if (!geometry || !this.isBrowser) {
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
const offset = this.scrollOffset();
|
|
2436
|
+
// Wait for a programmatic scroll to land rather than reading the index off an
|
|
2437
|
+
// intermediate frame of the animation.
|
|
2438
|
+
if (this.pendingScroll !== undefined) {
|
|
2439
|
+
if (Math.abs(offset - this.pendingScroll) > 1) {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
this.pendingScroll = undefined;
|
|
2443
|
+
}
|
|
2444
|
+
// Once the final items share the focal range they also share a scroll offset, so an
|
|
2445
|
+
// index that already resolves to this offset is left alone.
|
|
2446
|
+
if (Math.abs(scrollOffsetForIndex(geometry, this.index()) - offset) <= 1) {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const index = indexForScrollOffset(geometry, offset);
|
|
2450
|
+
if (index !== this.index()) {
|
|
2451
|
+
this.index.set(index);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Writes the current placement of every item.
|
|
2456
|
+
*
|
|
2457
|
+
* Runs on every scroll frame, so it touches the DOM directly and allocates nothing beyond
|
|
2458
|
+
* the resolved state.
|
|
2459
|
+
*/
|
|
2460
|
+
applyGeometry() {
|
|
2461
|
+
const geometry = this.geometry();
|
|
2462
|
+
const items = this.items();
|
|
2463
|
+
if (!geometry) {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
const style = this.element.style;
|
|
2467
|
+
style.setProperty('--md3-carousel-scroll-size', `${geometry.scrollSize}px`);
|
|
2468
|
+
style.setProperty('--md3-carousel-viewport-size', `${this.containerSize()}px`);
|
|
2469
|
+
style.setProperty('--md3-carousel-gap', `${this.metrics().gap}px`);
|
|
2470
|
+
const scrollOffset = this.scrollOffset();
|
|
2471
|
+
const state = resolveState(geometry, scrollOffset);
|
|
2472
|
+
const itemCount = items.length;
|
|
2473
|
+
items.forEach((item, index) => {
|
|
2474
|
+
item.applyGeometry(resolveItemGeometry(geometry, state, index, scrollOffset), geometry.itemSize);
|
|
2475
|
+
const label = `${index + 1} of ${itemCount}`;
|
|
2476
|
+
if (item.element.getAttribute('aria-label') !== label) {
|
|
2477
|
+
item.element.setAttribute('aria-label', label);
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Carousel, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2482
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: Carousel, isStandalone: true, selector: "md3-carousel", inputs: { carouselLayout: { classPropertyName: "carouselLayout", publicName: "carousel-layout", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "item-size", isSignal: true, isRequired: false, transformFunction: null }, smallItemSizeMin: { classPropertyName: "smallItemSizeMin", publicName: "small-item-size-min", isSignal: true, isRequired: false, transformFunction: null }, smallItemSizeMax: { classPropertyName: "smallItemSizeMax", publicName: "small-item-size-max", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { index: "indexChange" }, host: { attributes: { "role": "group", "aria-roledescription": "carousel" }, listeners: { "keydown": "onKeydown($event)", "focusin": "onFocusIn($event)" } }, queries: [{ propertyName: "items", predicate: CarouselItem, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #scroller class=\"md3-carousel-scroller\" tabindex=\"0\" [class.md3-snap]=\"snap()\">\n <div class=\"md3-carousel-content\">\n <div class=\"md3-carousel-viewport\">\n <ng-content></ng-content>\n </div>\n\n @for (point of snapPoints(); track $index) {\n <div class=\"md3-carousel-snap\" aria-hidden=\"true\" [style.inset-inline-start.px]=\"point\"></div>\n }\n </div>\n</div>\n", styles: [":host{--md3-carousel-height: 14em;--md3-carousel-scroll-size: 100%;--md3-carousel-viewport-size: 100%;--md3-carousel-gap: 0px;--outline-color: rgb(var(--md-scheme-secondary));--outline-width: .1875em;--outline-offset: .125em;font-size:1rem;display:block;inline-size:100%;block-size:var(--md3-carousel-height)}.md3-carousel-scroller{block-size:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;outline:none}.md3-carousel-scroller::-webkit-scrollbar{display:none}.md3-carousel-scroller:focus-visible{outline:var(--outline-width) solid var(--outline-color);outline-offset:var(--outline-offset);border-radius:var(--md-border-radius-xlarge)}.md3-carousel-scroller.md3-snap{scroll-snap-type:x mandatory}.md3-carousel-content{position:relative;inline-size:var(--md3-carousel-scroll-size);block-size:100%}.md3-carousel-viewport{position:sticky;inset-inline-start:0;inset-block-start:0;inline-size:var(--md3-carousel-viewport-size);block-size:100%}.md3-carousel-snap{position:absolute;inset-block-start:0;inline-size:1px;block-size:1px;scroll-snap-align:start;pointer-events:none;visibility:hidden}@media(prefers-reduced-motion:reduce){.md3-carousel-scroller{scroll-behavior:auto}}\n"] });
|
|
2483
|
+
}
|
|
2484
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Carousel, decorators: [{
|
|
2485
|
+
type: Component,
|
|
2486
|
+
args: [{ selector: 'md3-carousel', host: {
|
|
2487
|
+
'role': 'group',
|
|
2488
|
+
'aria-roledescription': 'carousel',
|
|
2489
|
+
}, template: "<div #scroller class=\"md3-carousel-scroller\" tabindex=\"0\" [class.md3-snap]=\"snap()\">\n <div class=\"md3-carousel-content\">\n <div class=\"md3-carousel-viewport\">\n <ng-content></ng-content>\n </div>\n\n @for (point of snapPoints(); track $index) {\n <div class=\"md3-carousel-snap\" aria-hidden=\"true\" [style.inset-inline-start.px]=\"point\"></div>\n }\n </div>\n</div>\n", styles: [":host{--md3-carousel-height: 14em;--md3-carousel-scroll-size: 100%;--md3-carousel-viewport-size: 100%;--md3-carousel-gap: 0px;--outline-color: rgb(var(--md-scheme-secondary));--outline-width: .1875em;--outline-offset: .125em;font-size:1rem;display:block;inline-size:100%;block-size:var(--md3-carousel-height)}.md3-carousel-scroller{block-size:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;outline:none}.md3-carousel-scroller::-webkit-scrollbar{display:none}.md3-carousel-scroller:focus-visible{outline:var(--outline-width) solid var(--outline-color);outline-offset:var(--outline-offset);border-radius:var(--md-border-radius-xlarge)}.md3-carousel-scroller.md3-snap{scroll-snap-type:x mandatory}.md3-carousel-content{position:relative;inline-size:var(--md3-carousel-scroll-size);block-size:100%}.md3-carousel-viewport{position:sticky;inset-inline-start:0;inset-block-start:0;inline-size:var(--md3-carousel-viewport-size);block-size:100%}.md3-carousel-snap{position:absolute;inset-block-start:0;inline-size:1px;block-size:1px;scroll-snap-align:start;pointer-events:none;visibility:hidden}@media(prefers-reduced-motion:reduce){.md3-carousel-scroller{scroll-behavior:auto}}\n"] }]
|
|
2490
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { carouselLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "carousel-layout", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], itemSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "item-size", required: false }] }], smallItemSizeMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "small-item-size-min", required: false }] }], smallItemSizeMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "small-item-size-max", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }, { type: i0.Output, args: ["indexChange"] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CarouselItem), { ...{ descendants: true }, isSignal: true }] }], scroller: [{ type: i0.ViewChild, args: ['scroller', { isSignal: true }] }], onKeydown: [{
|
|
2491
|
+
type: HostListener,
|
|
2492
|
+
args: ['keydown', ['$event']]
|
|
2493
|
+
}], onFocusIn: [{
|
|
2494
|
+
type: HostListener,
|
|
2495
|
+
args: ['focusin', ['$event']]
|
|
2496
|
+
}] } });
|
|
2497
|
+
|
|
1556
2498
|
class ChipAvatar {
|
|
1557
2499
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ChipAvatar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1558
2500
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.0", type: ChipAvatar, isStandalone: true, selector: "[md3-chip-avatar]", ngImport: i0 });
|
|
@@ -3115,6 +4057,7 @@ class DialogRef {
|
|
|
3115
4057
|
isFullScreen;
|
|
3116
4058
|
closed = new Subject();
|
|
3117
4059
|
closing = new Subject();
|
|
4060
|
+
hiddenChanges = new Subject();
|
|
3118
4061
|
closePromise = null;
|
|
3119
4062
|
closeStarted = false;
|
|
3120
4063
|
closeSettled = false;
|
|
@@ -3134,7 +4077,7 @@ class DialogRef {
|
|
|
3134
4077
|
get isClosing() {
|
|
3135
4078
|
return this.closeStarted;
|
|
3136
4079
|
}
|
|
3137
|
-
/** Whether the dialog is currently hidden behind another dialog. */
|
|
4080
|
+
/** Whether the dialog is currently hidden, on request or behind another dialog. */
|
|
3138
4081
|
get isHidden() {
|
|
3139
4082
|
return this.hidden;
|
|
3140
4083
|
}
|
|
@@ -3163,6 +4106,7 @@ class DialogRef {
|
|
|
3163
4106
|
/**
|
|
3164
4107
|
* Hides the dialog with the closing animation while keeping it alive, so
|
|
3165
4108
|
* its content, form state and subscriptions survive until show() is called.
|
|
4109
|
+
* The page underneath becomes usable again while the dialog waits behind it.
|
|
3166
4110
|
* The promise resolves once the dialog is out of sight.
|
|
3167
4111
|
*/
|
|
3168
4112
|
hide() {
|
|
@@ -3172,6 +4116,7 @@ class DialogRef {
|
|
|
3172
4116
|
this.hidden = true;
|
|
3173
4117
|
this.toggleHiddenState(true);
|
|
3174
4118
|
this.dialogInstance?.setActive(false);
|
|
4119
|
+
this.hiddenChanges.next(true);
|
|
3175
4120
|
return this.waitForSurfaceAnimation();
|
|
3176
4121
|
}
|
|
3177
4122
|
/**
|
|
@@ -3187,6 +4132,7 @@ class DialogRef {
|
|
|
3187
4132
|
this.hidden = false;
|
|
3188
4133
|
this.toggleHiddenState(false);
|
|
3189
4134
|
this.dialogInstance?.setActive(true);
|
|
4135
|
+
this.hiddenChanges.next(false);
|
|
3190
4136
|
}
|
|
3191
4137
|
this.dialogInstance?.recaptureFocus();
|
|
3192
4138
|
}
|
|
@@ -3194,6 +4140,10 @@ class DialogRef {
|
|
|
3194
4140
|
beforeClosed() {
|
|
3195
4141
|
return this.closing.asObservable();
|
|
3196
4142
|
}
|
|
4143
|
+
/** Emits the new value of `isHidden` every time the dialog is hidden or shown. */
|
|
4144
|
+
hiddenChanged() {
|
|
4145
|
+
return this.hiddenChanges.asObservable();
|
|
4146
|
+
}
|
|
3197
4147
|
afterClosed() {
|
|
3198
4148
|
return this.closed.asObservable();
|
|
3199
4149
|
}
|
|
@@ -3204,7 +4154,9 @@ class DialogRef {
|
|
|
3204
4154
|
this.cdkRef.containerInstance._closeInteractionType = 'mouse';
|
|
3205
4155
|
this.close();
|
|
3206
4156
|
});
|
|
3207
|
-
|
|
4157
|
+
// A hidden dialog is not the one the Escape key is meant for, even
|
|
4158
|
+
// though the overlay it sits in is still the one receiving the event.
|
|
4159
|
+
this.cdkRef.keydownEvents.pipe(filter((event) => !this.hidden && event.key === 'Escape' && !hasModifierKey(event)), take(1)).subscribe((event) => {
|
|
3208
4160
|
event.preventDefault();
|
|
3209
4161
|
this.cdkRef.containerInstance._closeInteractionType = 'keyboard';
|
|
3210
4162
|
this.close();
|
|
@@ -3276,6 +4228,7 @@ class DialogRef {
|
|
|
3276
4228
|
// waiting on beforeClosed() is still notified.
|
|
3277
4229
|
this.startClosing();
|
|
3278
4230
|
this.closeSettled = true;
|
|
4231
|
+
this.hiddenChanges.complete();
|
|
3279
4232
|
this.closed.next(result);
|
|
3280
4233
|
this.closed.complete();
|
|
3281
4234
|
}
|
|
@@ -3312,13 +4265,13 @@ class Dialog extends CdkDialogContainer {
|
|
|
3312
4265
|
this._recaptureFocus();
|
|
3313
4266
|
}
|
|
3314
4267
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3315
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: Dialog, isStandalone: true, selector: "md3-dialog", viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
|
|
4268
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.0", type: Dialog, isStandalone: true, selector: "md3-dialog", viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0;pointer-events:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"], dependencies: [{ kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }] });
|
|
3316
4269
|
}
|
|
3317
4270
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Dialog, decorators: [{
|
|
3318
4271
|
type: Component,
|
|
3319
4272
|
args: [{ selector: 'md3-dialog', imports: [
|
|
3320
4273
|
CdkPortalOutlet,
|
|
3321
|
-
], template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"] }]
|
|
4274
|
+
], template: "<!-- The role and aria attributes live on the host element, applied by the CDK dialog container. -->\n<section\n #surface\n class=\"md3-dialog-container\"\n [class.md3-active]=\"isActive()\"\n [class.md-scheme-dark]=\"config.scheme == 'dark'\"\n [class.md-scheme-light]=\"config.scheme == 'light'\"\n [attr.dir]=\"config.direction ?? null\">\n <ng-template cdkPortalOutlet></ng-template>\n</section>\n", styles: ["::ng-deep .cdk-overlay-container{position:fixed;z-index:1000;inset:0;pointer-events:none;font-size:1rem}::ng-deep .cdk-overlay-container .md3-dialog-scrim{position:absolute;inset:0;pointer-events:auto;background-color:rgb(var(--md-scheme-scrim));transition:all var(--md-motion-expressive-slow-spatial-duration) var(--md-motion-expressive-slow-spatial-easing);opacity:.32}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-opening{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-closing{opacity:0}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-hidden{opacity:0;pointer-events:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-kept{opacity:.32;transition:none}::ng-deep .cdk-overlay-container .md3-dialog-scrim.md3-dialog-scrim-instant{transition:none}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper{position:absolute;display:flex;inset:0;pointer-events:none;padding:3.5rem 1.5rem;overflow:hidden}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel{position:absolute;pointer-events:auto;max-width:35em}::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-closing,::ng-deep .cdk-overlay-container .cdk-global-overlay-wrapper .md3-dialog-panel.md3-dialog-hidden{pointer-events:none}:host{display:block;max-width:100%;min-width:17.5em;outline:0}:host .md3-dialog-container{display:block;width:100%;max-height:inherit;overflow:auto;color:rgb(var(--md-scheme-on-surface));background-color:rgb(var(--md-scheme-surface-container-high));border-radius:var(--md-border-radius-xlarge);box-shadow:var(--md-shadow-6dp);transform-origin:center;transition:all var(--md-motion-expressive-default-spatial-duration) var(--md-motion-expressive-default-spatial-easing);opacity:0;transform:scaleY(.8);margin-top:-8rem}:host .md3-dialog-container.md3-active{opacity:1;transform:scaleY(1);margin-top:0}\n"] }]
|
|
3322
4275
|
}], propDecorators: { surface: [{ type: i0.ViewChild, args: ['surface', { isSignal: true }] }] } });
|
|
3323
4276
|
|
|
3324
4277
|
/**
|
|
@@ -3397,10 +4350,16 @@ const DIALOG_HANDOVER_DELAY_MS = 160;
|
|
|
3397
4350
|
class DialogService {
|
|
3398
4351
|
cdkDialog = inject(Dialog$1);
|
|
3399
4352
|
overlay = inject(Overlay);
|
|
4353
|
+
overlayContainer = inject(OverlayContainer);
|
|
3400
4354
|
injector = inject(Injector);
|
|
3401
4355
|
document = inject(DOCUMENT);
|
|
3402
4356
|
/** Open dialogs, from the first one opened to the one currently on top. */
|
|
3403
4357
|
refs = [];
|
|
4358
|
+
/**
|
|
4359
|
+
* Elements the page was made of that the CDK hid from assistive technology,
|
|
4360
|
+
* kept here while the dialogs are hidden and the page is given back.
|
|
4361
|
+
*/
|
|
4362
|
+
pageAriaHidden = new Map();
|
|
3404
4363
|
/**
|
|
3405
4364
|
* Page scrolling is blocked here instead of per overlay, so stacked dialogs
|
|
3406
4365
|
* cannot unblock the page while another dialog is still open.
|
|
@@ -3408,9 +4367,14 @@ class DialogService {
|
|
|
3408
4367
|
scrollBlock = this.overlay.scrollStrategies.block();
|
|
3409
4368
|
/** Element that was focused before the first dialog of the stack opened. */
|
|
3410
4369
|
rootTrigger = null;
|
|
4370
|
+
pageStateQueued = false;
|
|
3411
4371
|
get openDialogs() {
|
|
3412
4372
|
return this.refs;
|
|
3413
4373
|
}
|
|
4374
|
+
/** Open dialogs that are currently on screen, from the bottom one up. */
|
|
4375
|
+
get visibleDialogs() {
|
|
4376
|
+
return this.refs.filter((ref) => !ref.isHidden);
|
|
4377
|
+
}
|
|
3414
4378
|
/** The full screen dialog that is open, if there is one. */
|
|
3415
4379
|
get fullScreenDialog() {
|
|
3416
4380
|
return this.refs.find((ref) => ref.isFullScreen && !ref.isClosing);
|
|
@@ -3546,7 +4510,7 @@ class DialogService {
|
|
|
3546
4510
|
this.bindDataToInputs(cdkRef.componentRef, dialogConfig);
|
|
3547
4511
|
}
|
|
3548
4512
|
this.refs.push(dialogRef);
|
|
3549
|
-
this.
|
|
4513
|
+
this.syncPageState();
|
|
3550
4514
|
if (previousRef) {
|
|
3551
4515
|
setTimeout(() => this.startReplacingAnimation(dialogRef, previousRef), DIALOG_HANDOVER_DELAY_MS);
|
|
3552
4516
|
}
|
|
@@ -3557,6 +4521,41 @@ class DialogService {
|
|
|
3557
4521
|
// already coming back while this one is fading out.
|
|
3558
4522
|
dialogRef.beforeClosed().subscribe(() => this.restorePreviousDialog(dialogRef));
|
|
3559
4523
|
dialogRef.afterClosed().subscribe(() => this.removeRef(dialogRef));
|
|
4524
|
+
// Hiding and showing can also come straight from the reference, so the
|
|
4525
|
+
// page state is driven by what the dialogs report rather than by the
|
|
4526
|
+
// calls that went through this service.
|
|
4527
|
+
dialogRef.hiddenChanged().subscribe(() => this.syncPageState());
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* Hides every dialog that is on screen, the full screen one included, with
|
|
4531
|
+
* the closing animation. They stay alive with their content and state, the
|
|
4532
|
+
* page underneath becomes usable, and showAll() puts them back. The promise
|
|
4533
|
+
* resolves once they are all out of sight.
|
|
4534
|
+
*/
|
|
4535
|
+
hideAll() {
|
|
4536
|
+
const visible = this.refs.filter((ref) => !ref.isHidden && !ref.isClosing);
|
|
4537
|
+
return Promise.all(visible.map((ref) => ref.hide())).then(() => undefined);
|
|
4538
|
+
}
|
|
4539
|
+
/**
|
|
4540
|
+
* Brings the dialogs back on screen, whether they were hidden one by one or
|
|
4541
|
+
* all at once: the dialog that was on top returns, along with the full
|
|
4542
|
+
* screen dialog it sits in. Dialogs hidden behind another dialog stay
|
|
4543
|
+
* hidden, since that is where they belong once the stack is back.
|
|
4544
|
+
*/
|
|
4545
|
+
showAll() {
|
|
4546
|
+
const topRef = this.topRef();
|
|
4547
|
+
if (!topRef) {
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
const fullScreenRef = this.fullScreenDialog;
|
|
4551
|
+
// The full screen dialog is the context the top one sits in, so it
|
|
4552
|
+
// comes back first and the dialog on top ends up on top, with the focus.
|
|
4553
|
+
if (fullScreenRef && fullScreenRef !== topRef && fullScreenRef.isHidden) {
|
|
4554
|
+
fullScreenRef.show();
|
|
4555
|
+
}
|
|
4556
|
+
if (topRef.isHidden) {
|
|
4557
|
+
topRef.show();
|
|
4558
|
+
}
|
|
3560
4559
|
}
|
|
3561
4560
|
/** Closes every open dialog, including the hidden and the full screen ones. */
|
|
3562
4561
|
closeAll() {
|
|
@@ -3719,15 +4718,82 @@ class DialogService {
|
|
|
3719
4718
|
if (this.refs.length === 0) {
|
|
3720
4719
|
this.rootTrigger = null;
|
|
3721
4720
|
}
|
|
3722
|
-
this.
|
|
4721
|
+
this.syncPageState();
|
|
3723
4722
|
}
|
|
3724
|
-
|
|
3725
|
-
|
|
4723
|
+
/**
|
|
4724
|
+
* State that belongs to the dialogs as a whole rather than to one of them:
|
|
4725
|
+
* page scrolling, the page being hidden from assistive technology, and
|
|
4726
|
+
* where focus sits. Applied in a task of its own, so a dialog handing the
|
|
4727
|
+
* screen over to another one does not give the page back and take it again
|
|
4728
|
+
* within the same frame.
|
|
4729
|
+
*/
|
|
4730
|
+
syncPageState() {
|
|
4731
|
+
if (this.pageStateQueued) {
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
4734
|
+
this.pageStateQueued = true;
|
|
4735
|
+
setTimeout(() => {
|
|
4736
|
+
this.pageStateQueued = false;
|
|
4737
|
+
this.applyPageState();
|
|
4738
|
+
});
|
|
4739
|
+
}
|
|
4740
|
+
applyPageState() {
|
|
4741
|
+
const hasVisibleDialog = this.refs.some((ref) => !ref.isHidden);
|
|
4742
|
+
if (hasVisibleDialog) {
|
|
3726
4743
|
this.scrollBlock.enable();
|
|
3727
4744
|
}
|
|
3728
4745
|
else {
|
|
3729
4746
|
this.scrollBlock.disable();
|
|
3730
4747
|
}
|
|
4748
|
+
if (this.refs.length === 0) {
|
|
4749
|
+
// Nothing is open anymore: the CDK gives the page back to assistive
|
|
4750
|
+
// technology on its own, so there is nothing left to restore here.
|
|
4751
|
+
this.pageAriaHidden.clear();
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
if (hasVisibleDialog) {
|
|
4755
|
+
this.hidePageFromAssistiveTechnology();
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
this.exposePageToAssistiveTechnology();
|
|
4759
|
+
this.releaseFocus();
|
|
4760
|
+
}
|
|
4761
|
+
/**
|
|
4762
|
+
* Gives the page back to assistive technology while every dialog is hidden.
|
|
4763
|
+
* The CDK hides it for as long as a dialog is open, which would otherwise
|
|
4764
|
+
* leave nothing to read at all once the dialogs are out of sight.
|
|
4765
|
+
*/
|
|
4766
|
+
exposePageToAssistiveTechnology() {
|
|
4767
|
+
if (this.pageAriaHidden.size > 0) {
|
|
4768
|
+
return;
|
|
4769
|
+
}
|
|
4770
|
+
const container = this.overlayContainer.getContainerElement();
|
|
4771
|
+
Array.from(container.parentElement?.children ?? []).forEach((sibling) => {
|
|
4772
|
+
const value = sibling.getAttribute('aria-hidden');
|
|
4773
|
+
if (sibling === container || value === null) {
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
this.pageAriaHidden.set(sibling, value);
|
|
4777
|
+
sibling.removeAttribute('aria-hidden');
|
|
4778
|
+
});
|
|
4779
|
+
}
|
|
4780
|
+
/** Hides the page again as soon as a dialog is back on screen. */
|
|
4781
|
+
hidePageFromAssistiveTechnology() {
|
|
4782
|
+
this.pageAriaHidden.forEach((value, element) => {
|
|
4783
|
+
element.setAttribute('aria-hidden', value);
|
|
4784
|
+
});
|
|
4785
|
+
this.pageAriaHidden.clear();
|
|
4786
|
+
}
|
|
4787
|
+
/**
|
|
4788
|
+
* Focus follows the dialogs off the screen: it goes back to whatever opened
|
|
4789
|
+
* the stack, so the page stays usable with the keyboard while the dialogs
|
|
4790
|
+
* wait behind it. Focus that already moved somewhere else is left alone.
|
|
4791
|
+
*/
|
|
4792
|
+
releaseFocus() {
|
|
4793
|
+
const activeElement = this.document.activeElement;
|
|
4794
|
+
if (this.rootTrigger && (!activeElement || activeElement === this.document.body)) {
|
|
4795
|
+
this.rootTrigger.focus();
|
|
4796
|
+
}
|
|
3731
4797
|
}
|
|
3732
4798
|
getFocusedElement() {
|
|
3733
4799
|
const activeElement = this.document.activeElement;
|
|
@@ -4835,5 +5901,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
|
|
|
4835
5901
|
* Generated bundle index. Do not edit.
|
|
4836
5902
|
*/
|
|
4837
5903
|
|
|
4838
|
-
export { AppBar, AppBarLogo, Avatar, Badge, Button, ButtonGroup, Card, Checkbox, ChipAvatar, Chips, CircularProgressIndicator, DIALOG_COMPONENT, DIALOG_CONFIG, DIALOG_DATA, Dialog, DialogActions, DialogBody, DialogHeader, DialogRef, DialogService, Divider, FloatingActionButton, FullScreenDialog, FullScreenDialogHeader, Grid, GridItem, IconButton, IconElement, InputElement, LayoutService, LinearProgressIndicator, List, ListItem, ListItemPrimaryAction, ListLeading, ListSlot, LoadingIndicator, MENU_COMPONENT, MENU_CONFIG, MENU_DATA, MaterialIcon, Menu, MenuGroup, MenuItem, MenuRef, MenuService, NavigationBar, NavigationGroup, NavigationItem, NavigationRail, RadioButton, SIDE_SHEET_COMPONENT, SIDE_SHEET_CONFIG, SIDE_SHEET_DATA, SNACKBAR_ACTION_LABEL, SNACKBAR_CONFIG, SNACKBAR_MESSAGE, Scaffold, ScaffoldBar, ScaffoldPane, ScaffoldRail, SheetsService, SideSheetActions, SideSheetBody, SideSheetHeader, SideSheetRef, Slider, Snackbar, SnackbarRef, SnackbarService, SplitButton, StateComponent, SupportingText, Switch, TextField, TypeBody, TypeDisplay, TypeHeadline, TypeLabel, TypeTitle };
|
|
5904
|
+
export { AppBar, AppBarLogo, Avatar, Badge, Button, ButtonGroup, CAROUSEL_STRATEGIES, Card, Carousel, CarouselItem, Checkbox, ChipAvatar, Chips, CircularProgressIndicator, DIALOG_COMPONENT, DIALOG_CONFIG, DIALOG_DATA, Dialog, DialogActions, DialogBody, DialogHeader, DialogRef, DialogService, Divider, FloatingActionButton, FullScreenDialog, FullScreenDialogHeader, Grid, GridItem, IconButton, IconElement, InputElement, LayoutService, LinearProgressIndicator, List, ListItem, ListItemPrimaryAction, ListLeading, ListSlot, LoadingIndicator, MENU_COMPONENT, MENU_CONFIG, MENU_DATA, MaterialIcon, Menu, MenuGroup, MenuItem, MenuRef, MenuService, NavigationBar, NavigationGroup, NavigationItem, NavigationRail, RadioButton, SIDE_SHEET_COMPONENT, SIDE_SHEET_CONFIG, SIDE_SHEET_DATA, SNACKBAR_ACTION_LABEL, SNACKBAR_CONFIG, SNACKBAR_MESSAGE, Scaffold, ScaffoldBar, ScaffoldPane, ScaffoldRail, SheetsService, SideSheetActions, SideSheetBody, SideSheetHeader, SideSheetRef, Slider, Snackbar, SnackbarRef, SnackbarService, SplitButton, StateComponent, SupportingText, Switch, TextField, TypeBody, TypeDisplay, TypeHeadline, TypeLabel, TypeTitle, buildGeometry, clampIndex, indexForScrollOffset, multiBrowseStrategy, resolveItemGeometry, resolveState, scrollOffsetForIndex, sizeBandFor };
|
|
4839
5905
|
//# sourceMappingURL=almoamendev-ngx-md3.mjs.map
|