@kubex/zinc 1.1.101 → 1.1.103
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/custom-elements.json +261 -82
- package/dist/vscode.html-custom-data.json +13 -13
- package/dist/web-types.json +25 -25
- package/dist/zn.d.ts +128 -6
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +469 -393
- package/docs/pages/components/remarkd-editor.md +59 -0
- package/package.json +1 -1
- package/scss/_root.scss +121 -8
- package/scss/shared/_base.scss +10 -4
- package/scss/shared/_button-mixin.scss +29 -0
- package/scss/shared/_button.scss +3 -33
- package/scss/themes/_aura.scss +87 -8
- package/scss/themes/_dark.scss +14 -2
- package/scss/themes/_light.scss +4 -3
- package/scss/themes/_modes.scss +32 -0
- package/src/components/animated-button/animated-button.component.ts +2 -1
- package/src/components/animated-button/animated-button.scss +1 -1
- package/src/components/bulk-actions/bulk-actions.component.ts +2 -1
- package/src/components/bulk-actions/bulk-actions.scss +0 -1
- package/src/components/button/button.component.ts +6 -1
- package/src/components/button/button.scss +1 -1
- package/src/components/chart/chart.component.ts +4 -0
- package/src/components/checkbox/checkbox.component.ts +3 -1
- package/src/components/checkbox/checkbox.scss +0 -2
- package/src/components/checkbox-group/checkbox-group.component.ts +2 -1
- package/src/components/checkbox-group/checkbox-group.scss +0 -1
- package/src/components/data-table/data-table.component.ts +3 -1
- package/src/components/data-table/data-table.scss +0 -1
- package/src/components/datepicker/datepicker.component.ts +2 -1
- package/src/components/datepicker/datepicker.scss +0 -1
- package/src/components/editor/modules/toolbar/toolbar.component.ts +3 -1
- package/src/components/editor/modules/toolbar/toolbar.scss +0 -1
- package/src/components/file/file.component.ts +2 -1
- package/src/components/file/file.scss +0 -1
- package/src/components/form-group/form-group.component.ts +2 -1
- package/src/components/form-group/form-group.scss +0 -1
- package/src/components/icon-picker/icon-picker.component.ts +8 -3
- package/src/components/icon-picker/icon-picker.scss +0 -1
- package/src/components/inline-edit/inline-edit.component.ts +2 -1
- package/src/components/inline-edit/inline-edit.scss +0 -1
- package/src/components/input/input.component.ts +2 -1
- package/src/components/input/input.scss +0 -1
- package/src/components/input-group/input-group.component.ts +2 -1
- package/src/components/input-group/input-group.scss +0 -1
- package/src/components/menu/menu.component.ts +4 -1
- package/src/components/menu/menu.scss +9 -1
- package/src/components/page/page.scss +19 -5
- package/src/components/priority-list/priority-list.component.ts +2 -1
- package/src/components/priority-list/priority-list.scss +0 -1
- package/src/components/query-builder/query-builder.component.ts +2 -1
- package/src/components/query-builder/query-builder.scss +0 -1
- package/src/components/radio/radio.component.ts +3 -1
- package/src/components/radio/radio.scss +0 -2
- package/src/components/radio-group/radio-group.component.ts +2 -1
- package/src/components/radio-group/radio-group.scss +0 -1
- package/src/components/rating/rating.component.ts +2 -1
- package/src/components/rating/rating.scss +0 -1
- package/src/components/remarkd-editor/actions.ts +155 -0
- package/src/components/remarkd-editor/feature-keys.ts +17 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +509 -49
- package/src/components/remarkd-editor/remarkd-editor.scss +68 -1
- package/src/components/remarkd-editor/remarkd-editor.test.ts +677 -9
- package/src/components/schedule-builder/schedule-builder.component.ts +2 -1
- package/src/components/schedule-builder/schedule-builder.scss +0 -1
- package/src/components/select/select.component.ts +2 -1
- package/src/components/select/select.scss +0 -1
- package/src/components/slash-menu/slash-menu.scss +2 -0
- package/src/components/tabs/tabs.component.ts +3 -1
- package/src/components/tabs/tabs.scss +0 -1
- package/src/components/textarea/textarea.component.ts +2 -1
- package/src/components/textarea/textarea.scss +0 -1
- package/src/components/tile/tile.component.ts +33 -20
- package/src/components/tile/tile.scss +2 -0
- package/src/components/tile/tile.test.ts +64 -3
- package/src/components/toggle/toggle.component.ts +2 -1
- package/src/components/toggle/toggle.scss +0 -1
- package/src/components/translations/translations.component.ts +2 -1
- package/src/components/translations/translations.scss +0 -1
- package/src/internal/theme.ts +37 -12
- package/src/internal/toolbar-overflow.ts +100 -0
- package/src/internal/zinc-element.ts +7 -5
|
@@ -14,6 +14,7 @@ import ZnOption from '../option';
|
|
|
14
14
|
import ZnSelect from '../select';
|
|
15
15
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
16
16
|
|
|
17
|
+
import formControlStyles from '../../form-control.scss';
|
|
17
18
|
import styles from './schedule-builder.scss';
|
|
18
19
|
|
|
19
20
|
/** The seven weekday keys used throughout the schedule. */
|
|
@@ -533,7 +534,7 @@ type SlotState = 'closed' | 'open' | 'reduced';
|
|
|
533
534
|
* @cssproperty --reduced-color - The fill used for hours an exception removes.
|
|
534
535
|
*/
|
|
535
536
|
export default class ZnScheduleBuilder extends ZincElement implements ZincFormControl {
|
|
536
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
537
|
+
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
537
538
|
|
|
538
539
|
static formAssociated = true;
|
|
539
540
|
|
|
@@ -21,6 +21,7 @@ import type {CSSResultGroup, PropertyValues, TemplateResult} from 'lit';
|
|
|
21
21
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
22
22
|
import type {ZnRemoveEvent} from "../../events/zn-remove";
|
|
23
23
|
|
|
24
|
+
import formControlStyles from '../../form-control.scss';
|
|
24
25
|
import styles from './select.scss';
|
|
25
26
|
|
|
26
27
|
/**
|
|
@@ -77,7 +78,7 @@ import styles from './select.scss';
|
|
|
77
78
|
* @csspart expand-icon - The container that wraps the expand icon.
|
|
78
79
|
*/
|
|
79
80
|
export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
80
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
81
|
+
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
81
82
|
static dependencies = {
|
|
82
83
|
'zn-icon': ZnIcon,
|
|
83
84
|
'zn-opt-group': ZnOptGroup,
|
|
@@ -17,6 +17,8 @@ import {property} from 'lit/decorators.js';
|
|
|
17
17
|
import {Store} from "../../internal/storage";
|
|
18
18
|
import ZincElement from '../../internal/zinc-element';
|
|
19
19
|
|
|
20
|
+
// Tabs load in such a way that the global sheet has to be re-applied inside the shadow root.
|
|
21
|
+
import bootStyles from '../../../scss/boot.scss';
|
|
20
22
|
import styles from './tabs.scss';
|
|
21
23
|
|
|
22
24
|
// Every element that manages its own tabs (ZnTabs and its subclasses). Tab
|
|
@@ -42,7 +44,7 @@ const tabContainerSelector = 'zn-tabs, zn-page, zn-page-nav';
|
|
|
42
44
|
* @cssproperty --example - An example CSS custom property.
|
|
43
45
|
*/
|
|
44
46
|
export default class ZnTabs extends ZincElement {
|
|
45
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
47
|
+
static styles: CSSResultGroup = [unsafeCSS(bootStyles), unsafeCSS(styles)];
|
|
46
48
|
@property({attribute: 'master-id', reflect: true}) masterId: string;
|
|
47
49
|
@property({attribute: 'default-uri', reflect: true}) defaultUri = '';
|
|
48
50
|
@property({attribute: 'active', reflect: true}) _current = '';
|
|
@@ -14,6 +14,7 @@ import ZincElement, {type ZincFormControl} from '../../internal/zinc-element';
|
|
|
14
14
|
import ZnSlashItem from "../slash-item";
|
|
15
15
|
import ZnSlashMenu from "../slash-menu";
|
|
16
16
|
|
|
17
|
+
import formControlStyles from '../../form-control.scss';
|
|
17
18
|
import styles from './textarea.scss';
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -52,7 +53,7 @@ import styles from './textarea.scss';
|
|
|
52
53
|
* @csspart slash-menu - The slash menu shown at the caret.
|
|
53
54
|
*/
|
|
54
55
|
export default class ZnTextarea extends ZincElement implements ZincFormControl {
|
|
55
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
56
|
+
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
56
57
|
static dependencies = {
|
|
57
58
|
'zn-slash-item': ZnSlashItem,
|
|
58
59
|
'zn-slash-menu': ZnSlashMenu
|
|
@@ -61,10 +61,21 @@ export default class ZnTile extends ZincElement {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
private _handleActionsClick(e: MouseEvent) {
|
|
64
|
-
if (this._isLink())
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
if (!this._isLink()) return;
|
|
65
|
+
|
|
66
|
+
// Let Rubix's delegated pagelet handler see an action's own link. For controls without
|
|
67
|
+
// navigation metadata, stop before the handler can walk up to the tile host's href.
|
|
68
|
+
for (const target of e.composedPath()) {
|
|
69
|
+
if (target === this) break;
|
|
70
|
+
if (!(target instanceof Element)) continue;
|
|
71
|
+
|
|
72
|
+
const href = target.getAttribute('href');
|
|
73
|
+
if (target.hasAttribute('data-uri') || (href !== null && href !== '' && !href.startsWith('#'))) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
67
76
|
}
|
|
77
|
+
|
|
78
|
+
e.stopPropagation();
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
render() {
|
|
@@ -77,11 +88,7 @@ export default class ZnTile extends ZincElement {
|
|
|
77
88
|
const hasImage = this.hasSlotController.test('image');
|
|
78
89
|
|
|
79
90
|
return html`
|
|
80
|
-
|
|
81
|
-
href="${ifDefined(this.href)}"
|
|
82
|
-
data-uri="${ifDefined(this.dataUri)}"
|
|
83
|
-
gaid="${ifDefined(this.gaid)}"
|
|
84
|
-
data-target="${ifDefined(this.dataTarget)}"
|
|
91
|
+
<div
|
|
85
92
|
class="${classMap({
|
|
86
93
|
tile: true,
|
|
87
94
|
'tile--flush': this.flush,
|
|
@@ -96,11 +103,15 @@ export default class ZnTile extends ZincElement {
|
|
|
96
103
|
'tile--has-actions': hasActions,
|
|
97
104
|
'tile--has-image': hasImage,
|
|
98
105
|
})}">
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
<${tag}
|
|
107
|
+
href="${ifDefined(this.href)}"
|
|
108
|
+
data-uri="${ifDefined(this.dataUri)}"
|
|
109
|
+
gaid="${ifDefined(this.gaid)}"
|
|
110
|
+
data-target="${ifDefined(this.dataTarget)}"
|
|
111
|
+
class="tile__link">
|
|
112
|
+
${!hasCaption && !hasDescription && !hasProperties && !hasActions ? html`
|
|
113
|
+
<slot></slot>
|
|
114
|
+
` : html`
|
|
104
115
|
<div class="tile__left">
|
|
105
116
|
${hasImage ? html`
|
|
106
117
|
<slot name="image" part="image" class="tile__image"></slot>` : html``}
|
|
@@ -113,15 +124,17 @@ export default class ZnTile extends ZincElement {
|
|
|
113
124
|
${this.description}</p>` : html`<slot name="description" class="tile__description"></slot>`}
|
|
114
125
|
</div>
|
|
115
126
|
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div class="tile__right">
|
|
119
127
|
${hasProperties ? html`
|
|
120
128
|
<slot name="properties" part="properties" class="tile__properties"></slot>` : ''}
|
|
121
|
-
|
|
129
|
+
`}
|
|
130
|
+
</${tag}>
|
|
131
|
+
|
|
132
|
+
${hasActions ? html`
|
|
133
|
+
<div class="tile__right">
|
|
134
|
+
<!-- Kept outside the link so slotted controls retain their default behaviour. -->
|
|
122
135
|
<slot name="actions" part="actions" class="tile__actions"
|
|
123
|
-
@click=${this._handleActionsClick}></slot
|
|
124
|
-
</div>`}
|
|
125
|
-
|
|
136
|
+
@click=${this._handleActionsClick}></slot>
|
|
137
|
+
</div>` : ''}
|
|
138
|
+
</div>`;
|
|
126
139
|
}
|
|
127
140
|
}
|
|
@@ -1,10 +1,71 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
+
import type ZnTile from './tile.component';
|
|
3
4
|
|
|
4
|
-
describe('<zn-
|
|
5
|
+
describe('<zn-tile>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
6
|
-
const el = await fixture(html
|
|
7
|
+
const el = await fixture(html`<zn-tile></zn-tile>`);
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
it('should render its content as a link when href is set', async () => {
|
|
13
|
+
const el = await fixture<ZnTile>(html`
|
|
14
|
+
<zn-tile href="/users/leslie" caption="Leslie Alexander"></zn-tile>`);
|
|
15
|
+
|
|
16
|
+
const link = el.shadowRoot!.querySelector<HTMLAnchorElement>('a.tile__link')!;
|
|
17
|
+
expect(link).to.exist;
|
|
18
|
+
expect(link.getAttribute('href')).to.equal('/users/leslie');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should render actions outside the link', async () => {
|
|
22
|
+
const el = await fixture<ZnTile>(html`
|
|
23
|
+
<zn-tile href="/users/leslie" caption="Leslie Alexander">
|
|
24
|
+
<zn-button slot="actions">Edit</zn-button>
|
|
25
|
+
</zn-tile>`);
|
|
26
|
+
|
|
27
|
+
const link = el.shadowRoot!.querySelector<HTMLAnchorElement>('a.tile__link')!;
|
|
28
|
+
const actions = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="actions"]')!;
|
|
29
|
+
expect(link.contains(actions)).to.be.false;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should not cancel clicks from a slotted action', async () => {
|
|
33
|
+
const el = await fixture<ZnTile>(html`
|
|
34
|
+
<zn-tile href="/users/leslie" caption="Leslie Alexander">
|
|
35
|
+
<zn-button slot="actions">Edit</zn-button>
|
|
36
|
+
</zn-tile>`);
|
|
37
|
+
|
|
38
|
+
const action = el.querySelector<HTMLElement>('zn-button')!;
|
|
39
|
+
const button = action.shadowRoot!.querySelector<HTMLButtonElement>('button')!;
|
|
40
|
+
let click: MouseEvent | undefined;
|
|
41
|
+
action.addEventListener('click', event => (click = event as MouseEvent));
|
|
42
|
+
|
|
43
|
+
button.click();
|
|
44
|
+
|
|
45
|
+
expect(click).to.exist;
|
|
46
|
+
expect(click!.defaultPrevented).to.be.false;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should let a linked action reach delegated navigation handlers', async () => {
|
|
50
|
+
const el = await fixture<ZnTile>(html`
|
|
51
|
+
<zn-tile href="/users/leslie" caption="Leslie Alexander">
|
|
52
|
+
<zn-button slot="actions" href="/users/leslie/edit" data-target="modal">Edit</zn-button>
|
|
53
|
+
</zn-tile>`);
|
|
54
|
+
|
|
55
|
+
const action = el.querySelector<HTMLElement>('zn-button')!;
|
|
56
|
+
const buttonLink = action.shadowRoot!.querySelector<HTMLAnchorElement>('a')!;
|
|
57
|
+
let selectedLink: Element | undefined;
|
|
58
|
+
const handleDocumentClick = (event: MouseEvent) => {
|
|
59
|
+
selectedLink = event.composedPath().find(target =>
|
|
60
|
+
target instanceof Element && target.matches('[href]')) as Element | undefined;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
};
|
|
63
|
+
document.addEventListener('click', handleDocumentClick, {once: true});
|
|
64
|
+
|
|
65
|
+
buttonLink.click();
|
|
66
|
+
|
|
67
|
+
expect(selectedLink).to.equal(buttonLink);
|
|
68
|
+
expect(selectedLink!.getAttribute('href')).to.equal('/users/leslie/edit');
|
|
69
|
+
expect(selectedLink!.getAttribute('data-target')).to.equal('modal');
|
|
70
|
+
});
|
|
10
71
|
});
|
|
@@ -8,6 +8,7 @@ import { live } from "lit/directives/live.js";
|
|
|
8
8
|
import { property, query, state } from 'lit/decorators.js';
|
|
9
9
|
import ZincElement, { type ZincFormControl } from '../../internal/zinc-element';
|
|
10
10
|
|
|
11
|
+
import formControlStyles from '../../form-control.scss';
|
|
11
12
|
import styles from './toggle.scss';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -32,7 +33,7 @@ import styles from './toggle.scss';
|
|
|
32
33
|
* @cssproperty --zn-toggle-margin - The margin around the toggle switch. Defaults to `8px 0`.
|
|
33
34
|
*/
|
|
34
35
|
export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
35
|
-
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
36
|
+
static styles: CSSResultGroup = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
36
37
|
|
|
37
38
|
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'description');
|
|
38
39
|
|
|
@@ -19,10 +19,11 @@ import type {PropertyValues} from 'lit';
|
|
|
19
19
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
20
20
|
import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
|
|
21
21
|
|
|
22
|
+
import formControlStyles from '../../form-control.scss';
|
|
22
23
|
import styles from './translations.scss';
|
|
23
24
|
|
|
24
25
|
export default class ZnTranslations extends ZincElement implements ZincFormControl {
|
|
25
|
-
static styles = unsafeCSS(styles);
|
|
26
|
+
static styles = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
|
|
26
27
|
static dependencies = {
|
|
27
28
|
'zn-button': ZnButton,
|
|
28
29
|
'zn-button-group': ZnButtonGroup,
|
package/src/internal/theme.ts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import {signal, SignalWatcher} from '@lit-labs/signals';
|
|
2
2
|
|
|
3
|
+
// Theming has two independent dimensions:
|
|
4
|
+
// `t` — the theme family (base, aura, ...), carrying the identity/palette.
|
|
5
|
+
// `m` — the rendered mode ('light' | 'dark') the family is currently drawn in.
|
|
6
|
+
// The host shell owns both attributes on <html>; components mirror them so CSS
|
|
7
|
+
// selectors like `[m="dark"]` and `:host-context([t="aura"])` keep working.
|
|
8
|
+
|
|
9
|
+
function readRootAttribute(name: string, fallback: string): string {
|
|
10
|
+
if (typeof document === 'undefined') return fallback;
|
|
11
|
+
return document.documentElement.getAttribute(name)
|
|
12
|
+
|| document.body?.getAttribute(name)
|
|
13
|
+
|| fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
function getDefaultTheme(): string {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
17
|
+
return readRootAttribute('t', 'light');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDefaultMode(): string {
|
|
21
|
+
return readRootAttribute('m', 'light');
|
|
8
22
|
}
|
|
9
23
|
|
|
10
24
|
export const themeSignal = signal<string>(getDefaultTheme());
|
|
25
|
+
export const modeSignal = signal<string>(getDefaultMode());
|
|
11
26
|
|
|
12
27
|
let listenerInstalled = false;
|
|
13
28
|
|
|
@@ -17,18 +32,28 @@ export function installThemeListener(): void {
|
|
|
17
32
|
|
|
18
33
|
window.addEventListener('theme-change', (e: Event) => {
|
|
19
34
|
const detail: unknown = (e as CustomEvent<unknown>).detail;
|
|
20
|
-
let
|
|
35
|
+
let theme: string | undefined;
|
|
36
|
+
let mode: string | undefined;
|
|
37
|
+
|
|
21
38
|
if (typeof detail === 'string') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
// Legacy payload: the theme name on its own.
|
|
40
|
+
theme = detail;
|
|
41
|
+
} else if (detail && typeof detail === 'object') {
|
|
42
|
+
const d = detail as {theme?: unknown; mode?: unknown};
|
|
43
|
+
if (typeof d.theme === 'string') theme = d.theme;
|
|
44
|
+
if (typeof d.mode === 'string') mode = d.mode;
|
|
27
45
|
}
|
|
28
|
-
|
|
46
|
+
|
|
47
|
+
// Anything the event did not carry is read back off the document, which the
|
|
48
|
+
// shell always writes before dispatching.
|
|
49
|
+
if (theme === undefined) theme = getDefaultTheme();
|
|
50
|
+
if (mode === undefined) mode = getDefaultMode();
|
|
51
|
+
|
|
52
|
+
if (theme !== themeSignal.get()) themeSignal.set(theme);
|
|
53
|
+
if (mode !== modeSignal.get()) modeSignal.set(mode);
|
|
29
54
|
});
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
installThemeListener();
|
|
33
58
|
|
|
34
|
-
export {SignalWatcher};
|
|
59
|
+
export {SignalWatcher};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
2
|
+
import type {ReactiveController, ReactiveControllerHost} from 'lit';
|
|
3
|
+
|
|
4
|
+
interface ToolbarOverflowOptions {
|
|
5
|
+
/** The measurable group elements, in toolbar order. */
|
|
6
|
+
groups: () => HTMLElement[];
|
|
7
|
+
/** The element whose width the groups have to fit inside. */
|
|
8
|
+
container: () => HTMLElement | null | undefined;
|
|
9
|
+
/** Space to keep for the overflow trigger. Defaults to 44px. */
|
|
10
|
+
reserve?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reports how many leading toolbar groups fit the container, so the host can render the
|
|
15
|
+
* rest into an overflow menu. Two passes: the first ignores the trigger, because when
|
|
16
|
+
* everything fits there is no trigger to make room for.
|
|
17
|
+
*/
|
|
18
|
+
export class ToolbarOverflowController implements ReactiveController {
|
|
19
|
+
visibleCount = Infinity;
|
|
20
|
+
|
|
21
|
+
private readonly host: ReactiveControllerHost & Element;
|
|
22
|
+
private readonly options: ToolbarOverflowOptions;
|
|
23
|
+
private resizeRafId = 0;
|
|
24
|
+
|
|
25
|
+
constructor(host: ReactiveControllerHost & Element, options: ToolbarOverflowOptions) {
|
|
26
|
+
this.host = host;
|
|
27
|
+
this.options = options;
|
|
28
|
+
host.addController(this);
|
|
29
|
+
// Deferred to a rAF rather than measuring inline: measure() mutates the display of
|
|
30
|
+
// elements inside the observed host, and doing that synchronously in the observer's own
|
|
31
|
+
// callback is what trips "ResizeObserver loop completed with undelivered notifications"
|
|
32
|
+
// (fatal in WebKit's test runner, not just a console warning). Same fix as button-menu.
|
|
33
|
+
// eslint-disable-next-line no-new -- ResizeController registers itself with the host.
|
|
34
|
+
new ResizeController(host, {
|
|
35
|
+
callback: () => {
|
|
36
|
+
if (this.resizeRafId) return;
|
|
37
|
+
this.resizeRafId = requestAnimationFrame(() => {
|
|
38
|
+
this.resizeRafId = 0;
|
|
39
|
+
this.measure();
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
hostUpdated() {
|
|
46
|
+
this.measure();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private measure() {
|
|
50
|
+
const groups = this.options.groups();
|
|
51
|
+
const container = this.options.container();
|
|
52
|
+
if (!groups.length || !container) return;
|
|
53
|
+
|
|
54
|
+
// clientWidth includes the container's own padding, so it has to come back out — and so
|
|
55
|
+
// does any non-group sibling (the raw-source toggle, or the trigger itself once it
|
|
56
|
+
// exists) that is really sitting in the same row and eating into the same space. Without
|
|
57
|
+
// this, `total <= available` can read true while a sibling's real footprint still clips
|
|
58
|
+
// trailing groups off-screen with no trigger rendered to reach them.
|
|
59
|
+
const style = getComputedStyle(container);
|
|
60
|
+
const paddingX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
|
|
61
|
+
const siblingsWidth = [...container.children]
|
|
62
|
+
.filter(child => !child.classList.contains('toolbar__group'))
|
|
63
|
+
.reduce((sum, child) => sum + child.getBoundingClientRect().width, 0);
|
|
64
|
+
const available = container.clientWidth - paddingX - siblingsWidth;
|
|
65
|
+
if (available <= 0) return;
|
|
66
|
+
|
|
67
|
+
// Show everything before measuring: a collapsed group measures 0, which would let the
|
|
68
|
+
// next pass re-expand it and oscillate forever. Same reason the editor toolbar resets
|
|
69
|
+
// display before each pass. Reads and writes stay in this frame, so nothing flickers.
|
|
70
|
+
groups.forEach(group => (group.style.display = ''));
|
|
71
|
+
const widths = groups.map(group => group.getBoundingClientRect().width);
|
|
72
|
+
const total = widths.reduce((sum, width) => sum + width, 0);
|
|
73
|
+
|
|
74
|
+
const next = total <= available
|
|
75
|
+
? groups.length
|
|
76
|
+
: this.countThatFit(widths, available - (this.options.reserve ?? 44));
|
|
77
|
+
|
|
78
|
+
groups.forEach((group, index) => (group.style.display = index < next ? '' : 'none'));
|
|
79
|
+
|
|
80
|
+
if (next !== this.visibleCount) {
|
|
81
|
+
this.visibleCount = next;
|
|
82
|
+
this.host.requestUpdate();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private countThatFit(widths: number[], available: number): number {
|
|
87
|
+
let used = 0;
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const width of widths) {
|
|
90
|
+
if (used + width > available) break;
|
|
91
|
+
used += width;
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
// No `Math.max(count, 1)` floor here: forcing at least one group into the bar when even
|
|
95
|
+
// that group alone doesn't fit `available` pushed the trigger past the host's own
|
|
96
|
+
// `overflow: hidden` edge — clipped and unreachable, not just visually tight. Zero visible
|
|
97
|
+
// groups is the correct answer in that case; every action still reaches the menu.
|
|
98
|
+
return count;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {LitElement, type PropertyValues} from "lit";
|
|
2
2
|
import {property} from "lit/decorators.js";
|
|
3
|
-
import {SignalWatcher, themeSignal} from "./theme";
|
|
3
|
+
import {modeSignal, SignalWatcher, themeSignal} from "./theme";
|
|
4
4
|
import type {
|
|
5
5
|
EventTypeDoesNotRequireDetail,
|
|
6
6
|
EventTypeRequiresDetail,
|
|
@@ -19,13 +19,15 @@ const ZincElementBase: typeof LitElement & Constructor<SignalWatcherApi> = Signa
|
|
|
19
19
|
export default class ZincElement extends ZincElementBase {
|
|
20
20
|
@property() dir: string; // LTR or RTL direction
|
|
21
21
|
@property() lang: string; // Language
|
|
22
|
-
@property({reflect: true}) t: string; // Theme (
|
|
22
|
+
@property({reflect: true}) t: string; // Theme family (base, aura, ...)
|
|
23
|
+
@property({reflect: true}) m: string; // Rendered mode (light or dark)
|
|
23
24
|
|
|
24
25
|
protected override willUpdate(changed: PropertyValues): void {
|
|
25
|
-
// Reading
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// Reading these signals here subscribes this component to theme changes via
|
|
27
|
+
// the SignalWatcher mixin. The reflected attributes keep CSS selectors like
|
|
28
|
+
// `[m="dark"]` and `:host-context([t="aura"])` working inside shadow roots.
|
|
28
29
|
this.t = themeSignal.get();
|
|
30
|
+
this.m = modeSignal.get();
|
|
29
31
|
super.willUpdate(changed);
|
|
30
32
|
}
|
|
31
33
|
|