ngwr 14.7.0 → 14.8.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/bar-chart/styles/_index.scss +1 -0
- package/calendar-heatmap/styles/_index.scss +1 -0
- package/cascader/styles/_rules.scss +86 -14
- package/date-picker/styles/_index.scss +30 -0
- package/density/styles/_index.scss +4 -2
- package/donut-chart/styles/_index.scss +10 -0
- package/dropdown/styles/_index.scss +67 -5
- package/event-calendar/styles/_index.scss +209 -35
- package/fesm2022/ngwr-bar-chart.mjs +29 -4
- package/fesm2022/ngwr-bar-chart.mjs.map +1 -1
- package/fesm2022/ngwr-calendar-heatmap.mjs +59 -4
- package/fesm2022/ngwr-calendar-heatmap.mjs.map +1 -1
- package/fesm2022/ngwr-cascader.mjs +2 -2
- package/fesm2022/ngwr-cascader.mjs.map +1 -1
- package/fesm2022/ngwr-checkbox.mjs +12 -2
- package/fesm2022/ngwr-checkbox.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker.mjs +705 -46
- package/fesm2022/ngwr-date-picker.mjs.map +1 -1
- package/fesm2022/ngwr-donut-chart.mjs +46 -4
- package/fesm2022/ngwr-donut-chart.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +166 -17
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar.mjs +9 -2
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-form.mjs +52 -1
- package/fesm2022/ngwr-form.mjs.map +1 -1
- package/fesm2022/ngwr-gauge.mjs +33 -4
- package/fesm2022/ngwr-gauge.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ar.mjs +2 -0
- package/fesm2022/ngwr-i18n-ar.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-cs.mjs +2 -0
- package/fesm2022/ngwr-i18n-cs.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-de.mjs +2 -0
- package/fesm2022/ngwr-i18n-de.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-en.mjs +5 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-es.mjs +2 -0
- package/fesm2022/ngwr-i18n-es.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-fr.mjs +2 -0
- package/fesm2022/ngwr-i18n-fr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-he.mjs +2 -0
- package/fesm2022/ngwr-i18n-he.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-hi.mjs +2 -0
- package/fesm2022/ngwr-i18n-hi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-id.mjs +2 -0
- package/fesm2022/ngwr-i18n-id.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-it.mjs +2 -0
- package/fesm2022/ngwr-i18n-it.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ja.mjs +2 -0
- package/fesm2022/ngwr-i18n-ja.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ko.mjs +2 -0
- package/fesm2022/ngwr-i18n-ko.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-nl.mjs +2 -0
- package/fesm2022/ngwr-i18n-nl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pl.mjs +2 -0
- package/fesm2022/ngwr-i18n-pl.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-pt.mjs +2 -0
- package/fesm2022/ngwr-i18n-pt.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +2 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-sv.mjs +2 -0
- package/fesm2022/ngwr-i18n-sv.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-tr.mjs +2 -0
- package/fesm2022/ngwr-i18n-tr.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-uk.mjs +2 -0
- package/fesm2022/ngwr-i18n-uk.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-vi.mjs +2 -0
- package/fesm2022/ngwr-i18n-vi.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh-TW.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh-TW.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-zh.mjs +2 -0
- package/fesm2022/ngwr-i18n-zh.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +12 -0
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-line-chart.mjs +16 -5
- package/fesm2022/ngwr-line-chart.mjs.map +1 -1
- package/fesm2022/ngwr-meter-group-testing.mjs +7 -3
- package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -1
- package/fesm2022/ngwr-meter-group.mjs +24 -4
- package/fesm2022/ngwr-meter-group.mjs.map +1 -1
- package/fesm2022/ngwr-page-header.mjs +10 -2
- package/fesm2022/ngwr-page-header.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +569 -7
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-sidebar.mjs +7 -0
- package/fesm2022/ngwr-sidebar.mjs.map +1 -1
- package/fesm2022/ngwr-sparkline.mjs +82 -8
- package/fesm2022/ngwr-sparkline.mjs.map +1 -1
- package/fesm2022/ngwr-table.mjs +1 -1
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-toolbar.mjs +7 -0
- package/fesm2022/ngwr-toolbar.mjs.map +1 -1
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/form/styles/_index.scss +11 -1
- package/gauge/styles/_index.scss +1 -0
- package/i18n/ar.json +3 -1
- package/i18n/cs.json +3 -1
- package/i18n/de.json +3 -1
- package/i18n/en.json +3 -1
- package/i18n/es.json +3 -1
- package/i18n/fr.json +3 -1
- package/i18n/he.json +3 -1
- package/i18n/hi.json +3 -1
- package/i18n/id.json +3 -1
- package/i18n/it.json +3 -1
- package/i18n/ja.json +3 -1
- package/i18n/ko.json +3 -1
- package/i18n/nl.json +3 -1
- package/i18n/pl.json +3 -1
- package/i18n/pt.json +3 -1
- package/i18n/ru.json +3 -1
- package/i18n/sv.json +3 -1
- package/i18n/tr.json +3 -1
- package/i18n/uk.json +3 -1
- package/i18n/vi.json +3 -1
- package/i18n/zh-TW.json +3 -1
- package/i18n/zh.json +3 -1
- package/input/styles/_index.scss +19 -0
- package/input-number/styles/_index.scss +69 -0
- package/line-chart/styles/_index.scss +30 -2
- package/llms-full.txt +3 -3
- package/meter-group/styles/_index.scss +1 -0
- package/overlay/styles/_arrow.scss +27 -2
- package/package.json +1 -1
- package/page-header/styles/_index.scss +37 -1
- package/pagination/styles/_index.scss +9 -5
- package/popover/styles/_index.scss +73 -5
- package/select/styles/_index.scss +48 -9
- package/sidebar/styles/_index.scss +34 -2
- package/sparkline/styles/_index.scss +19 -0
- package/toolbar/styles/_index.scss +35 -1
- package/tree/styles/_index.scss +45 -6
- package/types/ngwr-bar-chart.d.ts +19 -1
- package/types/ngwr-calendar-heatmap.d.ts +29 -1
- package/types/ngwr-checkbox.d.ts +2 -0
- package/types/ngwr-date-picker.d.ts +308 -43
- package/types/ngwr-donut-chart.d.ts +24 -1
- package/types/ngwr-dropdown.d.ts +22 -2
- package/types/ngwr-event-calendar.d.ts +8 -0
- package/types/ngwr-form.d.ts +35 -5
- package/types/ngwr-gauge.d.ts +20 -1
- package/types/ngwr-input-number.d.ts +7 -0
- package/types/ngwr-line-chart.d.ts +9 -1
- package/types/ngwr-meter-group-testing.d.ts +3 -2
- package/types/ngwr-meter-group.d.ts +14 -1
- package/types/ngwr-page-header.d.ts +10 -2
- package/types/ngwr-popover.d.ts +154 -3
- package/types/ngwr-sidebar.d.ts +7 -0
- package/types/ngwr-sparkline.d.ts +43 -1
- package/types/ngwr-toolbar.d.ts +7 -0
- package/types/ngwr-version.d.ts +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { input, computed, ViewEncapsulation, Component } from '@angular/core';
|
|
3
|
+
import { input, viewChild, computed, ViewEncapsulation, Component } from '@angular/core';
|
|
4
4
|
import { useI18nText } from 'ngwr/i18n';
|
|
5
|
+
import { useChartTooltip } from 'ngwr/popover';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @license
|
|
@@ -62,6 +63,14 @@ class WrDonutChart {
|
|
|
62
63
|
/** Smaller label under the value. */
|
|
63
64
|
centerLabel = input('', /* @ts-ignore */
|
|
64
65
|
...(ngDevMode ? [{ debugName: "centerLabel" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/**
|
|
67
|
+
* Show a tooltip with the slice's label and value on hover. Pointer only — the
|
|
68
|
+
* legend prints the same numbers, and with `showLegend` off `ariaLabel` is where
|
|
69
|
+
* to say them. @default true
|
|
70
|
+
*/
|
|
71
|
+
tooltip = input(true, { ...(ngDevMode ? { debugName: "tooltip" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
|
|
72
|
+
svg = viewChild('svg', /* @ts-ignore */
|
|
73
|
+
...(ngDevMode ? [{ debugName: "svg" }] : /* istanbul ignore next */ []));
|
|
65
74
|
// Drawn in a 100×100 viewBox.
|
|
66
75
|
outerR = 50;
|
|
67
76
|
innerR = computed(() => {
|
|
@@ -92,10 +101,43 @@ class WrDonutChart {
|
|
|
92
101
|
color: s.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length],
|
|
93
102
|
path: this.arcPath(startAngle, endAngle),
|
|
94
103
|
percent: (value / total) * 100,
|
|
104
|
+
midAngle: (startAngle + endAngle) / 2,
|
|
95
105
|
};
|
|
96
106
|
});
|
|
97
107
|
}, /* @ts-ignore */
|
|
98
108
|
...(ngDevMode ? [{ debugName: "slices" }] : /* istanbul ignore next */ []));
|
|
109
|
+
/**
|
|
110
|
+
* One tooltip for the whole chart, pointing at the hovered slice: at the middle of
|
|
111
|
+
* its OUTER arc, on the side that part of the ring faces — above for a slice whose
|
|
112
|
+
* middle is in the top quarter, below for the bottom one, beside it for the two at
|
|
113
|
+
* three and nine o'clock. From there the chip sits clear of the ring, and the way
|
|
114
|
+
* onto it runs straight out of the slice that opened it, over nothing else.
|
|
115
|
+
*
|
|
116
|
+
* Not the slice's `<path>` element: its box is most of the ring for any slice past
|
|
117
|
+
* a quarter, so the chip would point at the middle of the chart. And not the middle
|
|
118
|
+
* of the band: a chip there lies over the ring, under a pointer resting on it —
|
|
119
|
+
* measured in Chromium, the inner half of a lower slice landed on its own chip. A
|
|
120
|
+
* tiny slice keeps its own mid-angle, so the arrow says which sliver it is.
|
|
121
|
+
*/
|
|
122
|
+
tip = useChartTooltip(this.tooltip, index => {
|
|
123
|
+
const slice = this.slices()[index];
|
|
124
|
+
const svg = this.svg()?.nativeElement;
|
|
125
|
+
if (!slice?.path || !svg)
|
|
126
|
+
return null;
|
|
127
|
+
const box = svg.getBoundingClientRect();
|
|
128
|
+
// The drawing's y runs downwards, so a positive sine is the lower half.
|
|
129
|
+
const cos = Math.cos(slice.midAngle);
|
|
130
|
+
const sin = Math.sin(slice.midAngle);
|
|
131
|
+
const side = Math.abs(cos) > Math.abs(sin) ? (cos > 0 ? 'right' : 'left') : sin > 0 ? 'bottom' : 'top';
|
|
132
|
+
return {
|
|
133
|
+
datum: { label: slice.label, value: String(slice.value), color: slice.color },
|
|
134
|
+
anchor: {
|
|
135
|
+
x: box.left + ((50 + this.outerR * cos) / 100) * box.width,
|
|
136
|
+
y: box.top + ((50 + this.outerR * sin) / 100) * box.height,
|
|
137
|
+
},
|
|
138
|
+
side,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
99
141
|
// Helpers
|
|
100
142
|
/** SVG path for one donut slice using outer and inner arcs. */
|
|
101
143
|
arcPath(start, end) {
|
|
@@ -139,12 +181,12 @@ class WrDonutChart {
|
|
|
139
181
|
].join(' ');
|
|
140
182
|
}
|
|
141
183
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDonutChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
142
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDonutChart, isStandalone: true, selector: "wr-donut-chart", inputs: { segments: { classPropertyName: "segments", publicName: "segments", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, thickness: { classPropertyName: "thickness", publicName: "thickness", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, centerValue: { classPropertyName: "centerValue", publicName: "centerValue", isSignal: true, isRequired: false, transformFunction: null }, centerLabel: { classPropertyName: "centerLabel", publicName: "centerLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "wr-donut-chart" }, ngImport: i0, template: "<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree \u2014 including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance \u2014 for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n", encapsulation: i0.ViewEncapsulation.None });
|
|
184
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: WrDonutChart, isStandalone: true, selector: "wr-donut-chart", inputs: { segments: { classPropertyName: "segments", publicName: "segments", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, thickness: { classPropertyName: "thickness", publicName: "thickness", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, centerValue: { classPropertyName: "centerValue", publicName: "centerValue", isSignal: true, isRequired: false, transformFunction: null }, centerLabel: { classPropertyName: "centerLabel", publicName: "centerLabel", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "wr-donut-chart" }, viewQueries: [{ propertyName: "svg", first: true, predicate: ["svg"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree \u2014 including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg #svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance \u2014 for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n (mouseenter)=\"tip.enter($index)\"\n (mouseleave)=\"tip.leave($event)\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n", encapsulation: i0.ViewEncapsulation.None });
|
|
143
185
|
}
|
|
144
186
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDonutChart, decorators: [{
|
|
145
187
|
type: Component,
|
|
146
|
-
args: [{ selector: 'wr-donut-chart', encapsulation: ViewEncapsulation.None, host: { class: 'wr-donut-chart' }, template: "<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree \u2014 including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance \u2014 for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n" }]
|
|
147
|
-
}], propDecorators: { segments: [{ type: i0.Input, args: [{ isSignal: true, alias: "segments", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], thickness: [{ type: i0.Input, args: [{ isSignal: true, alias: "thickness", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], centerValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "centerValue", required: false }] }], centerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "centerLabel", required: false }] }] } });
|
|
188
|
+
args: [{ selector: 'wr-donut-chart', encapsulation: ViewEncapsulation.None, host: { class: 'wr-donut-chart' }, template: "<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree \u2014 including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg #svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance \u2014 for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n (mouseenter)=\"tip.enter($index)\"\n (mouseleave)=\"tip.leave($event)\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n" }]
|
|
189
|
+
}], propDecorators: { segments: [{ type: i0.Input, args: [{ isSignal: true, alias: "segments", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], thickness: [{ type: i0.Input, args: [{ isSignal: true, alias: "thickness", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], centerValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "centerValue", required: false }] }], centerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "centerLabel", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], svg: [{ type: i0.ViewChild, args: ['svg', { isSignal: true }] }] } });
|
|
148
190
|
|
|
149
191
|
/**
|
|
150
192
|
* Generated bundle index. Do not edit.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-donut-chart.mjs","sources":["../../../projects/lib/donut-chart/donut-chart.ts","../../../projects/lib/donut-chart/donut-chart.html","../../../projects/lib/donut-chart/ngwr-donut-chart.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport { Component, ViewEncapsulation, computed, input } from '@angular/core';\n\nimport { useI18nText } from 'ngwr/i18n';\n\nimport type { WrDonutSegment } from './interfaces';\n\n/**\n * A segment's contribution to the ring. `Math.max(0, NaN)` is NaN, and the running total\n * carries it forward — so one non-finite datum used to take out its own arc AND every arc\n * after it, writing the literal text `NaN` into the path `d`.\n */\nfunction weight(value: number): number {\n return Number.isFinite(value) ? Math.max(0, value) : 0;\n}\n\nconst FALLBACK_COLORS = [\n 'var(--wr-color-primary)',\n 'var(--wr-color-secondary)',\n 'var(--wr-color-success)',\n 'var(--wr-color-warning)',\n 'var(--wr-color-danger)',\n 'var(--wr-color-medium)',\n];\n\n/**\n * Donut chart. Set `thickness` to 0 for a solid pie. Optional center\n * text via inputs.\n *\n * @example\n * ```html\n * <wr-donut-chart [segments]=\"[\n * { label: 'Used', value: 60 },\n * { label: 'Free', value: 40 }\n * ]\" centerLabel=\"Disk\" centerValue=\"60%\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/donut-chart\n */\n@Component({\n selector: 'wr-donut-chart',\n templateUrl: './donut-chart.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-donut-chart' },\n})\nexport class WrDonutChart {\n readonly segments = input<readonly WrDonutSegment[]>([]);\n\n /**\n * Accessible name of the drawing. The arcs carry no text and the legend is optional, so\n * with `showLegend: false` this and the centre text are the whole of what a screen reader\n * gets. Falls back to `donutChart.label`.\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'donutChart.label', 'Donut chart');\n\n /** Diameter in CSS pixels. @default 200 */\n readonly size = input(200, { transform: (v: unknown): number => Math.max(48, coerceNumberProperty(v, 200)) });\n\n /** Inner-ring thickness as a percent of radius (0–100). `0` = solid pie. @default 30 */\n readonly thickness = input(30, {\n transform: (v: unknown): number => Math.min(100, Math.max(0, coerceNumberProperty(v, 30))),\n });\n\n /** Show the legend under the chart. @default true */\n readonly showLegend = input(true, { transform: coerceBooleanProperty });\n\n /** Bold value text in the center. */\n readonly centerValue = input<string>('');\n\n /** Smaller label under the value. */\n readonly centerLabel = input<string>('');\n\n // Drawn in a 100×100 viewBox.\n private readonly outerR = 50;\n\n protected readonly innerR = computed(() => {\n const thickness = this.thickness();\n // 0 = solid pie: fill all the way to the center, not a zero-width ring.\n if (thickness === 0) return 0;\n return this.outerR * (1 - thickness / 100);\n });\n\n protected readonly total = computed(() => {\n const sum = this.segments().reduce((acc, s) => acc + weight(s.value), 0);\n return sum > 0 ? sum : 1;\n });\n\n /** Resolved slices ready for the template — each carries its arc path. */\n protected readonly slices = computed(() => {\n const total = this.total();\n let cumulative = 0;\n return this.segments().map((s, i) => {\n const value = weight(s.value);\n const startAngle = (cumulative / total) * Math.PI * 2;\n cumulative += value;\n const endAngle = (cumulative / total) * Math.PI * 2;\n return {\n label: s.label,\n value,\n color: s.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length],\n path: this.arcPath(startAngle, endAngle),\n percent: (value / total) * 100,\n };\n });\n });\n\n // Helpers\n\n /** SVG path for one donut slice using outer and inner arcs. */\n private arcPath(start: number, end: number): string {\n const cx = 50;\n const cy = 50;\n const ro = this.outerR;\n const ri = this.innerR();\n const sweep = end - start;\n if (sweep <= 0) return '';\n // Full-circle slice — two halves so the arc can render.\n if (sweep >= Math.PI * 2 - 1e-6) {\n const path = [\n `M ${cx + ro} ${cy}`,\n `A ${ro} ${ro} 0 1 1 ${cx - ro} ${cy}`,\n `A ${ro} ${ro} 0 1 1 ${cx + ro} ${cy}`,\n ];\n if (ri > 0) {\n path.push(`M ${cx + ri} ${cy}`, `A ${ri} ${ri} 0 1 0 ${cx - ri} ${cy}`, `A ${ri} ${ri} 0 1 0 ${cx + ri} ${cy}`);\n }\n return path.join(' ');\n }\n const largeArc = sweep > Math.PI ? 1 : 0;\n const sox = cx + ro * Math.cos(start);\n const soy = cy + ro * Math.sin(start);\n const eox = cx + ro * Math.cos(end);\n const eoy = cy + ro * Math.sin(end);\n if (ri === 0) {\n return `M ${cx} ${cy} L ${sox.toFixed(2)} ${soy.toFixed(2)} A ${ro} ${ro} 0 ${largeArc} 1 ${eox.toFixed(2)} ${eoy.toFixed(2)} Z`;\n }\n const eix = cx + ri * Math.cos(end);\n const eiy = cy + ri * Math.sin(end);\n const six = cx + ri * Math.cos(start);\n const siy = cy + ri * Math.sin(start);\n return [\n `M ${sox.toFixed(2)} ${soy.toFixed(2)}`,\n `A ${ro} ${ro} 0 ${largeArc} 1 ${eox.toFixed(2)} ${eoy.toFixed(2)}`,\n `L ${eix.toFixed(2)} ${eiy.toFixed(2)}`,\n `A ${ri} ${ri} 0 ${largeArc} 0 ${six.toFixed(2)} ${siy.toFixed(2)}`,\n 'Z',\n ].join(' ');\n }\n}\n\nexport type { WrDonutSegment } from './interfaces';\n","<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree — including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance — for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ngwr-donut-chart.mjs","sources":["../../../projects/lib/donut-chart/donut-chart.ts","../../../projects/lib/donut-chart/donut-chart.html","../../../projects/lib/donut-chart/ngwr-donut-chart.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport { Component, type ElementRef, ViewEncapsulation, computed, input, viewChild } from '@angular/core';\n\nimport { useI18nText } from 'ngwr/i18n';\nimport { useChartTooltip } from 'ngwr/popover';\n\nimport type { WrDonutSegment } from './interfaces';\n\n/**\n * A segment's contribution to the ring. `Math.max(0, NaN)` is NaN, and the running total\n * carries it forward — so one non-finite datum used to take out its own arc AND every arc\n * after it, writing the literal text `NaN` into the path `d`.\n */\nfunction weight(value: number): number {\n return Number.isFinite(value) ? Math.max(0, value) : 0;\n}\n\nconst FALLBACK_COLORS = [\n 'var(--wr-color-primary)',\n 'var(--wr-color-secondary)',\n 'var(--wr-color-success)',\n 'var(--wr-color-warning)',\n 'var(--wr-color-danger)',\n 'var(--wr-color-medium)',\n];\n\n/**\n * Donut chart. Set `thickness` to 0 for a solid pie. Optional center\n * text via inputs.\n *\n * @example\n * ```html\n * <wr-donut-chart [segments]=\"[\n * { label: 'Used', value: 60 },\n * { label: 'Free', value: 40 }\n * ]\" centerLabel=\"Disk\" centerValue=\"60%\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/donut-chart\n */\n@Component({\n selector: 'wr-donut-chart',\n templateUrl: './donut-chart.html',\n encapsulation: ViewEncapsulation.None,\n host: { class: 'wr-donut-chart' },\n})\nexport class WrDonutChart {\n readonly segments = input<readonly WrDonutSegment[]>([]);\n\n /**\n * Accessible name of the drawing. The arcs carry no text and the legend is optional, so\n * with `showLegend: false` this and the centre text are the whole of what a screen reader\n * gets. Falls back to `donutChart.label`.\n */\n readonly ariaLabel = input<string | null>(null);\n\n protected readonly resolvedAriaLabel = useI18nText(this.ariaLabel, 'donutChart.label', 'Donut chart');\n\n /** Diameter in CSS pixels. @default 200 */\n readonly size = input(200, { transform: (v: unknown): number => Math.max(48, coerceNumberProperty(v, 200)) });\n\n /** Inner-ring thickness as a percent of radius (0–100). `0` = solid pie. @default 30 */\n readonly thickness = input(30, {\n transform: (v: unknown): number => Math.min(100, Math.max(0, coerceNumberProperty(v, 30))),\n });\n\n /** Show the legend under the chart. @default true */\n readonly showLegend = input(true, { transform: coerceBooleanProperty });\n\n /** Bold value text in the center. */\n readonly centerValue = input<string>('');\n\n /** Smaller label under the value. */\n readonly centerLabel = input<string>('');\n\n /**\n * Show a tooltip with the slice's label and value on hover. Pointer only — the\n * legend prints the same numbers, and with `showLegend` off `ariaLabel` is where\n * to say them. @default true\n */\n readonly tooltip = input(true, { transform: coerceBooleanProperty });\n\n private readonly svg = viewChild<ElementRef<SVGSVGElement>>('svg');\n\n // Drawn in a 100×100 viewBox.\n private readonly outerR = 50;\n\n protected readonly innerR = computed(() => {\n const thickness = this.thickness();\n // 0 = solid pie: fill all the way to the center, not a zero-width ring.\n if (thickness === 0) return 0;\n return this.outerR * (1 - thickness / 100);\n });\n\n protected readonly total = computed(() => {\n const sum = this.segments().reduce((acc, s) => acc + weight(s.value), 0);\n return sum > 0 ? sum : 1;\n });\n\n /** Resolved slices ready for the template — each carries its arc path. */\n protected readonly slices = computed(() => {\n const total = this.total();\n let cumulative = 0;\n return this.segments().map((s, i) => {\n const value = weight(s.value);\n const startAngle = (cumulative / total) * Math.PI * 2;\n cumulative += value;\n const endAngle = (cumulative / total) * Math.PI * 2;\n return {\n label: s.label,\n value,\n color: s.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length],\n path: this.arcPath(startAngle, endAngle),\n percent: (value / total) * 100,\n midAngle: (startAngle + endAngle) / 2,\n };\n });\n });\n\n /**\n * One tooltip for the whole chart, pointing at the hovered slice: at the middle of\n * its OUTER arc, on the side that part of the ring faces — above for a slice whose\n * middle is in the top quarter, below for the bottom one, beside it for the two at\n * three and nine o'clock. From there the chip sits clear of the ring, and the way\n * onto it runs straight out of the slice that opened it, over nothing else.\n *\n * Not the slice's `<path>` element: its box is most of the ring for any slice past\n * a quarter, so the chip would point at the middle of the chart. And not the middle\n * of the band: a chip there lies over the ring, under a pointer resting on it —\n * measured in Chromium, the inner half of a lower slice landed on its own chip. A\n * tiny slice keeps its own mid-angle, so the arrow says which sliver it is.\n */\n protected readonly tip = useChartTooltip(this.tooltip, index => {\n const slice = this.slices()[index];\n const svg = this.svg()?.nativeElement;\n if (!slice?.path || !svg) return null;\n const box = svg.getBoundingClientRect();\n // The drawing's y runs downwards, so a positive sine is the lower half.\n const cos = Math.cos(slice.midAngle);\n const sin = Math.sin(slice.midAngle);\n const side = Math.abs(cos) > Math.abs(sin) ? (cos > 0 ? 'right' : 'left') : sin > 0 ? 'bottom' : 'top';\n return {\n datum: { label: slice.label, value: String(slice.value), color: slice.color },\n anchor: {\n x: box.left + ((50 + this.outerR * cos) / 100) * box.width,\n y: box.top + ((50 + this.outerR * sin) / 100) * box.height,\n },\n side,\n };\n });\n\n // Helpers\n\n /** SVG path for one donut slice using outer and inner arcs. */\n private arcPath(start: number, end: number): string {\n const cx = 50;\n const cy = 50;\n const ro = this.outerR;\n const ri = this.innerR();\n const sweep = end - start;\n if (sweep <= 0) return '';\n // Full-circle slice — two halves so the arc can render.\n if (sweep >= Math.PI * 2 - 1e-6) {\n const path = [\n `M ${cx + ro} ${cy}`,\n `A ${ro} ${ro} 0 1 1 ${cx - ro} ${cy}`,\n `A ${ro} ${ro} 0 1 1 ${cx + ro} ${cy}`,\n ];\n if (ri > 0) {\n path.push(`M ${cx + ri} ${cy}`, `A ${ri} ${ri} 0 1 0 ${cx - ri} ${cy}`, `A ${ri} ${ri} 0 1 0 ${cx + ri} ${cy}`);\n }\n return path.join(' ');\n }\n const largeArc = sweep > Math.PI ? 1 : 0;\n const sox = cx + ro * Math.cos(start);\n const soy = cy + ro * Math.sin(start);\n const eox = cx + ro * Math.cos(end);\n const eoy = cy + ro * Math.sin(end);\n if (ri === 0) {\n return `M ${cx} ${cy} L ${sox.toFixed(2)} ${soy.toFixed(2)} A ${ro} ${ro} 0 ${largeArc} 1 ${eox.toFixed(2)} ${eoy.toFixed(2)} Z`;\n }\n const eix = cx + ri * Math.cos(end);\n const eiy = cy + ri * Math.sin(end);\n const six = cx + ri * Math.cos(start);\n const siy = cy + ri * Math.sin(start);\n return [\n `M ${sox.toFixed(2)} ${soy.toFixed(2)}`,\n `A ${ro} ${ro} 0 ${largeArc} 1 ${eox.toFixed(2)} ${eoy.toFixed(2)}`,\n `L ${eix.toFixed(2)} ${eiy.toFixed(2)}`,\n `A ${ri} ${ri} 0 ${largeArc} 0 ${six.toFixed(2)} ${siy.toFixed(2)}`,\n 'Z',\n ].join(' ');\n }\n}\n\nexport type { WrDonutSegment } from './interfaces';\n","<div class=\"wr-donut-chart__surface\" [style.width.px]=\"size()\">\n <!-- The name sits on the DRAWING, not on this wrapper. `img` is a\n children-presentational role, so with it out here WebKit dropped the whole\n subtree — including the centre value and label, which are real text and are\n the headline datum the legend does not carry. -->\n <svg #svg viewBox=\"0 0 100 100\" role=\"img\" [attr.aria-label]=\"resolvedAriaLabel()\">\n <!-- Every slice is stroked in the SURFACE colour, so adjacent arcs are\n separated by a visible gap rather than by the boundary between two\n fills. Without it the only thing dividing two slices is the colour\n change itself, and the default palette's entries sit within about\n 1.06:1 of each other in relative luminance — for a reader who cannot\n separate hue, two neighbours merge into one arc and the chart reads as\n having fewer slices than it has. A gap is a shape, and it survives\n greyscale, printing and a monochrome display. -->\n @for (slice of slices(); track $index) {\n <path\n [attr.d]=\"slice.path\"\n [attr.fill]=\"slice.color\"\n stroke=\"var(--wr-color-surface)\"\n stroke-width=\"1\"\n stroke-linejoin=\"round\"\n (mouseenter)=\"tip.enter($index)\"\n (mouseleave)=\"tip.leave($event)\"\n />\n }\n </svg>\n @if (centerValue() || centerLabel()) {\n <div class=\"wr-donut-chart__center\">\n @if (centerValue()) {\n <div class=\"wr-donut-chart__center-value\">{{ centerValue() }}</div>\n }\n @if (centerLabel()) {\n <div class=\"wr-donut-chart__center-label\">{{ centerLabel() }}</div>\n }\n </div>\n }\n</div>\n\n@if (showLegend() && slices().length > 0) {\n <ul class=\"wr-donut-chart__legend\">\n @for (slice of slices(); track $index) {\n <li class=\"wr-donut-chart__legend-item\">\n <span class=\"wr-donut-chart__swatch\" [style.background]=\"slice.color\"></span>\n <span class=\"wr-donut-chart__legend-label\">{{ slice.label }}</span>\n <span class=\"wr-donut-chart__legend-value\">{{ slice.value }}</span>\n </li>\n }\n </ul>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -15,20 +15,77 @@ import { WrIcon } from 'ngwr/icon';
|
|
|
15
15
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* The CDK geometry for each placement — one position, no fallbacks.
|
|
19
|
+
*
|
|
20
|
+
* No `offsetX` / `offsetY`, unlike `wr-popover`'s table: the gap between the
|
|
21
|
+
* trigger and the menu is PADDING on the pane (`.wr-dropdown-overlay--<placement>`),
|
|
22
|
+
* so the pointer crossing it is still over the overlay and a `trigger="hover"`
|
|
23
|
+
* menu does not close on the way in. It also means nothing here is
|
|
24
|
+
* direction-dependent, which is why the directive needs no `wrMirrorOffsets`.
|
|
19
25
|
*
|
|
20
26
|
* @internal
|
|
21
27
|
*/
|
|
22
28
|
const WR_DROPDOWN_POSITIONS = {
|
|
23
|
-
top:
|
|
24
|
-
'top-start':
|
|
25
|
-
'top-end':
|
|
26
|
-
bottom:
|
|
27
|
-
'bottom-start':
|
|
28
|
-
'bottom-end':
|
|
29
|
-
left:
|
|
30
|
-
right:
|
|
29
|
+
top: { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' },
|
|
30
|
+
'top-start': { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
|
|
31
|
+
'top-end': { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
|
|
32
|
+
bottom: { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' },
|
|
33
|
+
'bottom-start': { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
|
|
34
|
+
'bottom-end': { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
|
|
35
|
+
left: { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center' },
|
|
36
|
+
right: { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center' },
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What each placement is allowed to flip to when it does not fit, best first.
|
|
40
|
+
* The requested placement always leads, so a menu that fits never moves.
|
|
41
|
+
*
|
|
42
|
+
* The same chains as `wr-popover`'s, restricted to the eight placements a
|
|
43
|
+
* dropdown offers — which needs no editing, since every link in those eight
|
|
44
|
+
* chains is itself one of the eight. Alignment is kept down the chain, a
|
|
45
|
+
* block-axis placement only flips to its opposite, and a side placement may
|
|
46
|
+
* also fall below or above, because on a phone neither side of a mid-page
|
|
47
|
+
* trigger has the room. Once it has, the directive drops the side links for as
|
|
48
|
+
* long as the menu stays open — the gap changes axis with the placement, so a
|
|
49
|
+
* pane is only measured correctly against links on one axis at a time.
|
|
50
|
+
*
|
|
51
|
+
* Each list used to hold ONE position. With `withPush(true)` and nothing to
|
|
52
|
+
* fall back to, a menu that did not fit below its trigger was pushed back up
|
|
53
|
+
* over it instead — measured in Chromium at 1280×700 with a `bottom-start`
|
|
54
|
+
* trigger 40px above the fold: the menu's top landed at 626, inside the
|
|
55
|
+
* trigger's 630–660 box, covering the control that opened it.
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
const WR_DROPDOWN_FALLBACKS = {
|
|
60
|
+
top: ['top', 'bottom'],
|
|
61
|
+
'top-start': ['top-start', 'bottom-start'],
|
|
62
|
+
'top-end': ['top-end', 'bottom-end'],
|
|
63
|
+
bottom: ['bottom', 'top'],
|
|
64
|
+
'bottom-start': ['bottom-start', 'top-start'],
|
|
65
|
+
'bottom-end': ['bottom-end', 'top-end'],
|
|
66
|
+
left: ['left', 'right', 'bottom', 'top'],
|
|
67
|
+
right: ['right', 'left', 'bottom', 'top'],
|
|
31
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The list `withPositions()` wants — the chain in `WR_DROPDOWN_FALLBACKS` order,
|
|
71
|
+
* as fresh objects, so the directive can tell which link the CDK landed on by
|
|
72
|
+
* the IDENTITY of the position it reports back.
|
|
73
|
+
*
|
|
74
|
+
* Deliberately untagged, and this is where the dropdown parts from `wr-popover`,
|
|
75
|
+
* which puts each placement's class in the position's own `panelClass`. The CDK
|
|
76
|
+
* takes those classes off the pane BEFORE it measures it for fit, and here the
|
|
77
|
+
* class carries the gap, as padding — so a tagged list is measured without its
|
|
78
|
+
* gap. A menu with less room than that gap was judged to fit and the flexible
|
|
79
|
+
* bounding box squeezed it instead: measured in Chromium at 1280×700 with 2px to
|
|
80
|
+
* spare below the trigger, the menu's box shrank from 74px to 68px and its last
|
|
81
|
+
* row ran through the bottom border. The directive writes the class itself, which
|
|
82
|
+
* the CDK leaves alone, so the pane is measured with the gap it will render with.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
function wrDropdownPositions(position) {
|
|
87
|
+
return WR_DROPDOWN_FALLBACKS[position].map(name => ({ ...WR_DROPDOWN_POSITIONS[name] }));
|
|
88
|
+
}
|
|
32
89
|
|
|
33
90
|
/**
|
|
34
91
|
* @license
|
|
@@ -36,6 +93,8 @@ const WR_DROPDOWN_POSITIONS = {
|
|
|
36
93
|
* Use of this source code is governed by an MIT-style license that can be
|
|
37
94
|
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
38
95
|
*/
|
|
96
|
+
/** Whether a placement puts the menu BESIDE its trigger, where the gap is inline padding. */
|
|
97
|
+
const isSidePlacement = (name) => name === 'left' || name === 'right';
|
|
39
98
|
/**
|
|
40
99
|
* Attach to any element to open a `<wr-dropdown-menu>` as a CDK overlay.
|
|
41
100
|
*
|
|
@@ -60,9 +119,29 @@ class WrDropdown {
|
|
|
60
119
|
/** How the menu opens. @default 'click' */
|
|
61
120
|
trigger = input('click', /* @ts-ignore */
|
|
62
121
|
...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
63
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Where the menu anchors relative to the trigger. A placement that does not
|
|
124
|
+
* fit flips to the opposite side (a `left` / `right` menu may also drop below
|
|
125
|
+
* or above, and then stays there until it closes), and the pane's
|
|
126
|
+
* `wr-dropdown-overlay--<placement>` class names the side it actually took.
|
|
127
|
+
* @default 'bottom-start'
|
|
128
|
+
*/
|
|
64
129
|
position = input('bottom-start', /* @ts-ignore */
|
|
65
130
|
...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
131
|
+
/**
|
|
132
|
+
* Draw a small arrow on the menu's edge, pointing at the trigger — the one
|
|
133
|
+
* `wr-popover` and `wr-popconfirm` draw, from the same shared rule. It follows
|
|
134
|
+
* the placement the menu actually took, so a flipped menu points the right
|
|
135
|
+
* way, and it widens the gap to the trigger from 0.25rem to popover's 0.5rem
|
|
136
|
+
* so the tip does not touch the control.
|
|
137
|
+
*
|
|
138
|
+
* Never drawn on a bottom sheet (`responsive`), which is anchored to nothing.
|
|
139
|
+
* Turn it off on a menu with no padding of its own (`--wr-dropdown-padding: 0`)
|
|
140
|
+
* or one that clips its overflow: the square reaches about 3px inside the
|
|
141
|
+
* menu's border, over the first row, and `overflow` cuts its tip off.
|
|
142
|
+
* Read when the menu opens. @default true
|
|
143
|
+
*/
|
|
144
|
+
arrow = input(true, { ...(ngDevMode ? { debugName: "arrow" } : /* istanbul ignore next */ {}), transform: coerceBooleanProperty });
|
|
66
145
|
/**
|
|
67
146
|
* Present the menu as a full-width bottom-sheet on small viewports instead
|
|
68
147
|
* of an anchored panel. `undefined` follows the app-wide
|
|
@@ -189,23 +268,93 @@ class WrDropdown {
|
|
|
189
268
|
// On small viewports (when opted in) detach from the trigger and present
|
|
190
269
|
// the menu as a full-width slide-up sheet pinned to the bottom edge.
|
|
191
270
|
const asSheet = wrPresentAsSheet(this.responsive(), this.responsiveConfig);
|
|
192
|
-
const
|
|
193
|
-
|
|
271
|
+
const requested = this.position();
|
|
272
|
+
// A list of fresh objects, kept: the CDK reports the link it landed on by
|
|
273
|
+
// handing that same object back, which is how the pane learns its placement.
|
|
274
|
+
const positions = wrDropdownPositions(requested);
|
|
275
|
+
// `withGrowAfterOpen`: past the first pass the CDK never gives the pane a box
|
|
276
|
+
// larger than the last one it gave it. A menu measured with the wrong gap
|
|
277
|
+
// (see the `positionChanges` handler below) is laid out once in a box sized
|
|
278
|
+
// for the placement it is about to leave, and without this the pass that
|
|
279
|
+
// corrects it inherits that box — a `right` menu with 70px below its trigger
|
|
280
|
+
// moved above it into a 70px box and was squeezed all the same. The menu's
|
|
281
|
+
// content does not change while it is open, so the box only ever grows back
|
|
282
|
+
// to what the menu already needs.
|
|
283
|
+
const connected = asSheet
|
|
284
|
+
? null
|
|
194
285
|
: this.overlay
|
|
195
286
|
.position()
|
|
196
287
|
.flexibleConnectedTo(this.host)
|
|
197
|
-
.withPositions(
|
|
198
|
-
.withPush(true)
|
|
288
|
+
.withPositions(positions)
|
|
289
|
+
.withPush(true)
|
|
290
|
+
.withGrowAfterOpen(true);
|
|
291
|
+
const positionStrategy = connected ?? this.overlay.position().global().centerHorizontally().bottom('0');
|
|
199
292
|
this.overlayRef = this.overlay.create({
|
|
200
293
|
positionStrategy,
|
|
201
294
|
scrollStrategy: asSheet ? this.scrollStrategies.block() : this.scrollStrategies.reposition(),
|
|
202
295
|
width: asSheet ? '100%' : undefined,
|
|
203
296
|
hasBackdrop: asSheet,
|
|
204
297
|
backdropClass: asSheet ? 'wr-overlay-backdrop' : undefined,
|
|
298
|
+
// A sheet is anchored to nothing, so it gets neither a placement nor an
|
|
299
|
+
// arrow. A floating menu starts on the placement it asked for, which is
|
|
300
|
+
// what the CDK measures it with — see `wrDropdownPositions` for why that
|
|
301
|
+
// class must already be on the pane rather than arrive with the position.
|
|
205
302
|
panelClass: asSheet
|
|
206
303
|
? ['wr-dropdown-overlay', 'wr-overlay-sheet']
|
|
207
|
-
: [
|
|
304
|
+
: [
|
|
305
|
+
'wr-dropdown-overlay',
|
|
306
|
+
`wr-dropdown-overlay--${requested}`,
|
|
307
|
+
...(this.arrow() ? ['wr-dropdown-overlay--arrow'] : []),
|
|
308
|
+
],
|
|
208
309
|
});
|
|
310
|
+
// When the menu flips, the pane has to name the side it actually took: the
|
|
311
|
+
// gap padding belongs on the edge facing the trigger and the arrow hangs off
|
|
312
|
+
// that same edge, and both key off this class. The CDK emits synchronously
|
|
313
|
+
// from inside its own `apply()`, so the swap lands in the same frame as the
|
|
314
|
+
// move, and the next measurement (a scroll, a resize) sees the new gap.
|
|
315
|
+
if (connected) {
|
|
316
|
+
const ref = this.overlayRef;
|
|
317
|
+
const pane = ref.overlayElement;
|
|
318
|
+
// Index-aligned: `links[i]` is the object the CDK hands back for `chain[i]`.
|
|
319
|
+
let chain = WR_DROPDOWN_FALLBACKS[requested];
|
|
320
|
+
let links = positions;
|
|
321
|
+
connected.positionChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(({ connectionPair }) => {
|
|
322
|
+
const landed = chain[links.indexOf(connectionPair)];
|
|
323
|
+
if (!landed)
|
|
324
|
+
return;
|
|
325
|
+
for (const name of Object.keys(WR_DROPDOWN_POSITIONS)) {
|
|
326
|
+
pane.classList.toggle(`wr-dropdown-overlay--${name}`, name === landed);
|
|
327
|
+
}
|
|
328
|
+
// A `left` / `right` menu that fell below or above was tested for fit
|
|
329
|
+
// with the WRONG gap. The CDK measures the pane once per pass, for every
|
|
330
|
+
// link, with the class on it at the time — the side placement's, whose
|
|
331
|
+
// gap is inline padding — so the block link was judged by a box one gap
|
|
332
|
+
// shorter than the one the swap above has just given it, and a menu with
|
|
333
|
+
// less room than that gap was squeezed instead of flipped. Measured in
|
|
334
|
+
// Chromium at 375×700, `right` with 76px below the trigger: the menu shrank
|
|
335
|
+
// from 74px to 68px and its last row ran through the bottom border.
|
|
336
|
+
//
|
|
337
|
+
// So the sides are dropped for as long as this menu stays open and the
|
|
338
|
+
// pane is measured again, now carrying the block gap, against block links
|
|
339
|
+
// only. Not re-admitted afterwards: measured with a block gap, a side link
|
|
340
|
+
// looks one gap narrower than it renders, which is the same mistake the
|
|
341
|
+
// other way round and a menu that would flap between the two. Deferred to
|
|
342
|
+
// a microtask because this runs INSIDE the CDK's `apply()`, which records
|
|
343
|
+
// the link it landed on after this returns — a nested pass would have that
|
|
344
|
+
// record overwritten by the outer one. A microtask still lands before the
|
|
345
|
+
// frame is painted.
|
|
346
|
+
if (isSidePlacement(landed) || !chain.some(isSidePlacement))
|
|
347
|
+
return;
|
|
348
|
+
const kept = chain.flatMap((name, i) => (isSidePlacement(name) ? [] : [i]));
|
|
349
|
+
chain = kept.map(i => chain[i]);
|
|
350
|
+
links = kept.map(i => links[i]);
|
|
351
|
+
connected.withPositions(links);
|
|
352
|
+
queueMicrotask(() => {
|
|
353
|
+
if (this.overlayRef === ref)
|
|
354
|
+
ref.updatePosition();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
209
358
|
// `create()` above froze the direction into the ref as a string, and nothing
|
|
210
359
|
// in the CDK ever revisits it. This menu is where it shows first: the docs
|
|
211
360
|
// site keeps its own LTR/RTL switch INSIDE a dropdown, so the flip is made
|
|
@@ -373,7 +522,7 @@ class WrDropdown {
|
|
|
373
522
|
this.isOpen.set(false);
|
|
374
523
|
};
|
|
375
524
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDropdown, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
376
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: WrDropdown, isStandalone: true, selector: "[wrDropdown]", inputs: { menu: { classPropertyName: "menu", publicName: "wrDropdown", isSignal: true, isRequired: true, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave($event)" }, properties: { "attr.id": "triggerId", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "isOpen()", "attr.aria-controls": "isOpen() ? menu().menuId() : null" }, classAttribute: "wr-dropdown-trigger" }, exportAs: ["wrDropdown"], ngImport: i0 });
|
|
525
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: WrDropdown, isStandalone: true, selector: "[wrDropdown]", inputs: { menu: { classPropertyName: "menu", publicName: "wrDropdown", isSignal: true, isRequired: true, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, host: { listeners: { "click": "onClick($event)", "keydown": "onKeydown($event)", "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave($event)" }, properties: { "attr.id": "triggerId", "attr.aria-haspopup": "\"menu\"", "attr.aria-expanded": "isOpen()", "attr.aria-controls": "isOpen() ? menu().menuId() : null" }, classAttribute: "wr-dropdown-trigger" }, exportAs: ["wrDropdown"], ngImport: i0 });
|
|
377
526
|
}
|
|
378
527
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: WrDropdown, decorators: [{
|
|
379
528
|
type: Directive,
|
|
@@ -392,7 +541,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
392
541
|
'(mouseleave)': 'onMouseLeave($event)',
|
|
393
542
|
},
|
|
394
543
|
}]
|
|
395
|
-
}], ctorParameters: () => [], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrDropdown", required: true }] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
544
|
+
}], ctorParameters: () => [], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrDropdown", required: true }] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
396
545
|
|
|
397
546
|
/**
|
|
398
547
|
* @license
|