@ds-mo/ui 2.12.0 → 2.13.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/README.md +1 -1
- package/dist/.build-stamp +1 -1
- package/dist/components/ds-app-shell.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/{ds-card-data-viz.d.ts → ds-card-data-viz-donut.d.ts} +4 -4
- package/dist/components/ds-card-data-viz-donut.js +2 -0
- package/dist/components/ds-card-data-viz-donut.js.map +1 -0
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-setting.js.map +1 -1
- package/dist/components/{ds-header.d.ts → ds-card.d.ts} +4 -4
- package/dist/components/ds-card.js +2 -0
- package/dist/components/ds-card.js.map +1 -0
- package/dist/components/ds-chart-bar.js +1 -1
- package/dist/components/ds-chart-bar.js.map +1 -1
- package/dist/components/ds-chart-donut.js +1 -1
- package/dist/components/ds-chart-donut.js.map +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart-line.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-empty-state.js.map +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-field.js.map +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-input.js.map +1 -1
- package/dist/components/ds-loader.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-panel-tools.js.map +1 -1
- package/dist/components/ds-radio-group.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +1 -1
- package/dist/components/ds-shell-gradient-picker.js +1 -1
- package/dist/components/ds-shell-gradient-swatch.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-tab-group-nav.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/{p-BPK76IsW.js → p-1m8r0wFg.js} +2 -2
- package/dist/components/{p-BZIOTcnM.js → p-3FLHei19.js} +2 -2
- package/dist/components/p-B2_D4IDN.js +2 -0
- package/dist/components/p-B2_D4IDN.js.map +1 -0
- package/dist/components/p-B5NcWtpp.js +2 -0
- package/dist/components/p-B5NcWtpp.js.map +1 -0
- package/dist/components/{p-CyFVn2Sc.js → p-B82KZX9D.js} +2 -2
- package/dist/components/p-B_g8X0eh.js +2 -0
- package/dist/components/p-B_g8X0eh.js.map +1 -0
- package/dist/components/{p-C3ORQUOp.js → p-Be3PHv34.js} +2 -2
- package/dist/components/{p-DkcH8mRl.js → p-CQmf2CeW.js} +2 -2
- package/dist/components/{p-DRqxVEye.js → p-CtwNUCnp.js} +2 -2
- package/dist/components/p-CuRjYmBk.js +2 -0
- package/dist/components/p-CuRjYmBk.js.map +1 -0
- package/dist/components/{p-DKdI8T8v.js → p-CwARVsZQ.js} +2 -2
- package/dist/components/{p-DKdI8T8v.js.map → p-CwARVsZQ.js.map} +1 -1
- package/dist/components/{p-UbKTMQjv.js → p-DohXlNCJ.js} +2 -2
- package/dist/components/p-DuFm-ieS.js +2 -0
- package/dist/components/p-DuFm-ieS.js.map +1 -0
- package/dist/lib/nav/index.js.map +2 -2
- package/dist/lib/utils/control-width.d.ts +4 -0
- package/dist/lib/utils/index.d.ts +2 -0
- package/dist/lib/utils/index.js +14 -1
- package/dist/lib/utils/index.js.map +3 -3
- package/dist/lib/utils/token-defaults.d.ts +4 -0
- package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +4 -0
- package/dist/types/components/ButtonFilled/index.d.ts +1 -1
- package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +7 -3
- package/dist/types/components/ButtonUnfilled/index.d.ts +1 -1
- package/dist/types/components/Card/Card.d.ts +20 -0
- package/dist/types/components/Card/index.d.ts +2 -0
- package/dist/types/components/CardDataVizDonut/CardDataVizDonut.d.ts +26 -0
- package/dist/types/components/CardDataVizDonut/index.d.ts +2 -0
- package/dist/types/components/CardSetting/CardSetting.d.ts +7 -2
- package/dist/types/components/ChartDonut/ChartDonut.d.ts +24 -1
- package/dist/types/components/Select/Select.d.ts +32 -0
- package/dist/types/components/Select/index.d.ts +2 -0
- package/dist/types/components.d.ts +207 -110
- package/dist/types/utils/control-width.d.ts +5 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/token-defaults.d.ts +4 -0
- package/package.json +2 -2
- package/src/angular/index.ts +2 -3
- package/src/angular/proxies.ts +40 -56
- package/src/react/components.ts +2 -3
- package/src/react/ds-card-data-viz-donut.ts +27 -0
- package/src/react/{ds-header.ts → ds-card.ts} +7 -7
- package/src/wc/components/ButtonFilled/ButtonFilled.tsx +7 -0
- package/src/wc/components/ButtonFilled/index.ts +1 -0
- package/src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx +10 -3
- package/src/wc/components/ButtonUnfilled/index.ts +1 -0
- package/src/wc/components/Card/Card.tsx +79 -0
- package/src/wc/components/Card/index.ts +1 -0
- package/src/wc/components/{CardDataViz/CardDataViz.tsx → CardDataVizDonut/CardDataVizDonut.tsx} +47 -40
- package/src/wc/components/CardDataVizDonut/index.ts +1 -0
- package/src/wc/components/CardSetting/CardSetting.tsx +22 -35
- package/src/wc/components/ChartDonut/ChartDonut.tsx +112 -11
- package/src/wc/components/Select/Select.tsx +141 -60
- package/src/wc/components/Select/index.ts +1 -0
- package/src/wc/components.d.ts +207 -113
- package/src/wc/utils/control-width.css +28 -0
- package/src/wc/utils/control-width.ts +10 -0
- package/src/wc/utils/index.ts +2 -0
- package/src/wc/utils/token-defaults.ts +4 -0
- package/dist/components/ds-card-data-viz.js +0 -2
- package/dist/components/ds-card-data-viz.js.map +0 -1
- package/dist/components/ds-header.js +0 -2
- package/dist/components/ds-header.js.map +0 -1
- package/dist/components/ds-scrollbar.d.ts +0 -11
- package/dist/components/ds-scrollbar.js +0 -2
- package/dist/components/ds-scrollbar.js.map +0 -1
- package/dist/components/p-B9dHCgY-.js +0 -2
- package/dist/components/p-B9dHCgY-.js.map +0 -1
- package/dist/components/p-BTC9Njj6.js +0 -2
- package/dist/components/p-BTC9Njj6.js.map +0 -1
- package/dist/components/p-DusYu-if.js +0 -2
- package/dist/components/p-DusYu-if.js.map +0 -1
- package/dist/types/components/CardDataViz/CardDataViz.d.ts +0 -22
- package/dist/types/components/CardDataViz/index.d.ts +0 -2
- package/dist/types/components/Header/Header.d.ts +0 -7
- package/dist/types/components/Scrollbar/Scrollbar.d.ts +0 -27
- package/src/react/ds-card-data-viz.ts +0 -26
- package/src/react/ds-scrollbar.ts +0 -26
- package/src/wc/components/CardDataViz/index.ts +0 -1
- package/src/wc/components/Header/Header.tsx +0 -33
- package/src/wc/components/Scrollbar/Scrollbar.tsx +0 -189
- /package/dist/components/{p-BPK76IsW.js.map → p-1m8r0wFg.js.map} +0 -0
- /package/dist/components/{p-BZIOTcnM.js.map → p-3FLHei19.js.map} +0 -0
- /package/dist/components/{p-CyFVn2Sc.js.map → p-B82KZX9D.js.map} +0 -0
- /package/dist/components/{p-C3ORQUOp.js.map → p-Be3PHv34.js.map} +0 -0
- /package/dist/components/{p-DkcH8mRl.js.map → p-CQmf2CeW.js.map} +0 -0
- /package/dist/components/{p-DRqxVEye.js.map → p-CtwNUCnp.js.map} +0 -0
- /package/dist/components/{p-UbKTMQjv.js.map → p-DohXlNCJ.js.map} +0 -0
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { Component, Event, EventEmitter, h, Host, Prop } from '@stencil/core';
|
|
2
|
+
import type { CardWidth } from '../Card/Card';
|
|
2
3
|
|
|
3
|
-
export type CardSettingWidth =
|
|
4
|
-
|
|
5
|
-
const CARD_WIDTH_VARS: Record<CardSettingWidth, string> = {
|
|
6
|
-
sm: 'var(--dimension-card-width-sm)',
|
|
7
|
-
md: 'var(--dimension-card-width-md)',
|
|
8
|
-
lg: 'var(--dimension-card-width-lg)',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const FAINT_BRAND_TITLE_COLOR = 'var(--color-foreground-faint-brand)';
|
|
4
|
+
export type CardSettingWidth = CardWidth;
|
|
12
5
|
|
|
13
6
|
@Component({
|
|
14
7
|
tag: 'ds-card-setting',
|
|
@@ -19,7 +12,11 @@ export class CardSetting {
|
|
|
19
12
|
/** Section heading shown in the card header. */
|
|
20
13
|
@Prop() heading!: string;
|
|
21
14
|
|
|
22
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Card width token (`sm` / `md` / `lg`). Also sets host `min-height` to the
|
|
17
|
+
* matching `--dimension-card-height-*` so the body fills available space
|
|
18
|
+
* even when the slot is empty.
|
|
19
|
+
*/
|
|
23
20
|
@Prop() cardWidth: CardSettingWidth = 'md';
|
|
24
21
|
|
|
25
22
|
/** Controlled edit state — parent owns single-edit orchestration. */
|
|
@@ -40,41 +37,33 @@ export class CardSetting {
|
|
|
40
37
|
const editing = this.editing;
|
|
41
38
|
|
|
42
39
|
return (
|
|
43
|
-
<Host
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>
|
|
50
|
-
<header class="card-setting__header">
|
|
51
|
-
<div class="card-setting__title-wrap">
|
|
52
|
-
<ds-text
|
|
53
|
-
class="card-setting__title"
|
|
54
|
-
variant="text-title-small"
|
|
55
|
-
color={editing ? FAINT_BRAND_TITLE_COLOR : 'primary'}
|
|
56
|
-
as="h2"
|
|
57
|
-
>
|
|
58
|
-
{this.heading}
|
|
59
|
-
</ds-text>
|
|
60
|
-
</div>
|
|
40
|
+
<Host class="card-setting">
|
|
41
|
+
<ds-card
|
|
42
|
+
heading={this.heading}
|
|
43
|
+
cardWidth={this.cardWidth}
|
|
44
|
+
appearance={editing ? 'editing' : 'default'}
|
|
45
|
+
>
|
|
61
46
|
{!editing ? (
|
|
62
|
-
<ds-button-unfilled
|
|
47
|
+
<ds-button-unfilled
|
|
48
|
+
slot="actions"
|
|
49
|
+
variant="icon"
|
|
63
50
|
type="button"
|
|
64
51
|
icon="Pencil"
|
|
65
52
|
aria-label="Edit"
|
|
66
53
|
onDsClick={this.enterEdit}
|
|
67
54
|
/>
|
|
68
55
|
) : (
|
|
69
|
-
<div class="card-setting__actions">
|
|
70
|
-
<ds-button-unfilled
|
|
56
|
+
<div slot="actions" class="card-setting__actions">
|
|
57
|
+
<ds-button-unfilled
|
|
58
|
+
variant="icon"
|
|
71
59
|
type="button"
|
|
72
60
|
icon="Cross"
|
|
73
61
|
backgroundContrast="bold"
|
|
74
62
|
aria-label="Cancel"
|
|
75
63
|
onDsClick={this.exitEdit}
|
|
76
64
|
/>
|
|
77
|
-
<ds-button-filled
|
|
65
|
+
<ds-button-filled
|
|
66
|
+
variant="icon"
|
|
78
67
|
type="button"
|
|
79
68
|
icon="Check"
|
|
80
69
|
intent="brand"
|
|
@@ -84,10 +73,8 @@ export class CardSetting {
|
|
|
84
73
|
/>
|
|
85
74
|
</div>
|
|
86
75
|
)}
|
|
87
|
-
</header>
|
|
88
|
-
<div class="card-setting__body">
|
|
89
76
|
<slot />
|
|
90
|
-
</
|
|
77
|
+
</ds-card>
|
|
91
78
|
</Host>
|
|
92
79
|
);
|
|
93
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Prop, State, Event, EventEmitter, h, Host } from '@stencil/core';
|
|
1
|
+
import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';
|
|
2
2
|
import { arc, pie } from 'd3-shape';
|
|
3
3
|
import { categoryColor } from '../../utils/chart-colors';
|
|
4
4
|
import { formatCompactNumber, resolveCssLengthPx, truncateSvgTextToWidth, TOKEN_DEFAULTS } from '../../utils';
|
|
@@ -9,6 +9,8 @@ const DIMMED_OPACITY = 0.25;
|
|
|
9
9
|
const CENTER_GAP_PX = 0;
|
|
10
10
|
/** Fraction of the inner-circle diameter center text is allowed to use before truncating. */
|
|
11
11
|
const CENTER_TEXT_SAFE_WIDTH_RATIO = 0.82;
|
|
12
|
+
/** Fallback when filling before the first ResizeObserver measurement (matches fill min). */
|
|
13
|
+
const FILL_FALLBACK_PX = 128;
|
|
12
14
|
|
|
13
15
|
type DonutTooltipState = {
|
|
14
16
|
datum: ChartDatum;
|
|
@@ -22,9 +24,17 @@ type DonutTooltipState = {
|
|
|
22
24
|
scoped: true,
|
|
23
25
|
})
|
|
24
26
|
export class ChartDonut {
|
|
27
|
+
@Element() el!: HTMLElement;
|
|
28
|
+
|
|
25
29
|
/** Slices to render. Set as a JS property (not an HTML attribute). */
|
|
26
30
|
@Prop() data: ChartDatum[] = [];
|
|
27
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Explicit diameter in px. When unset, the donut sizes to its container
|
|
33
|
+
* (ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and
|
|
34
|
+
* `* 24` (192px), and stays centered in the leftover space. Prefer unset
|
|
35
|
+
* inside card layouts.
|
|
36
|
+
*/
|
|
37
|
+
@Prop() size: number | undefined;
|
|
28
38
|
/** Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px). */
|
|
29
39
|
@Prop() thickness: number | string = TOKEN_DEFAULTS.size200;
|
|
30
40
|
/** Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px). */
|
|
@@ -50,6 +60,8 @@ export class ChartDonut {
|
|
|
50
60
|
/** Center text, truncated with an ellipsis if it doesn't fit inside the inner circle. */
|
|
51
61
|
@State() private displayedValueText: string = '';
|
|
52
62
|
@State() private displayedCaptionText: string = '';
|
|
63
|
+
/** Measured fill diameter when `size` is unset. */
|
|
64
|
+
@State() private measuredSize: number = FILL_FALLBACK_PX;
|
|
53
65
|
|
|
54
66
|
/** Fires with the hovered/focused slice's datum, or `null` on leave/blur. */
|
|
55
67
|
@Event() dsSliceHover!: EventEmitter<ChartDatum | null>;
|
|
@@ -57,6 +69,79 @@ export class ChartDonut {
|
|
|
57
69
|
private valueTextEl?: SVGTextElement;
|
|
58
70
|
private captionTextEl?: SVGTextElement;
|
|
59
71
|
private wrapperEl?: HTMLDivElement;
|
|
72
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
73
|
+
|
|
74
|
+
private get isFillMode(): boolean {
|
|
75
|
+
return this.size == null || !Number.isFinite(this.size) || this.size <= 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private get fillMinPx(): number {
|
|
79
|
+
return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMin, FILL_FALLBACK_PX);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private get fillMaxPx(): number {
|
|
83
|
+
return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMax, 192);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Clamp available square side into the fill min/max band. */
|
|
87
|
+
private clampFillSize(availablePx: number): number {
|
|
88
|
+
const minPx = this.fillMinPx;
|
|
89
|
+
const maxPx = this.fillMaxPx;
|
|
90
|
+
if (!(availablePx > 0)) return minPx;
|
|
91
|
+
return Math.round(Math.min(maxPx, Math.max(minPx, availablePx)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private get resolvedSize(): number {
|
|
95
|
+
if (!this.isFillMode) return this.size as number;
|
|
96
|
+
return this.clampFillSize(this.measuredSize);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
componentDidLoad() {
|
|
100
|
+
this.syncResizeObserver();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Watch('size')
|
|
104
|
+
onSizePropChange() {
|
|
105
|
+
this.syncResizeObserver();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
disconnectedCallback() {
|
|
109
|
+
this.teardownResizeObserver();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private syncResizeObserver() {
|
|
113
|
+
this.teardownResizeObserver();
|
|
114
|
+
if (!this.isFillMode) return;
|
|
115
|
+
// Observe the host (which fills its parent), not the wrapper — the wrapper
|
|
116
|
+
// only wraps the SVG and would collapse to the clamped size, locking fill at min.
|
|
117
|
+
const target = this.el;
|
|
118
|
+
if (!target || typeof ResizeObserver === 'undefined') return;
|
|
119
|
+
|
|
120
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
121
|
+
const entry = entries[0];
|
|
122
|
+
if (!entry) return;
|
|
123
|
+
this.applyAvailableSize(entry.contentRect.width, entry.contentRect.height);
|
|
124
|
+
});
|
|
125
|
+
this.resizeObserver.observe(target);
|
|
126
|
+
this.measureHost();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private measureHost() {
|
|
130
|
+
const rect = this.el.getBoundingClientRect();
|
|
131
|
+
this.applyAvailableSize(rect.width, rect.height);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private applyAvailableSize(width: number, height: number) {
|
|
135
|
+
const available = Math.floor(Math.min(width, height));
|
|
136
|
+
if (available > 0 && available !== this.measuredSize) {
|
|
137
|
+
this.measuredSize = available;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private teardownResizeObserver() {
|
|
142
|
+
this.resizeObserver?.disconnect();
|
|
143
|
+
this.resizeObserver = null;
|
|
144
|
+
}
|
|
60
145
|
|
|
61
146
|
private handleHover(datum: ChartDatum | null, event?: MouseEvent) {
|
|
62
147
|
this.hoveredLabel = datum?.label ?? null;
|
|
@@ -76,14 +161,21 @@ export class ChartDonut {
|
|
|
76
161
|
private updateTooltip(datum: ChartDatum, event?: MouseEvent) {
|
|
77
162
|
const wrapper = this.wrapperEl;
|
|
78
163
|
if (!wrapper) return;
|
|
164
|
+
const size = this.resolvedSize;
|
|
79
165
|
const rect = wrapper.getBoundingClientRect();
|
|
80
|
-
const x = event ? event.clientX - rect.left :
|
|
81
|
-
const y = event ? event.clientY - rect.top :
|
|
166
|
+
const x = event ? event.clientX - rect.left : size / 2;
|
|
167
|
+
const y = event ? event.clientY - rect.top : size / 2;
|
|
82
168
|
this.tooltip = { datum, x, y };
|
|
83
169
|
}
|
|
84
170
|
|
|
85
171
|
componentDidRender() {
|
|
86
|
-
|
|
172
|
+
// Re-bind after HMR when DidLoad's observer was dropped.
|
|
173
|
+
if (this.isFillMode && !this.resizeObserver) {
|
|
174
|
+
this.syncResizeObserver();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const size = this.resolvedSize;
|
|
178
|
+
const radius = size / 2;
|
|
87
179
|
const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);
|
|
88
180
|
const innerRadius = radius - thicknessPx;
|
|
89
181
|
const maxWidth = innerRadius * 2 * CENTER_TEXT_SAFE_WIDTH_RATIO;
|
|
@@ -105,7 +197,8 @@ export class ChartDonut {
|
|
|
105
197
|
}
|
|
106
198
|
|
|
107
199
|
render() {
|
|
108
|
-
const
|
|
200
|
+
const size = this.resolvedSize;
|
|
201
|
+
const radius = size / 2;
|
|
109
202
|
const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);
|
|
110
203
|
const cornerRadiusPx = resolveCssLengthPx(this.cornerRadius, TOKEN_DEFAULTS.radius025);
|
|
111
204
|
const innerRadius = radius - thicknessPx;
|
|
@@ -146,21 +239,29 @@ export class ChartDonut {
|
|
|
146
239
|
const fullValueText = this.centerValue ?? formatCompactNumber(total);
|
|
147
240
|
const fullCaptionText = this.centerCaption ?? '';
|
|
148
241
|
const tip = this.tooltip;
|
|
242
|
+
const fill = this.isFillMode;
|
|
149
243
|
|
|
150
244
|
return (
|
|
151
|
-
<Host
|
|
245
|
+
<Host
|
|
246
|
+
class={{
|
|
247
|
+
'chart-donut': true,
|
|
248
|
+
'chart-donut--fill': fill,
|
|
249
|
+
'chart-donut--fixed': !fill,
|
|
250
|
+
}}
|
|
251
|
+
style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}
|
|
252
|
+
>
|
|
152
253
|
<div
|
|
153
254
|
class="chart-donut__wrapper"
|
|
154
|
-
style={{ width: `${
|
|
255
|
+
style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}
|
|
155
256
|
ref={el => {
|
|
156
257
|
this.wrapperEl = (el as HTMLDivElement) ?? undefined;
|
|
157
258
|
}}
|
|
158
259
|
>
|
|
159
260
|
<svg
|
|
160
261
|
class="chart-donut__svg"
|
|
161
|
-
viewBox={`0 0 ${
|
|
162
|
-
width={
|
|
163
|
-
height={
|
|
262
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
263
|
+
width={size}
|
|
264
|
+
height={size}
|
|
164
265
|
>
|
|
165
266
|
<g transform={`translate(${radius}, ${radius})`}>
|
|
166
267
|
{isEmpty ? (
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';
|
|
2
|
+
import { controlWidthClass, type ControlWidth } from '../../utils/control-width';
|
|
3
|
+
import { resolveCssLengthPx, TOKEN_DEFAULTS } from '../../utils';
|
|
2
4
|
import type { MenuItemData } from '../Menu/menu-types';
|
|
3
5
|
|
|
4
6
|
export interface SelectOption {
|
|
@@ -6,6 +8,28 @@ export interface SelectOption {
|
|
|
6
8
|
value: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
export type SelectSize = 'md' | 'sm' | 'xs';
|
|
12
|
+
|
|
13
|
+
export type SelectWidth = ControlWidth;
|
|
14
|
+
|
|
15
|
+
/** Regular (non-emphasis) type per density — selects stay visually lighter than buttons. */
|
|
16
|
+
const TEXT_VARIANT: Record<SelectSize, string> = {
|
|
17
|
+
md: 'text-body-medium',
|
|
18
|
+
sm: 'text-body-small',
|
|
19
|
+
xs: 'text-caption',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** `ds-icon` size matching control-density icon metrics. */
|
|
23
|
+
const ICON_SIZE: Record<SelectSize, 'md' | 'sm' | 'xs'> = {
|
|
24
|
+
md: 'md',
|
|
25
|
+
sm: 'sm',
|
|
26
|
+
xs: 'xs',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dropdown select — unfilled-button chrome (label + trailing ChevronDown) in
|
|
31
|
+
* control-density sizes, with `ds-menu` for the option list.
|
|
32
|
+
*/
|
|
9
33
|
@Component({
|
|
10
34
|
tag: 'ds-select',
|
|
11
35
|
styleUrl: 'Select.css',
|
|
@@ -24,11 +48,28 @@ export class Select {
|
|
|
24
48
|
@Prop({ mutable: true }) value: string = '';
|
|
25
49
|
|
|
26
50
|
/** Placeholder shown when no value is selected. */
|
|
27
|
-
@Prop() placeholder: string = 'Select
|
|
51
|
+
@Prop() placeholder: string = 'Select';
|
|
52
|
+
|
|
53
|
+
/** Control density (height, padding, icon, type). */
|
|
54
|
+
@Prop() size: SelectSize = 'md';
|
|
55
|
+
|
|
56
|
+
/** Width fit — fill the parent (default) or hug content. */
|
|
57
|
+
@Prop() width: SelectWidth = 'fill';
|
|
28
58
|
|
|
29
59
|
/** Disables interaction (25% opacity via ds-control-inactive). */
|
|
30
60
|
@Prop() isInactive: boolean = false;
|
|
31
61
|
|
|
62
|
+
/**
|
|
63
|
+
* When a value is selected, render the selected interaction fill (same recipe
|
|
64
|
+
* as unfilled-button `activeFill`). Default `true`. Pass `false` for
|
|
65
|
+
* foreground-only selection (primary label, no fill).
|
|
66
|
+
* Selected/open promotes the label to primary; chevron stays secondary.
|
|
67
|
+
*/
|
|
68
|
+
@Prop() activeFill: boolean = true;
|
|
69
|
+
|
|
70
|
+
/** Show a 1px secondary inset border. Default on (matches unfilled button). */
|
|
71
|
+
@Prop() hasBorder: boolean = true;
|
|
72
|
+
|
|
32
73
|
@Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
|
|
33
74
|
@Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;
|
|
34
75
|
|
|
@@ -39,32 +80,28 @@ export class Select {
|
|
|
39
80
|
|
|
40
81
|
private triggerEl: HTMLButtonElement | null = null;
|
|
41
82
|
private menuEl: HTMLDsMenuElement | null = null;
|
|
83
|
+
/** True when the open was keyboard-driven — menu shows initial focus ring. */
|
|
84
|
+
private openWithFocusVisible = false;
|
|
42
85
|
|
|
43
86
|
componentDidLoad() {
|
|
44
|
-
// Wire up the menu anchor to the trigger button
|
|
45
87
|
if (this.menuEl && this.triggerEl) {
|
|
46
88
|
this.menuEl.anchor = this.triggerEl;
|
|
47
89
|
}
|
|
48
|
-
|
|
49
|
-
this.menuEl?.addEventListener('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
// Listen for menu close event
|
|
58
|
-
this.menuEl?.addEventListener('dsClose', () => {
|
|
59
|
-
this.isOpen = false;
|
|
60
|
-
});
|
|
90
|
+
this.menuEl?.addEventListener('dsSelect', this.handleMenuSelect);
|
|
91
|
+
this.menuEl?.addEventListener('dsClose', this.handleMenuClose);
|
|
92
|
+
this.syncMenuItems();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
disconnectedCallback() {
|
|
96
|
+
this.menuEl?.removeEventListener('dsSelect', this.handleMenuSelect);
|
|
97
|
+
this.menuEl?.removeEventListener('dsClose', this.handleMenuClose);
|
|
61
98
|
}
|
|
62
99
|
|
|
63
100
|
@Watch('isOpen')
|
|
64
101
|
onIsOpenChange(open: boolean) {
|
|
65
|
-
if (this.menuEl)
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
if (!this.menuEl) return;
|
|
103
|
+
this.menuEl.initialFocusVisible = open && this.openWithFocusVisible;
|
|
104
|
+
this.menuEl.open = open;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
@Watch('options')
|
|
@@ -77,6 +114,19 @@ export class Select {
|
|
|
77
114
|
this.syncMenuItems();
|
|
78
115
|
}
|
|
79
116
|
|
|
117
|
+
private handleMenuSelect = (e: Event) => {
|
|
118
|
+
const detail = (e as CustomEvent<MenuItemData>).detail;
|
|
119
|
+
if (detail?.value !== undefined) {
|
|
120
|
+
this.value = String(detail.value);
|
|
121
|
+
this.dsChange.emit(this.value);
|
|
122
|
+
}
|
|
123
|
+
this.close();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
private handleMenuClose = () => {
|
|
127
|
+
this.close();
|
|
128
|
+
};
|
|
129
|
+
|
|
80
130
|
private syncMenuItems() {
|
|
81
131
|
if (!this.menuEl) return;
|
|
82
132
|
this.menuEl.items = this.options.map(opt => ({
|
|
@@ -86,32 +136,46 @@ export class Select {
|
|
|
86
136
|
}));
|
|
87
137
|
}
|
|
88
138
|
|
|
89
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Size/place the menu so option labels line up with the select label.
|
|
141
|
+
* Menu section padding insets items; we expand min-width by that chrome and
|
|
142
|
+
* shift left by the same amount (`alignOffset`). Menu can grow wider than
|
|
143
|
+
* the field (left-bottom / `align="start"`) when labels need more room.
|
|
144
|
+
*/
|
|
145
|
+
private syncMenuLayout() {
|
|
146
|
+
if (!this.menuEl || !this.triggerEl) return;
|
|
147
|
+
const sectionPadPx = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);
|
|
148
|
+
const triggerW = this.triggerEl.offsetWidth;
|
|
149
|
+
// Prefer min-width over fixed width so long options can grow the menu.
|
|
150
|
+
this.menuEl.menuWidth = '';
|
|
151
|
+
this.menuEl.minWidth = `${triggerW + sectionPadPx * 2}px`;
|
|
152
|
+
this.menuEl.alignOffset = -sectionPadPx;
|
|
153
|
+
this.menuEl.anchor = this.triggerEl;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private open(opts: { focusVisible: boolean } = { focusVisible: false }) {
|
|
90
157
|
if (this.isInactive || !this.options.length) return;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.menuEl.menuWidth = `${this.triggerEl.offsetWidth}px`;
|
|
94
|
-
this.menuEl.anchor = this.triggerEl;
|
|
95
|
-
}
|
|
158
|
+
this.openWithFocusVisible = opts.focusVisible;
|
|
159
|
+
this.syncMenuLayout();
|
|
96
160
|
this.syncMenuItems();
|
|
97
161
|
this.isOpen = true;
|
|
98
162
|
}
|
|
99
163
|
|
|
100
164
|
private close() {
|
|
101
165
|
this.isOpen = false;
|
|
166
|
+
this.openWithFocusVisible = false;
|
|
102
167
|
}
|
|
103
168
|
|
|
104
|
-
private toggle() {
|
|
169
|
+
private toggle(opts: { focusVisible: boolean }) {
|
|
105
170
|
if (this.isOpen) {
|
|
106
171
|
this.close();
|
|
107
172
|
} else {
|
|
108
|
-
this.open();
|
|
173
|
+
this.open(opts);
|
|
109
174
|
}
|
|
110
175
|
}
|
|
111
176
|
|
|
112
177
|
@Listen('keydown')
|
|
113
178
|
handleKeyDown(e: KeyboardEvent) {
|
|
114
|
-
// Only handle keydown on the trigger button
|
|
115
179
|
if (document.activeElement !== this.triggerEl) return;
|
|
116
180
|
|
|
117
181
|
if (!this.isOpen) {
|
|
@@ -121,67 +185,84 @@ export class Select {
|
|
|
121
185
|
case 'Enter':
|
|
122
186
|
case ' ':
|
|
123
187
|
e.preventDefault();
|
|
124
|
-
this.open();
|
|
188
|
+
this.open({ focusVisible: true });
|
|
125
189
|
break;
|
|
126
190
|
}
|
|
127
|
-
} else {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.triggerEl?.focus();
|
|
132
|
-
}
|
|
191
|
+
} else if (e.key === 'Escape') {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
this.close();
|
|
194
|
+
this.triggerEl?.focus();
|
|
133
195
|
}
|
|
134
196
|
}
|
|
135
197
|
|
|
136
198
|
private get selectedLabel(): string {
|
|
137
|
-
|
|
138
|
-
return found?.label ?? '';
|
|
199
|
+
return this.options.find(o => o.value === this.value)?.label ?? '';
|
|
139
200
|
}
|
|
140
201
|
|
|
141
202
|
private get hasSelection(): boolean {
|
|
142
|
-
return
|
|
203
|
+
return this.options.some(o => o.value === this.value);
|
|
143
204
|
}
|
|
144
205
|
|
|
145
206
|
render() {
|
|
146
|
-
const label = this.hasSelection ? this.selectedLabel : this.placeholder;
|
|
147
207
|
const showPlaceholder = !this.hasSelection;
|
|
208
|
+
const hasValue = !showPlaceholder;
|
|
209
|
+
const label = showPlaceholder ? this.placeholder : this.selectedLabel;
|
|
210
|
+
const textVariant = TEXT_VARIANT[this.size];
|
|
211
|
+
const iconSize = ICON_SIZE[this.size];
|
|
212
|
+
const density = `ds-control--${this.size}`;
|
|
213
|
+
// Selected fill when a value is set (or while open), gated by `activeFill`.
|
|
214
|
+
const showSelectedFill =
|
|
215
|
+
!this.isInactive && this.activeFill && (hasValue || this.isOpen);
|
|
148
216
|
|
|
149
217
|
return (
|
|
150
|
-
<Host
|
|
218
|
+
<Host
|
|
219
|
+
class={{
|
|
220
|
+
'select-host': true,
|
|
221
|
+
'ds-control-inactive': this.isInactive,
|
|
222
|
+
'ds-control--md': this.size === 'md',
|
|
223
|
+
'ds-control--sm': this.size === 'sm',
|
|
224
|
+
'ds-control--xs': this.size === 'xs',
|
|
225
|
+
...controlWidthClass(this.width),
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
151
228
|
<button
|
|
152
229
|
ref={el => {
|
|
153
230
|
this.triggerEl = (el as HTMLButtonElement) ?? null;
|
|
154
231
|
}}
|
|
155
232
|
type="button"
|
|
156
|
-
class={{
|
|
233
|
+
class={{
|
|
234
|
+
trigger: true,
|
|
235
|
+
'ds-focus-ring-inset': true,
|
|
236
|
+
'ds-interaction-fill': true,
|
|
237
|
+
'ds-interaction-fill--selected': showSelectedFill,
|
|
238
|
+
'trigger--open': this.isOpen,
|
|
239
|
+
'trigger--bordered': this.hasBorder,
|
|
240
|
+
'trigger--placeholder': showPlaceholder,
|
|
241
|
+
'trigger--has-value': hasValue,
|
|
242
|
+
[density]: true,
|
|
243
|
+
}}
|
|
157
244
|
disabled={this.isInactive}
|
|
158
245
|
role="combobox"
|
|
159
|
-
aria-haspopup="
|
|
160
|
-
aria-expanded={this.isOpen}
|
|
246
|
+
aria-haspopup="menu"
|
|
247
|
+
aria-expanded={String(this.isOpen)}
|
|
161
248
|
aria-label={this.ariaLabel}
|
|
162
249
|
aria-labelledby={this.ariaLabelledby}
|
|
163
|
-
onClick={() => this.toggle()}
|
|
250
|
+
onClick={() => this.toggle({ focusVisible: false })}
|
|
164
251
|
>
|
|
165
|
-
<span
|
|
252
|
+
<span
|
|
253
|
+
class={{
|
|
254
|
+
'trigger__label': true,
|
|
255
|
+
[textVariant]: true,
|
|
256
|
+
'ds-interaction-fill__content': true,
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
166
259
|
{label}
|
|
167
260
|
</span>
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
height="16"
|
|
172
|
-
viewBox="0 0 16 16"
|
|
173
|
-
fill="none"
|
|
174
|
-
aria-hidden="true"
|
|
175
|
-
>
|
|
176
|
-
<path
|
|
177
|
-
d="M4 6L8 10L12 6"
|
|
178
|
-
stroke="currentColor"
|
|
179
|
-
stroke-width="1.25"
|
|
180
|
-
stroke-linecap="round"
|
|
181
|
-
stroke-linejoin="round"
|
|
182
|
-
/>
|
|
183
|
-
</svg>
|
|
261
|
+
<span class="trigger__chevron ds-interaction-fill__content" aria-hidden="true">
|
|
262
|
+
<ds-icon name="ChevronDown" size={iconSize} color="inherit" />
|
|
263
|
+
</span>
|
|
184
264
|
</button>
|
|
265
|
+
{/* bottom + start: left-aligned under the field; wider menus grow right. */}
|
|
185
266
|
<ds-menu
|
|
186
267
|
ref={el => {
|
|
187
268
|
this.menuEl = (el as HTMLDsMenuElement) ?? null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { SelectOption, SelectSize, SelectWidth } from './Select';
|