@ds-mo/ui 1.0.1 → 1.1.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/dist/components/ds-bar-nav-action.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-breadcrumb.js +1 -1
- package/dist/components/ds-button-group.js +1 -1
- package/dist/components/ds-button.js +1 -1
- package/dist/components/ds-card.js +1 -1
- package/dist/components/ds-card.js.map +1 -1
- package/dist/components/ds-checkbox.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-fade.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-header.js +1 -1
- package/dist/components/ds-header.js.map +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-label-wrap.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-radio-group.js +1 -1
- package/dist/components/ds-radio-group.js.map +1 -1
- package/dist/components/ds-scrollbar.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-surface.js +1 -1
- package/dist/components/ds-tab-group-nav.d.ts +11 -0
- package/dist/components/ds-tab-group-nav.js +2 -0
- package/dist/components/ds-tab-group-nav.js.map +1 -0
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tab-group.js.map +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast-provider.js +1 -1
- package/dist/components/ds-toggle-button-group.js +1 -1
- package/dist/components/ds-toggle-button.js +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/p-BQq26pt9.js +2 -0
- package/dist/components/p-BQq26pt9.js.map +1 -0
- package/dist/components/p-CMyQsk0K.js +2 -0
- package/dist/components/p-CMyQsk0K.js.map +1 -0
- package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
- package/dist/components/p-CpbnmEu1.js +2 -0
- package/dist/components/p-CpbnmEu1.js.map +1 -0
- package/dist/components/{p-DHF5BQbO.js → p-Dd0GrKaF.js} +2 -2
- package/dist/components/p-H89PzqGU.js +2 -0
- package/dist/components/p-H89PzqGU.js.map +1 -0
- package/dist/components/{p-Dfx0StHj.js → p-Vwlmhl4E.js} +2 -2
- package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
- package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
- package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
- package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
- package/dist/types/components.d.ts +68 -0
- package/package.json +39 -9
- package/src/angular/index.ts +1 -0
- package/src/angular/proxies.ts +28 -0
- package/src/react/components.ts +46 -0
- package/src/react/ds-accordion.ts +27 -0
- package/src/react/ds-badge.ts +26 -0
- package/src/react/ds-banner.ts +27 -0
- package/src/react/ds-bar-nav-action.ts +27 -0
- package/src/react/ds-bar-nav.ts +33 -0
- package/src/react/ds-breadcrumb.ts +27 -0
- package/src/react/ds-button-group.ts +27 -0
- package/src/react/ds-button.ts +35 -0
- package/src/react/ds-card.ts +26 -0
- package/src/react/ds-checkbox.ts +27 -0
- package/src/react/ds-divider.ts +26 -0
- package/src/react/ds-empty-state.ts +26 -0
- package/src/react/ds-fade.ts +26 -0
- package/src/react/ds-field.ts +26 -0
- package/src/react/ds-header.ts +26 -0
- package/src/react/ds-icon.ts +26 -0
- package/src/react/ds-input.ts +33 -0
- package/src/react/ds-label-wrap.ts +26 -0
- package/src/react/ds-loader.ts +26 -0
- package/src/react/ds-menu.ts +33 -0
- package/src/react/ds-modal.ts +27 -0
- package/src/react/ds-pagination.ts +27 -0
- package/src/react/ds-panel-nav.ts +37 -0
- package/src/react/ds-radio-group.ts +27 -0
- package/src/react/ds-scrollbar.ts +26 -0
- package/src/react/ds-select.ts +27 -0
- package/src/react/ds-sidebar.ts +33 -0
- package/src/react/ds-skeleton.ts +26 -0
- package/src/react/ds-slider.ts +27 -0
- package/src/react/ds-surface.ts +26 -0
- package/src/react/ds-tab-group-nav.ts +27 -0
- package/src/react/ds-tab-group.ts +27 -0
- package/src/react/ds-table.ts +35 -0
- package/src/react/ds-tag.ts +35 -0
- package/src/react/ds-text.ts +26 -0
- package/src/react/ds-toast-provider.ts +26 -0
- package/src/react/ds-toggle-button-group.ts +27 -0
- package/src/react/ds-toggle-button.ts +27 -0
- package/src/react/ds-toggle.ts +27 -0
- package/src/react/ds-tooltip.ts +26 -0
- package/src/wc/components/Accordion/Accordion.tsx +144 -0
- package/src/wc/components/Badge/Badge.tsx +34 -0
- package/src/wc/components/Badge/index.ts +1 -0
- package/src/wc/components/Banner/Banner.tsx +148 -0
- package/src/wc/components/BarNav/BarNav.tsx +605 -0
- package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
- package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
- package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
- package/src/wc/components/BarNav/index.ts +1 -0
- package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
- package/src/wc/components/BarNavAction/index.ts +1 -0
- package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
- package/src/wc/components/Button/Button.tsx +158 -0
- package/src/wc/components/Button/index.ts +1 -0
- package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
- package/src/wc/components/Card/Card.tsx +26 -0
- package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
- package/src/wc/components/Checkbox/index.ts +1 -0
- package/src/wc/components/Divider/Divider.tsx +23 -0
- package/src/wc/components/Divider/index.ts +2 -0
- package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
- package/src/wc/components/Fade/Fade.tsx +32 -0
- package/src/wc/components/Fade/index.ts +1 -0
- package/src/wc/components/Field/Field.tsx +28 -0
- package/src/wc/components/Header/Header.tsx +33 -0
- package/src/wc/components/Icon/Icon.tsx +112 -0
- package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
- package/src/wc/components/Icon/index.ts +1 -0
- package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
- package/src/wc/components/Input/Input.tsx +100 -0
- package/src/wc/components/Input/index.ts +1 -0
- package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
- package/src/wc/components/Loader/Loader.tsx +47 -0
- package/src/wc/components/Loader/index.ts +1 -0
- package/src/wc/components/Menu/Menu.tsx +340 -0
- package/src/wc/components/Modal/Modal.tsx +197 -0
- package/src/wc/components/Pagination/Pagination.tsx +97 -0
- package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
- package/src/wc/components/PanelNav/index.ts +7 -0
- package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
- package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
- package/src/wc/components/RadioGroup/index.ts +1 -0
- package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
- package/src/wc/components/Select/Select.tsx +195 -0
- package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
- package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
- package/src/wc/components/Skeleton/index.ts +1 -0
- package/src/wc/components/Slider/Slider.tsx +68 -0
- package/src/wc/components/Slider/index.ts +1 -0
- package/src/wc/components/Surface/Surface.tsx +81 -0
- package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
- package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
- package/src/wc/components/Table/Table.tsx +268 -0
- package/src/wc/components/Tag/Tag.tsx +135 -0
- package/src/wc/components/Tag/index.ts +1 -0
- package/src/wc/components/Text/Text.tsx +100 -0
- package/src/wc/components/Text/index.ts +1 -0
- package/src/wc/components/Toast/Toast.tsx +174 -0
- package/src/wc/components/Toast/toast-service.ts +60 -0
- package/src/wc/components/Toggle/Toggle.tsx +43 -0
- package/src/wc/components/Toggle/index.ts +1 -0
- package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
- package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
- package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
- package/src/wc/components.d.ts +3028 -0
- package/dist/components/p-DV8pmt1m.js +0 -2
- package/dist/components/p-DV8pmt1m.js.map +0 -1
- package/dist/components/p-Dqga4VCz.js +0 -2
- package/dist/components/p-Dqga4VCz.js.map +0 -1
- package/dist/components/p-STnE_bmQ.js +0 -2
- package/dist/components/p-STnE_bmQ.js.map +0 -1
- /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
- /package/dist/components/{p-DHF5BQbO.js.map → p-Dd0GrKaF.js.map} +0 -0
- /package/dist/components/{p-Dfx0StHj.js.map → p-Vwlmhl4E.js.map} +0 -0
- /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { PanelNavGroup, PanelNavItem, PanelNavVariant } from './PanelNav';
|
|
2
|
+
|
|
3
|
+
/** Whether `path` matches `href` at a segment boundary (exact or child path). */
|
|
4
|
+
export function hrefMatchesPath(path: string, href: string): boolean {
|
|
5
|
+
return path === href || path.startsWith(`${href}/`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Derive the active nav item id from a URL using longest segment-boundary prefix match. */
|
|
9
|
+
export function deriveActiveIdFromUrl(path: string, items: PanelNavItem[]): string {
|
|
10
|
+
if (!path) return '';
|
|
11
|
+
|
|
12
|
+
let bestId = '';
|
|
13
|
+
let bestLen = 0;
|
|
14
|
+
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
if (!item.href) continue;
|
|
17
|
+
if (hrefMatchesPath(path, item.href) && item.href.length > bestLen) {
|
|
18
|
+
bestId = item.id;
|
|
19
|
+
bestLen = item.href.length;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return bestId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Total nav item count across all groups. */
|
|
27
|
+
export function countPanelNavItems(groups: PanelNavGroup[]): number {
|
|
28
|
+
return groups.reduce((sum, g) => sum + g.items.length, 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parse `groups` from either a JSON attribute string or a JS property array. */
|
|
32
|
+
export function parsePanelNavGroups(groups: string | unknown): PanelNavGroup[] {
|
|
33
|
+
if (Array.isArray(groups)) return groups;
|
|
34
|
+
if (typeof groups === 'string') {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(groups);
|
|
37
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `document.documentElement` attribute for pre-bootstrap variant hints (SPA hard reload). */
|
|
46
|
+
export const PANEL_NAV_VARIANT_HINT_ATTR = 'data-panel-nav-variant';
|
|
47
|
+
|
|
48
|
+
/** Parse `variant` from a host attribute value. */
|
|
49
|
+
export function readPanelNavVariantAttr(attr: string | null): PanelNavVariant | undefined {
|
|
50
|
+
if (attr === 'settings' || attr === 'dashboard') return attr;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Set a document-level variant hint before the custom element upgrades (hard reload). */
|
|
55
|
+
export function setPanelNavVariantHint(variant: PanelNavVariant): void {
|
|
56
|
+
if (typeof document === 'undefined') return;
|
|
57
|
+
document.documentElement.setAttribute(PANEL_NAV_VARIANT_HINT_ATTR, variant);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function clearPanelNavVariantHint(): void {
|
|
61
|
+
if (typeof document === 'undefined') return;
|
|
62
|
+
document.documentElement.removeAttribute(PANEL_NAV_VARIANT_HINT_ATTR);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolve initial variant — host attr, then document hint, then prop default. */
|
|
66
|
+
export function resolvePanelNavVariant(
|
|
67
|
+
variantProp: PanelNavVariant,
|
|
68
|
+
variantAttr: string | null,
|
|
69
|
+
documentVariantAttr: string | null = typeof document !== 'undefined'
|
|
70
|
+
? document.documentElement.getAttribute(PANEL_NAV_VARIANT_HINT_ATTR)
|
|
71
|
+
: null,
|
|
72
|
+
): PanelNavVariant {
|
|
73
|
+
return (
|
|
74
|
+
readPanelNavVariantAttr(variantAttr) ??
|
|
75
|
+
readPanelNavVariantAttr(documentVariantAttr) ??
|
|
76
|
+
variantProp
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve whether VT is disabled from prop and/or host attribute. */
|
|
81
|
+
export function resolvePanelNavDisableVt(prop: boolean, attr: string | null): boolean {
|
|
82
|
+
if (prop) return true;
|
|
83
|
+
if (attr === null) return false;
|
|
84
|
+
return attr !== 'false';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when internal rendered surface lags the host `variant` prop. */
|
|
88
|
+
export function shouldResyncPanelNavVariant(
|
|
89
|
+
renderedVariant: PanelNavVariant,
|
|
90
|
+
variant: PanelNavVariant,
|
|
91
|
+
): boolean {
|
|
92
|
+
return renderedVariant !== variant;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when host `groups` has items but internal parsed state is still empty. */
|
|
96
|
+
export function shouldResyncPanelNavGroups(
|
|
97
|
+
parsedGroups: PanelNavGroup[],
|
|
98
|
+
groups: string | unknown,
|
|
99
|
+
): boolean {
|
|
100
|
+
return (
|
|
101
|
+
countPanelNavItems(parsedGroups) === 0 &&
|
|
102
|
+
countPanelNavItems(parsePanelNavGroups(groups)) > 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let vtStyleInjected = false;
|
|
107
|
+
|
|
108
|
+
/** Inject view-transition suppress styles once per document (shared by PanelNav + hosts). */
|
|
109
|
+
export function ensurePanelNavVtStyle(): void {
|
|
110
|
+
if (vtStyleInjected) return;
|
|
111
|
+
const id = 'ds-panel-nav-vt-style';
|
|
112
|
+
if (document.getElementById(id)) {
|
|
113
|
+
vtStyleInjected = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const style = document.createElement('style');
|
|
117
|
+
style.id = id;
|
|
118
|
+
style.textContent = [
|
|
119
|
+
'::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}',
|
|
120
|
+
'::view-transition-new(root){clip-path:circle(0px at var(--vt-x,50%) var(--vt-y,50%))}',
|
|
121
|
+
].join('\n');
|
|
122
|
+
document.head.appendChild(style);
|
|
123
|
+
vtStyleInjected = true;
|
|
124
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, Element, Listen, h, Host } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
export interface RadioOption {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
inactive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
tag: 'ds-radio-group',
|
|
11
|
+
styleUrl: 'RadioGroup.css',
|
|
12
|
+
scoped: true,
|
|
13
|
+
})
|
|
14
|
+
export class RadioGroup {
|
|
15
|
+
@Element() el!: HTMLElement;
|
|
16
|
+
|
|
17
|
+
@Prop() options: RadioOption[] = [];
|
|
18
|
+
@Prop({ mutable: true }) value: string = '';
|
|
19
|
+
@Prop() direction: 'vertical' | 'horizontal' = 'vertical';
|
|
20
|
+
@Prop() inactive: boolean = false;
|
|
21
|
+
@Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
|
|
22
|
+
@Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;
|
|
23
|
+
|
|
24
|
+
@Event() dsChange!: EventEmitter<string>;
|
|
25
|
+
|
|
26
|
+
private get activeItems(): HTMLElement[] {
|
|
27
|
+
return Array.from(this.el.querySelectorAll<HTMLElement>('[data-radio-item]:not([data-inactive])'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Listen('keydown')
|
|
31
|
+
handleKeyDown(e: KeyboardEvent) {
|
|
32
|
+
const items = this.activeItems;
|
|
33
|
+
if (!items.length) return;
|
|
34
|
+
|
|
35
|
+
const focused = items.find(item => item === document.activeElement);
|
|
36
|
+
if (!focused) return;
|
|
37
|
+
|
|
38
|
+
const idx = items.indexOf(focused);
|
|
39
|
+
let next = -1;
|
|
40
|
+
|
|
41
|
+
switch (e.key) {
|
|
42
|
+
case 'ArrowDown':
|
|
43
|
+
case 'ArrowRight':
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
next = (idx + 1) % items.length;
|
|
46
|
+
break;
|
|
47
|
+
case 'ArrowUp':
|
|
48
|
+
case 'ArrowLeft':
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
next = (idx - 1 + items.length) % items.length;
|
|
51
|
+
break;
|
|
52
|
+
case 'Home':
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
next = 0;
|
|
55
|
+
break;
|
|
56
|
+
case 'End':
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
next = items.length - 1;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (next >= 0) {
|
|
63
|
+
items[next].focus();
|
|
64
|
+
const nextValue = items[next].dataset['value'];
|
|
65
|
+
if (nextValue && nextValue !== this.value) {
|
|
66
|
+
this.value = nextValue;
|
|
67
|
+
this.dsChange.emit(nextValue);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private selectItem(optValue: string) {
|
|
73
|
+
if (this.inactive) return;
|
|
74
|
+
if (optValue !== this.value) {
|
|
75
|
+
this.value = optValue;
|
|
76
|
+
this.dsChange.emit(optValue);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
render() {
|
|
81
|
+
const selectedIdx = this.options.findIndex(o => o.value === this.value);
|
|
82
|
+
const firstActiveIdx = this.options.findIndex(o => !this.inactive && !o.inactive);
|
|
83
|
+
const focusableIdx = selectedIdx >= 0 && !this.inactive && !this.options[selectedIdx]?.inactive
|
|
84
|
+
? selectedIdx
|
|
85
|
+
: firstActiveIdx;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Host
|
|
89
|
+
role="radiogroup"
|
|
90
|
+
aria-label={this.ariaLabel}
|
|
91
|
+
aria-labelledby={this.ariaLabelledby}
|
|
92
|
+
class={{ group: true, 'group--horizontal': this.direction === 'horizontal' }}
|
|
93
|
+
>
|
|
94
|
+
{this.options.map((opt, i) => {
|
|
95
|
+
const isItemInactive = this.inactive || !!opt.inactive;
|
|
96
|
+
const isChecked = opt.value === this.value;
|
|
97
|
+
const tabIdx = isItemInactive ? -1 : (i === focusableIdx ? 0 : -1);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
key={opt.value}
|
|
102
|
+
role="radio"
|
|
103
|
+
aria-checked={String(isChecked)}
|
|
104
|
+
aria-disabled={isItemInactive || undefined}
|
|
105
|
+
tabIndex={tabIdx}
|
|
106
|
+
data-radio-item
|
|
107
|
+
data-value={opt.value}
|
|
108
|
+
data-inactive={isItemInactive || undefined}
|
|
109
|
+
class={{ 'radio-item': true, 'radio-item--inactive': isItemInactive }}
|
|
110
|
+
onClick={() => !isItemInactive && this.selectItem(opt.value)}
|
|
111
|
+
onKeyDown={(e: KeyboardEvent) => {
|
|
112
|
+
if ((e.key === ' ' || e.key === 'Enter') && !isItemInactive) {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
this.selectItem(opt.value);
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<span class={{ circle: true, 'circle--checked': isChecked }}>
|
|
119
|
+
{isChecked && <span class="dot" />}
|
|
120
|
+
</span>
|
|
121
|
+
<span class="text-body-medium radio-label">{opt.label}</span>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</Host>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { RadioOption } from './RadioGroup';
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Component, Prop, State, Element, h, Host } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
export type ScrollbarVariant = 'default' | 'thick';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
tag: 'ds-scrollbar',
|
|
7
|
+
styleUrl: 'Scrollbar.css',
|
|
8
|
+
scoped: true,
|
|
9
|
+
})
|
|
10
|
+
export class Scrollbar {
|
|
11
|
+
@Element() el!: HTMLElement;
|
|
12
|
+
|
|
13
|
+
@Prop() variant: ScrollbarVariant = 'default';
|
|
14
|
+
@Prop() showTrackOnHover: boolean = true;
|
|
15
|
+
|
|
16
|
+
@State() private thumbHeight: number = 0;
|
|
17
|
+
@State() private thumbTop: number = 0;
|
|
18
|
+
@State() private thumbWidth: number = 0;
|
|
19
|
+
@State() private thumbLeft: number = 0;
|
|
20
|
+
@State() private isDragging: boolean = false;
|
|
21
|
+
@State() private isDraggingH: boolean = false;
|
|
22
|
+
@State() private isPointerInside: boolean = false;
|
|
23
|
+
|
|
24
|
+
private contentEl: HTMLElement | null = null;
|
|
25
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
26
|
+
private scrollHandler: (() => void) | null = null;
|
|
27
|
+
|
|
28
|
+
// Drag state (not reactive — no re-render needed)
|
|
29
|
+
private dragStartY = 0;
|
|
30
|
+
private dragStartScrollTop = 0;
|
|
31
|
+
private dragStartX = 0;
|
|
32
|
+
private dragStartScrollLeft = 0;
|
|
33
|
+
|
|
34
|
+
componentDidLoad() {
|
|
35
|
+
this.contentEl = this.el.querySelector('.scrollbar-content') as HTMLElement | null;
|
|
36
|
+
if (!this.contentEl) return;
|
|
37
|
+
|
|
38
|
+
this.scrollHandler = () => this.updateThumb();
|
|
39
|
+
this.contentEl.addEventListener('scroll', this.scrollHandler);
|
|
40
|
+
|
|
41
|
+
this.resizeObserver = new ResizeObserver(() => this.updateThumb());
|
|
42
|
+
this.resizeObserver.observe(this.contentEl);
|
|
43
|
+
window.addEventListener('resize', this.scrollHandler);
|
|
44
|
+
|
|
45
|
+
this.updateThumb();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
disconnectedCallback() {
|
|
49
|
+
if (this.contentEl && this.scrollHandler) {
|
|
50
|
+
this.contentEl.removeEventListener('scroll', this.scrollHandler);
|
|
51
|
+
}
|
|
52
|
+
if (this.scrollHandler) {
|
|
53
|
+
window.removeEventListener('resize', this.scrollHandler);
|
|
54
|
+
}
|
|
55
|
+
this.resizeObserver?.disconnect();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private updateThumb() {
|
|
59
|
+
const container = this.el.querySelector('.scrollbar-container') as HTMLElement | null;
|
|
60
|
+
const content = this.contentEl;
|
|
61
|
+
if (!container || !content) return;
|
|
62
|
+
|
|
63
|
+
const cH = container.clientHeight;
|
|
64
|
+
const cW = container.clientWidth;
|
|
65
|
+
const sH = content.scrollHeight;
|
|
66
|
+
const sW = content.scrollWidth;
|
|
67
|
+
const st = content.scrollTop;
|
|
68
|
+
const sl = content.scrollLeft;
|
|
69
|
+
|
|
70
|
+
if (sH <= cH) {
|
|
71
|
+
this.thumbHeight = 0;
|
|
72
|
+
} else {
|
|
73
|
+
const h = Math.max(20, cH * (cH / sH));
|
|
74
|
+
this.thumbHeight = h;
|
|
75
|
+
this.thumbTop = (st / (sH - cH)) * (cH - h);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sW <= cW) {
|
|
79
|
+
this.thumbWidth = 0;
|
|
80
|
+
} else {
|
|
81
|
+
const w = Math.max(20, cW * (cW / sW));
|
|
82
|
+
this.thumbWidth = w;
|
|
83
|
+
this.thumbLeft = (sl / (sW - cW)) * (cW - w);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handleThumbMouseDown(e: MouseEvent) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
e.stopPropagation();
|
|
90
|
+
const content = this.contentEl;
|
|
91
|
+
if (!content) return;
|
|
92
|
+
|
|
93
|
+
this.isDragging = true;
|
|
94
|
+
this.dragStartY = e.clientY;
|
|
95
|
+
this.dragStartScrollTop = content.scrollTop;
|
|
96
|
+
|
|
97
|
+
const container = this.el.querySelector('.scrollbar-container') as HTMLElement;
|
|
98
|
+
|
|
99
|
+
const onMove = (ev: MouseEvent) => {
|
|
100
|
+
const deltaY = ev.clientY - this.dragStartY;
|
|
101
|
+
const scrollable = content.scrollHeight - container.clientHeight;
|
|
102
|
+
const ratio = scrollable / (container.clientHeight - this.thumbHeight);
|
|
103
|
+
content.scrollTop = Math.max(0, Math.min(scrollable, this.dragStartScrollTop + deltaY * ratio));
|
|
104
|
+
};
|
|
105
|
+
const onUp = () => {
|
|
106
|
+
this.isDragging = false;
|
|
107
|
+
document.removeEventListener('mousemove', onMove);
|
|
108
|
+
document.removeEventListener('mouseup', onUp);
|
|
109
|
+
};
|
|
110
|
+
document.addEventListener('mousemove', onMove);
|
|
111
|
+
document.addEventListener('mouseup', onUp);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private handleThumbHMouseDown(e: MouseEvent) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
e.stopPropagation();
|
|
117
|
+
const content = this.contentEl;
|
|
118
|
+
if (!content) return;
|
|
119
|
+
|
|
120
|
+
this.isDraggingH = true;
|
|
121
|
+
this.dragStartX = e.clientX;
|
|
122
|
+
this.dragStartScrollLeft = content.scrollLeft;
|
|
123
|
+
|
|
124
|
+
const container = this.el.querySelector('.scrollbar-container') as HTMLElement;
|
|
125
|
+
|
|
126
|
+
const onMove = (ev: MouseEvent) => {
|
|
127
|
+
const deltaX = ev.clientX - this.dragStartX;
|
|
128
|
+
const scrollable = content.scrollWidth - container.clientWidth;
|
|
129
|
+
const ratio = scrollable / (container.clientWidth - this.thumbWidth);
|
|
130
|
+
content.scrollLeft = Math.max(0, Math.min(scrollable, this.dragStartScrollLeft + deltaX * ratio));
|
|
131
|
+
};
|
|
132
|
+
const onUp = () => {
|
|
133
|
+
this.isDraggingH = false;
|
|
134
|
+
document.removeEventListener('mousemove', onMove);
|
|
135
|
+
document.removeEventListener('mouseup', onUp);
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener('mousemove', onMove);
|
|
138
|
+
document.addEventListener('mouseup', onUp);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render() {
|
|
142
|
+
const showThumb = this.thumbHeight > 0;
|
|
143
|
+
const showThumbH = this.thumbWidth > 0;
|
|
144
|
+
const isScrollable = showThumb || showThumbH;
|
|
145
|
+
const showTrack = this.showTrackOnHover && (this.isPointerInside || this.isDragging || this.isDraggingH);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Host style={{ display: 'contents' }}>
|
|
149
|
+
<div
|
|
150
|
+
class={{
|
|
151
|
+
'scrollbar-container': true,
|
|
152
|
+
'scrollbar-container--thick': this.variant === 'thick',
|
|
153
|
+
}}
|
|
154
|
+
data-show-track={showTrack ? 'true' : 'false'}
|
|
155
|
+
data-dragging={this.isDragging || this.isDraggingH}
|
|
156
|
+
onMouseEnter={() => { if (this.showTrackOnHover) this.isPointerInside = true; }}
|
|
157
|
+
onMouseLeave={() => { this.isPointerInside = false; }}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
class="scrollbar-content"
|
|
161
|
+
tabIndex={isScrollable ? 0 : undefined}
|
|
162
|
+
>
|
|
163
|
+
<slot />
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{showThumb && (
|
|
167
|
+
<div class="scrollbar-track" aria-hidden="true">
|
|
168
|
+
<div
|
|
169
|
+
class="scrollbar-thumb"
|
|
170
|
+
style={{ height: `${this.thumbHeight}px`, top: `${this.thumbTop}px` }}
|
|
171
|
+
onMouseDown={(e: MouseEvent) => this.handleThumbMouseDown(e)}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{showThumbH && (
|
|
177
|
+
<div class="scrollbar-track-h" aria-hidden="true">
|
|
178
|
+
<div
|
|
179
|
+
class="scrollbar-thumb-h"
|
|
180
|
+
style={{ width: `${this.thumbWidth}px`, left: `${this.thumbLeft}px` }}
|
|
181
|
+
onMouseDown={(e: MouseEvent) => this.handleThumbHMouseDown(e)}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</Host>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';
|
|
2
|
+
import type { MenuItemData } from '../Menu/Menu';
|
|
3
|
+
|
|
4
|
+
export interface SelectOption {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Component({
|
|
10
|
+
tag: 'ds-select',
|
|
11
|
+
styleUrl: 'Select.css',
|
|
12
|
+
scoped: true,
|
|
13
|
+
})
|
|
14
|
+
export class Select {
|
|
15
|
+
@Element() el!: HTMLElement;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Array of options. Set via JS property.
|
|
19
|
+
* @example el.options = [{ label: 'Apple', value: 'apple' }];
|
|
20
|
+
*/
|
|
21
|
+
@Prop() options: SelectOption[] = [];
|
|
22
|
+
|
|
23
|
+
/** Currently selected value. */
|
|
24
|
+
@Prop({ mutable: true }) value: string = '';
|
|
25
|
+
|
|
26
|
+
/** Placeholder shown when no value is selected. */
|
|
27
|
+
@Prop() placeholder: string = 'Select option';
|
|
28
|
+
|
|
29
|
+
/** Disables interaction. */
|
|
30
|
+
@Prop() inactive: boolean = false;
|
|
31
|
+
|
|
32
|
+
@Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
|
|
33
|
+
@Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;
|
|
34
|
+
|
|
35
|
+
/** Emits the selected value string. */
|
|
36
|
+
@Event() dsChange!: EventEmitter<string>;
|
|
37
|
+
|
|
38
|
+
@State() private isOpen: boolean = false;
|
|
39
|
+
|
|
40
|
+
private triggerEl: HTMLButtonElement | null = null;
|
|
41
|
+
private menuEl: HTMLDsMenuElement | null = null;
|
|
42
|
+
|
|
43
|
+
componentDidLoad() {
|
|
44
|
+
// Wire up the menu anchor to the trigger button
|
|
45
|
+
if (this.menuEl && this.triggerEl) {
|
|
46
|
+
this.menuEl.anchor = this.triggerEl;
|
|
47
|
+
}
|
|
48
|
+
// Listen for menu selection events
|
|
49
|
+
this.menuEl?.addEventListener('dsSelect', (e: Event) => {
|
|
50
|
+
const detail = (e as CustomEvent<MenuItemData>).detail;
|
|
51
|
+
if (detail?.value !== undefined) {
|
|
52
|
+
this.value = String(detail.value);
|
|
53
|
+
this.dsChange.emit(this.value);
|
|
54
|
+
}
|
|
55
|
+
this.isOpen = false;
|
|
56
|
+
});
|
|
57
|
+
// Listen for menu close event
|
|
58
|
+
this.menuEl?.addEventListener('dsClose', () => {
|
|
59
|
+
this.isOpen = false;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Watch('isOpen')
|
|
64
|
+
onIsOpenChange(open: boolean) {
|
|
65
|
+
if (this.menuEl) {
|
|
66
|
+
this.menuEl.open = open;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Watch('options')
|
|
71
|
+
onOptionsChange() {
|
|
72
|
+
this.syncMenuItems();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Watch('value')
|
|
76
|
+
onValueChange() {
|
|
77
|
+
this.syncMenuItems();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private syncMenuItems() {
|
|
81
|
+
if (!this.menuEl) return;
|
|
82
|
+
this.menuEl.items = this.options.map(opt => ({
|
|
83
|
+
label: opt.label,
|
|
84
|
+
value: opt.value,
|
|
85
|
+
isSelected: opt.value === this.value,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private open() {
|
|
90
|
+
if (this.inactive || !this.options.length) return;
|
|
91
|
+
// Sync menu width to trigger width
|
|
92
|
+
if (this.menuEl && this.triggerEl) {
|
|
93
|
+
this.menuEl.menuWidth = `${this.triggerEl.offsetWidth}px`;
|
|
94
|
+
this.menuEl.anchor = this.triggerEl;
|
|
95
|
+
}
|
|
96
|
+
this.syncMenuItems();
|
|
97
|
+
this.isOpen = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private close() {
|
|
101
|
+
this.isOpen = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private toggle() {
|
|
105
|
+
if (this.isOpen) {
|
|
106
|
+
this.close();
|
|
107
|
+
} else {
|
|
108
|
+
this.open();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Listen('keydown')
|
|
113
|
+
handleKeyDown(e: KeyboardEvent) {
|
|
114
|
+
// Only handle keydown on the trigger button
|
|
115
|
+
if (document.activeElement !== this.triggerEl) return;
|
|
116
|
+
|
|
117
|
+
if (!this.isOpen) {
|
|
118
|
+
switch (e.key) {
|
|
119
|
+
case 'ArrowDown':
|
|
120
|
+
case 'ArrowUp':
|
|
121
|
+
case 'Enter':
|
|
122
|
+
case ' ':
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
this.open();
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (e.key === 'Escape') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
this.close();
|
|
131
|
+
this.triggerEl?.focus();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private get selectedLabel(): string {
|
|
137
|
+
const found = this.options.find(o => o.value === this.value);
|
|
138
|
+
return found?.label ?? '';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private get hasSelection(): boolean {
|
|
142
|
+
return !!this.options.find(o => o.value === this.value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
render() {
|
|
146
|
+
const label = this.hasSelection ? this.selectedLabel : this.placeholder;
|
|
147
|
+
const showPlaceholder = !this.hasSelection;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Host class={{ 'select-host': true, inactive: this.inactive }}>
|
|
151
|
+
<button
|
|
152
|
+
ref={el => {
|
|
153
|
+
this.triggerEl = (el as HTMLButtonElement) ?? null;
|
|
154
|
+
}}
|
|
155
|
+
type="button"
|
|
156
|
+
class={{ trigger: true, 'trigger--open': this.isOpen }}
|
|
157
|
+
disabled={this.inactive}
|
|
158
|
+
role="combobox"
|
|
159
|
+
aria-haspopup="listbox"
|
|
160
|
+
aria-expanded={this.isOpen}
|
|
161
|
+
aria-label={this.ariaLabel}
|
|
162
|
+
aria-labelledby={this.ariaLabelledby}
|
|
163
|
+
onClick={() => this.toggle()}
|
|
164
|
+
>
|
|
165
|
+
<span class={{ 'trigger__label': true, 'trigger__label--placeholder': showPlaceholder }}>
|
|
166
|
+
{label}
|
|
167
|
+
</span>
|
|
168
|
+
<svg
|
|
169
|
+
class="trigger__chevron"
|
|
170
|
+
width="16"
|
|
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>
|
|
184
|
+
</button>
|
|
185
|
+
<ds-menu
|
|
186
|
+
ref={el => {
|
|
187
|
+
this.menuEl = (el as HTMLDsMenuElement) ?? null;
|
|
188
|
+
}}
|
|
189
|
+
side="bottom"
|
|
190
|
+
align="start"
|
|
191
|
+
/>
|
|
192
|
+
</Host>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|