@nectary/components 5.33.0 → 5.34.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/bundle.d.ts +3 -0
- package/bundle.js +772 -176
- package/bundle.ts +3 -0
- package/floating-panel/global/index.d.ts +1 -0
- package/floating-panel/global/index.js +2 -0
- package/floating-panel/index.d.ts +41 -0
- package/floating-panel/index.js +502 -0
- package/floating-panel/types.d.ts +80 -0
- package/floating-panel/types.js +1 -0
- package/floating-panel-button/index.d.ts +11 -0
- package/floating-panel-button/index.js +43 -0
- package/floating-panel-button/types.d.ts +35 -0
- package/floating-panel-button/types.js +1 -0
- package/floating-panel-icon-button/index.d.ts +12 -0
- package/floating-panel-icon-button/index.js +50 -0
- package/floating-panel-icon-button/types.d.ts +35 -0
- package/floating-panel-icon-button/types.js +1 -0
- package/icon/index.js +30 -4
- package/package.json +3 -3
- package/pop/index.js +1 -1
- package/standalone.d.ts +3 -0
- package/standalone.js +3 -0
- package/standalone.ts +3 -0
- package/tooltip/index.js +4 -4
- package/utils/component-names.d.ts +2 -2
- package/utils/component-names.js +3 -0
- package/utils/element.d.ts +3 -0
package/bundle.ts
CHANGED
|
@@ -28,6 +28,9 @@ export * from './file-drop/index.js'
|
|
|
28
28
|
export * from './file-picker/index.js'
|
|
29
29
|
export * from './file-status/index.js'
|
|
30
30
|
export * from './flag/index.js'
|
|
31
|
+
export * from './floating-panel/index.js'
|
|
32
|
+
export * from './floating-panel-button/index.js'
|
|
33
|
+
export * from './floating-panel-icon-button/index.js'
|
|
31
34
|
export * from './grid-item/index.js'
|
|
32
35
|
export * from './grid/index.js'
|
|
33
36
|
export * from './help-tooltip/index.js'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../types';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NectaryElement } from '../utils';
|
|
2
|
+
import '../action-menu';
|
|
3
|
+
import '../action-menu-option';
|
|
4
|
+
import '../floating-panel-icon-button';
|
|
5
|
+
import '../floating-panel-button';
|
|
6
|
+
import '../icon';
|
|
7
|
+
import '../popover';
|
|
8
|
+
export * from './types';
|
|
9
|
+
export declare class FloatingPanel extends NectaryElement {
|
|
10
|
+
#private;
|
|
11
|
+
constructor();
|
|
12
|
+
connectedCallback(): void;
|
|
13
|
+
disconnectedCallback(): void;
|
|
14
|
+
static get observedAttributes(): string[];
|
|
15
|
+
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null): void;
|
|
16
|
+
get panelRect(): import("../types").TRect;
|
|
17
|
+
get closeButtonRect(): import("../types").TRect;
|
|
18
|
+
get ctaButtonRect(): import("../types").TRect;
|
|
19
|
+
set open(value: boolean);
|
|
20
|
+
get open(): boolean;
|
|
21
|
+
set noOfItemsSelected(value: string | null);
|
|
22
|
+
get noOfItemsSelected(): string | null;
|
|
23
|
+
set ctaLabel(value: string | null);
|
|
24
|
+
get ctaLabel(): string | null;
|
|
25
|
+
set itemsSelectedSingularLabel(value: string | null);
|
|
26
|
+
get itemsSelectedSingularLabel(): string | null;
|
|
27
|
+
set itemsSelectedPluralLabel(value: string | null);
|
|
28
|
+
get itemsSelectedPluralLabel(): string | null;
|
|
29
|
+
set itemsSelectedCompactLabel(value: string | null);
|
|
30
|
+
get itemsSelectedCompactLabel(): string | null;
|
|
31
|
+
set selectAllLabel(value: string | null);
|
|
32
|
+
get selectAllLabel(): string | null;
|
|
33
|
+
set selectAllShortLabel(value: string | null);
|
|
34
|
+
get selectAllShortLabel(): string | null;
|
|
35
|
+
set clearAllLabel(value: string | null);
|
|
36
|
+
get clearAllLabel(): string | null;
|
|
37
|
+
set allSelected(value: boolean);
|
|
38
|
+
get allSelected(): boolean;
|
|
39
|
+
set actions(value: string | null);
|
|
40
|
+
get actions(): string | null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { isSinchActionMenuOption } from "../action-menu-option/utils.js";
|
|
2
|
+
import { isAttrEqual, getBooleanAttribute, getAttribute, shouldReduceMotion, updateBooleanAttribute, updateAttribute } from "../utils/dom.js";
|
|
3
|
+
import { defineCustomElement, NectaryElement } from "../utils/element.js";
|
|
4
|
+
import { getRect } from "../utils/rect.js";
|
|
5
|
+
import { throttleAnimationFrame } from "../utils/throttle.js";
|
|
6
|
+
import { getReactEventHandler } from "../utils/get-react-event-handler.js";
|
|
7
|
+
import "../action-menu/index.js";
|
|
8
|
+
import "../action-menu-option/index.js";
|
|
9
|
+
import "../floating-panel-icon-button/index.js";
|
|
10
|
+
import "../floating-panel-button/index.js";
|
|
11
|
+
import "../icon/index.js";
|
|
12
|
+
import "../popover/index.js";
|
|
13
|
+
const templateHTML = '<style>:host{display:block;max-width:100%;min-width:0}@keyframes panel-slide-in{from{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes panel-slide-out{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}dialog{display:flex;align-items:center;box-sizing:border-box;height:40px;padding:0;overflow:visible;width:fit-content;max-width:100%;min-width:0;background-color:var(--sinch-comp-floating-panel-color-background,var(--sinch-sys-color-primary-default));border:none;border-radius:var(--sinch-comp-floating-panel-shape-radius);box-shadow:var(--sinch-comp-floating-panel-shadow);color:var(--sinch-comp-floating-panel-color-text,var(--sinch-sys-color-primary-foreground))}dialog.closing,dialog.opening{overflow:hidden}dialog:not([open]){display:none}dialog.opening{animation:panel-slide-in .2s ease-out forwards}dialog.closing{animation:panel-slide-out .2s ease-in forwards}#items-selected{display:flex;align-items:baseline;gap:10px;padding:0 8px;white-space:nowrap}#cta{display:flex}#no-of-items-selected{font:var(--sinch-comp-floating-panel-font-bold)}#items-selected-label{font:var(--sinch-comp-floating-panel-font-regular)}#left-actions{display:flex;align-items:center}#divider{width:1px;align-self:stretch;background-color:var(--sinch-comp-floating-panel-color-divider)}#actions-end{display:flex;align-items:center;flex-shrink:1;min-width:0}#right-actions{display:flex;align-items:center}#right-actions-overflow{display:none;align-items:center;flex-shrink:0}:host([data-density=compact]) #right-actions,:host([data-density=extra-compact]) #right-actions{display:none}:host([data-density=compact]) #right-actions-overflow,:host([data-density=extra-compact]) #right-actions-overflow{display:flex}#actions-popover{display:block;--sinch-comp-pop-z-index:2147483000}#overflow-trigger{cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}#overflow-menu-surface{box-sizing:border-box;display:block;min-width:var(--sinch-comp-floating-panel-overflow-menu-min-width,220px);width:max-content;max-width:min(var(--sinch-comp-floating-panel-overflow-menu-max-width,288px),calc(100vw - 24px))}#overflow-menu-surface sinch-action-menu{display:block;width:100%}#right-actions sinch-floating-panel-icon-button:last-child:active,#right-actions sinch-floating-panel-icon-button:last-child:focus,#right-actions sinch-floating-panel-icon-button:last-child:hover{--sinch-button-shape-radius-top-right:var(--sinch-comp-floating-panel-shape-radius);--sinch-button-shape-radius-bottom-right:var(--sinch-comp-floating-panel-shape-radius)}@media (prefers-reduced-motion){dialog.closing,dialog.opening{animation:none}}</style><dialog id="wrapper" role="toolbar" aria-label="Bulk actions"><div id="left-actions"><sinch-floating-panel-icon-button id="close-btn" icon="fa-xmark" aria-label="Close selection" class="edge-start"></sinch-floating-panel-icon-button></div><div id="divider" role="separator" aria-orientation="vertical"></div><div id="items-selected" aria-live="polite" aria-atomic="true"><span id="no-of-items-selected"></span> <span id="items-selected-label"></span></div><div id="cta"><sinch-floating-panel-button id="cta-btn" text=""></sinch-floating-panel-button></div><div id="actions-end"><div id="right-actions" role="group" aria-label="Actions"></div><div id="right-actions-overflow"><sinch-popover id="actions-popover" orientation="bottom-right" allow-scroll aria-label="More actions"><sinch-floating-panel-icon-button slot="target" id="overflow-trigger" icon="fa-ellipsis" aria-label="More actions" class="edge-end"></sinch-floating-panel-icon-button><div id="overflow-menu-surface" slot="content"><sinch-action-menu id="overflow-menu" aria-label="Actions" rows="5"></sinch-action-menu></div></sinch-popover></div></div></dialog>';
|
|
14
|
+
const template = document.createElement("template");
|
|
15
|
+
template.innerHTML = templateHTML;
|
|
16
|
+
const MAX_ACTIONS = 5;
|
|
17
|
+
const WIDTH_COMPACT = 400;
|
|
18
|
+
const WIDTH_EXTRA_COMPACT = 320;
|
|
19
|
+
const DEFAULT_ITEMS_SELECTED_SINGULAR_LABEL = "item selected";
|
|
20
|
+
const DEFAULT_ITEMS_SELECTED_PLURAL_LABEL = "items selected";
|
|
21
|
+
const DEFAULT_ITEMS_SELECTED_COMPACT_LABEL = "selected";
|
|
22
|
+
const DEFAULT_SELECT_ALL_LABEL = "Select all {count} items";
|
|
23
|
+
const DEFAULT_SELECT_ALL_SHORT_LABEL = "Select all";
|
|
24
|
+
const DEFAULT_CLEAR_ALL_LABEL = "Clear all";
|
|
25
|
+
const applyCountPlaceholder = (template2, count) => {
|
|
26
|
+
if (count === null || count === "") {
|
|
27
|
+
return template2.replace(/\{count\} ?/g, "").replace(/ {2,}/g, " ").trim();
|
|
28
|
+
}
|
|
29
|
+
return template2.replaceAll("{count}", count);
|
|
30
|
+
};
|
|
31
|
+
const sanitizeAttr = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
32
|
+
const isValidAction = (value) => {
|
|
33
|
+
if (typeof value !== "object" || value === null) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { name, icon, label } = value;
|
|
37
|
+
return typeof name === "string" && name !== "" && typeof icon === "string" && icon !== "" && (label === void 0 || typeof label === "string");
|
|
38
|
+
};
|
|
39
|
+
const DEFAULT_ACTIONS = [
|
|
40
|
+
{ name: "export", icon: "fa-file-export", label: "Export" },
|
|
41
|
+
{ name: "clone", icon: "fa-clone", label: "Duplicate" },
|
|
42
|
+
{ name: "delete", icon: "fa-trash", label: "Delete" }
|
|
43
|
+
];
|
|
44
|
+
class FloatingPanel extends NectaryElement {
|
|
45
|
+
#$dialog;
|
|
46
|
+
#$noOfItemsSelected;
|
|
47
|
+
#$itemsSelectedLabel;
|
|
48
|
+
#$rightActions;
|
|
49
|
+
#$rightActionsOverflow;
|
|
50
|
+
#$overflowMenu;
|
|
51
|
+
#$actionsPopover;
|
|
52
|
+
#$overflowTrigger;
|
|
53
|
+
#$closeBtn;
|
|
54
|
+
#$ctaBtn;
|
|
55
|
+
#controller = null;
|
|
56
|
+
#hideController = null;
|
|
57
|
+
#resizeObserver = null;
|
|
58
|
+
#resizeThrottle = null;
|
|
59
|
+
#overflowDismissController = null;
|
|
60
|
+
#density = "default";
|
|
61
|
+
/** After touch `pointerup` toggles the menu, ignore the synthetic `click` (WebKit). */
|
|
62
|
+
#suppressOverflowClickUntil = 0;
|
|
63
|
+
constructor() {
|
|
64
|
+
super();
|
|
65
|
+
const shadowRoot = this.attachShadow();
|
|
66
|
+
shadowRoot.appendChild(template.content.cloneNode(true));
|
|
67
|
+
this.#$dialog = shadowRoot.querySelector("#wrapper");
|
|
68
|
+
this.#$noOfItemsSelected = shadowRoot.querySelector("#no-of-items-selected");
|
|
69
|
+
this.#$itemsSelectedLabel = shadowRoot.querySelector("#items-selected-label");
|
|
70
|
+
this.#$rightActions = shadowRoot.querySelector("#right-actions");
|
|
71
|
+
this.#$rightActionsOverflow = shadowRoot.querySelector("#right-actions-overflow");
|
|
72
|
+
this.#$overflowMenu = shadowRoot.querySelector("#overflow-menu");
|
|
73
|
+
this.#$actionsPopover = shadowRoot.querySelector("#actions-popover");
|
|
74
|
+
this.#$overflowTrigger = shadowRoot.querySelector("#overflow-trigger");
|
|
75
|
+
this.#$closeBtn = shadowRoot.querySelector("#close-btn");
|
|
76
|
+
this.#$ctaBtn = shadowRoot.querySelector("#cta-btn");
|
|
77
|
+
}
|
|
78
|
+
connectedCallback() {
|
|
79
|
+
super.connectedCallback();
|
|
80
|
+
this.setAttribute("role", "region");
|
|
81
|
+
this.setAttribute("aria-label", "Bulk actions toolbar");
|
|
82
|
+
this.#controller = new AbortController();
|
|
83
|
+
const { signal } = this.#controller;
|
|
84
|
+
this.#$closeBtn.addEventListener("click", this.#onClose, { signal });
|
|
85
|
+
this.#$ctaBtn.addEventListener("click", this.#onSelectAll, { signal });
|
|
86
|
+
this.#$rightActions.addEventListener("click", this.#onActionClick, { signal });
|
|
87
|
+
this.#$overflowMenu.addEventListener("-click", this.#onOverflowMenuClick, { signal, capture: true });
|
|
88
|
+
this.shadowRoot.addEventListener("click", this.#onToolbarShadowClickCapture, { capture: true, signal });
|
|
89
|
+
this.shadowRoot.addEventListener("pointerup", this.#onOverflowTriggerPointerUp, { signal });
|
|
90
|
+
this.#$actionsPopover.addEventListener("-close", this.#onActionsPopoverClose, { signal });
|
|
91
|
+
this.#resizeThrottle = throttleAnimationFrame(() => {
|
|
92
|
+
if (!this.isDomConnected || !this.#$dialog.open) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const width = this.#$dialog.getBoundingClientRect().width;
|
|
96
|
+
this.#updateDensityFromWidth(width);
|
|
97
|
+
});
|
|
98
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
99
|
+
this.#resizeThrottle?.fn();
|
|
100
|
+
});
|
|
101
|
+
this.#resizeObserver.observe(this.#$dialog);
|
|
102
|
+
this.addEventListener("-close", this.#onCloseReact, { signal });
|
|
103
|
+
this.addEventListener("-select-all", this.#onSelectAllReact, { signal });
|
|
104
|
+
this.addEventListener("-action", this.#onActionReact, { signal });
|
|
105
|
+
if (!this.hasAttribute("actions")) {
|
|
106
|
+
this.#renderActions(null);
|
|
107
|
+
}
|
|
108
|
+
if (this.hasAttribute("open")) {
|
|
109
|
+
this.#show();
|
|
110
|
+
}
|
|
111
|
+
this.#syncDensityAndLabel();
|
|
112
|
+
}
|
|
113
|
+
disconnectedCallback() {
|
|
114
|
+
super.disconnectedCallback();
|
|
115
|
+
this.#hideController?.abort();
|
|
116
|
+
this.#hideController = null;
|
|
117
|
+
this.#overflowDismissController?.abort();
|
|
118
|
+
this.#overflowDismissController = null;
|
|
119
|
+
this.#resizeThrottle?.cancel();
|
|
120
|
+
this.#resizeThrottle = null;
|
|
121
|
+
this.#resizeObserver?.disconnect();
|
|
122
|
+
this.#resizeObserver = null;
|
|
123
|
+
this.#controller.abort();
|
|
124
|
+
this.#controller = null;
|
|
125
|
+
}
|
|
126
|
+
static get observedAttributes() {
|
|
127
|
+
return [
|
|
128
|
+
"open",
|
|
129
|
+
"no-of-items-selected",
|
|
130
|
+
"cta-label",
|
|
131
|
+
"items-selected-singular-label",
|
|
132
|
+
"items-selected-plural-label",
|
|
133
|
+
"items-selected-compact-label",
|
|
134
|
+
"select-all-label",
|
|
135
|
+
"select-all-short-label",
|
|
136
|
+
"clear-all-label",
|
|
137
|
+
"actions",
|
|
138
|
+
"all-selected"
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
142
|
+
if (isAttrEqual(oldVal, newVal)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
switch (name) {
|
|
146
|
+
case "open": {
|
|
147
|
+
if (newVal !== null) {
|
|
148
|
+
this.#show();
|
|
149
|
+
} else {
|
|
150
|
+
this.#hide();
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case "no-of-items-selected": {
|
|
155
|
+
this.#$noOfItemsSelected.textContent = newVal;
|
|
156
|
+
this.#updateItemsSelectedLabel();
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "cta-label":
|
|
160
|
+
case "select-all-label":
|
|
161
|
+
case "select-all-short-label":
|
|
162
|
+
case "clear-all-label": {
|
|
163
|
+
this.#updateCtaText(getBooleanAttribute(this, "all-selected"));
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "items-selected-singular-label":
|
|
167
|
+
case "items-selected-plural-label":
|
|
168
|
+
case "items-selected-compact-label": {
|
|
169
|
+
this.#updateItemsSelectedLabel();
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "actions": {
|
|
173
|
+
this.#renderActions(newVal);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case "all-selected": {
|
|
177
|
+
this.#updateCtaText(newVal !== null);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
#updateItemsSelectedLabel() {
|
|
183
|
+
if (this.#density === "extra-compact") {
|
|
184
|
+
this.#$itemsSelectedLabel.textContent = getAttribute(this, "items-selected-compact-label") ?? DEFAULT_ITEMS_SELECTED_COMPACT_LABEL;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const raw = getAttribute(this, "no-of-items-selected");
|
|
188
|
+
const n = raw === null || raw === "" ? NaN : Number(raw);
|
|
189
|
+
const singular = Number.isFinite(n) && n === 1;
|
|
190
|
+
this.#$itemsSelectedLabel.textContent = singular ? getAttribute(this, "items-selected-singular-label") ?? DEFAULT_ITEMS_SELECTED_SINGULAR_LABEL : getAttribute(this, "items-selected-plural-label") ?? DEFAULT_ITEMS_SELECTED_PLURAL_LABEL;
|
|
191
|
+
}
|
|
192
|
+
#updateDensityFromWidth(width) {
|
|
193
|
+
if (!this.#$dialog.open || width <= 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let next;
|
|
197
|
+
if (width <= WIDTH_EXTRA_COMPACT) {
|
|
198
|
+
next = "extra-compact";
|
|
199
|
+
} else if (width <= WIDTH_COMPACT) {
|
|
200
|
+
next = "compact";
|
|
201
|
+
} else {
|
|
202
|
+
next = "default";
|
|
203
|
+
}
|
|
204
|
+
if (next === this.#density) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.#density = next;
|
|
208
|
+
if (next === "default") {
|
|
209
|
+
this.removeAttribute("data-density");
|
|
210
|
+
this.#closeOverflowMenu();
|
|
211
|
+
} else {
|
|
212
|
+
this.setAttribute("data-density", next);
|
|
213
|
+
}
|
|
214
|
+
this.#syncDensityAndLabel();
|
|
215
|
+
}
|
|
216
|
+
#syncDensityAndLabel() {
|
|
217
|
+
this.#updateItemsSelectedLabel();
|
|
218
|
+
this.#updateCtaText(getBooleanAttribute(this, "all-selected"));
|
|
219
|
+
}
|
|
220
|
+
#show() {
|
|
221
|
+
this.#hideController?.abort();
|
|
222
|
+
this.#hideController = null;
|
|
223
|
+
this.#$dialog.show();
|
|
224
|
+
this.#$dialog.classList.remove("closing");
|
|
225
|
+
if (shouldReduceMotion()) {
|
|
226
|
+
queueMicrotask(() => {
|
|
227
|
+
const { width } = this.#$dialog.getBoundingClientRect();
|
|
228
|
+
this.#updateDensityFromWidth(width);
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
this.#$dialog.classList.add("opening");
|
|
233
|
+
this.#$dialog.addEventListener("animationend", () => {
|
|
234
|
+
this.#$dialog.classList.remove("opening");
|
|
235
|
+
}, { once: true });
|
|
236
|
+
queueMicrotask(() => {
|
|
237
|
+
const { width } = this.#$dialog.getBoundingClientRect();
|
|
238
|
+
this.#updateDensityFromWidth(width);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
#hide() {
|
|
242
|
+
this.#closeOverflowMenu();
|
|
243
|
+
this.#hideController?.abort();
|
|
244
|
+
if (shouldReduceMotion()) {
|
|
245
|
+
this.#$dialog.close();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.#hideController = new AbortController();
|
|
249
|
+
this.#$dialog.classList.remove("opening");
|
|
250
|
+
this.#$dialog.classList.add("closing");
|
|
251
|
+
this.#$dialog.addEventListener("animationend", () => {
|
|
252
|
+
this.#hideController = null;
|
|
253
|
+
this.#$dialog.classList.remove("closing");
|
|
254
|
+
this.#$dialog.close();
|
|
255
|
+
}, { once: true, signal: this.#hideController.signal });
|
|
256
|
+
}
|
|
257
|
+
#updateCtaText(allSelected) {
|
|
258
|
+
if (allSelected) {
|
|
259
|
+
this.#$ctaBtn.setAttribute("text", getAttribute(this, "clear-all-label") ?? DEFAULT_CLEAR_ALL_LABEL);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (this.#density !== "default") {
|
|
263
|
+
this.#$ctaBtn.setAttribute("text", getAttribute(this, "select-all-short-label") ?? DEFAULT_SELECT_ALL_SHORT_LABEL);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const template2 = getAttribute(this, "select-all-label") ?? DEFAULT_SELECT_ALL_LABEL;
|
|
267
|
+
this.#$ctaBtn.setAttribute("text", applyCountPlaceholder(template2, getAttribute(this, "cta-label")));
|
|
268
|
+
}
|
|
269
|
+
#renderActions(json) {
|
|
270
|
+
this.#$rightActions.innerHTML = "";
|
|
271
|
+
this.#$overflowMenu.innerHTML = "";
|
|
272
|
+
let actions;
|
|
273
|
+
if (json === null) {
|
|
274
|
+
actions = DEFAULT_ACTIONS;
|
|
275
|
+
} else {
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(json);
|
|
278
|
+
if (!Array.isArray(parsed)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
actions = parsed.filter(isValidAction);
|
|
282
|
+
} catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const sliced = actions.slice(0, MAX_ACTIONS);
|
|
287
|
+
this.#$rightActions.innerHTML = sliced.map((action) => {
|
|
288
|
+
const safeName = sanitizeAttr(action.name);
|
|
289
|
+
const safeIcon = sanitizeAttr(action.icon);
|
|
290
|
+
const safeLabel = sanitizeAttr(action.label ?? action.name);
|
|
291
|
+
return `<sinch-floating-panel-icon-button data-action="${safeName}" icon="${safeIcon}" aria-label="${safeLabel}"></sinch-floating-panel-icon-button>`;
|
|
292
|
+
}).join("");
|
|
293
|
+
this.#$overflowMenu.innerHTML = sliced.map((action) => {
|
|
294
|
+
const safeName = sanitizeAttr(action.name);
|
|
295
|
+
const safeIcon = sanitizeAttr(action.icon);
|
|
296
|
+
const safeLabel = sanitizeAttr(action.label ?? action.name);
|
|
297
|
+
return `<sinch-action-menu-option data-action="${safeName}" text="${safeLabel}" aria-label="${safeLabel}"><sinch-icon icons-version="2" name="${safeIcon}" slot="icon"></sinch-icon></sinch-action-menu-option>`;
|
|
298
|
+
}).join("");
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Fires the generic `-action` event (detail = action name) plus a per-action
|
|
302
|
+
* `-${action}` convenience event. The per-action event lets consumers bind
|
|
303
|
+
* directly to a specific action (e.g. `on-archive`) without switching on
|
|
304
|
+
* `e.detail` — and it works for any user-defined action, not just the
|
|
305
|
+
* built-in defaults. React handlers are forwarded by `#onActionReact`.
|
|
306
|
+
*/
|
|
307
|
+
#dispatchAction(action) {
|
|
308
|
+
this.dispatchEvent(new CustomEvent("-action", { detail: action }));
|
|
309
|
+
this.dispatchEvent(new CustomEvent(`-${action}`));
|
|
310
|
+
}
|
|
311
|
+
#onClose = () => {
|
|
312
|
+
this.dispatchEvent(new CustomEvent("-close"));
|
|
313
|
+
};
|
|
314
|
+
#onSelectAll = () => {
|
|
315
|
+
this.dispatchEvent(new CustomEvent("-select-all"));
|
|
316
|
+
};
|
|
317
|
+
#onActionClick = (e) => {
|
|
318
|
+
const btn = e.target.closest("sinch-floating-panel-icon-button[data-action]");
|
|
319
|
+
if (btn === null) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const action = btn.dataset.action;
|
|
323
|
+
if (action === void 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.#dispatchAction(action);
|
|
327
|
+
};
|
|
328
|
+
#onOverflowMenuClick = (e) => {
|
|
329
|
+
if (!isSinchActionMenuOption(e.target)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const action = e.target.dataset.action;
|
|
333
|
+
if (action === void 0) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.#closeOverflowMenu();
|
|
337
|
+
this.#dispatchAction(action);
|
|
338
|
+
};
|
|
339
|
+
/**
|
|
340
|
+
* Safari/WebKit often does not deliver a bubbling click from sinch-button (shadow) to the overflow host;
|
|
341
|
+
* capture on our shadow + composedPath() matches Chrome and Safari. Touch: pointerup (see below).
|
|
342
|
+
*/
|
|
343
|
+
#onToolbarShadowClickCapture = (e) => {
|
|
344
|
+
if (this.#density === "default") {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (!(e instanceof MouseEvent)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (e.button !== 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!e.composedPath().includes(this.#$overflowTrigger)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (performance.now() < this.#suppressOverflowClickUntil) {
|
|
357
|
+
e.preventDefault();
|
|
358
|
+
e.stopPropagation();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
e.stopPropagation();
|
|
362
|
+
this.#toggleOverflowMenu();
|
|
363
|
+
};
|
|
364
|
+
#onOverflowTriggerPointerUp = (e) => {
|
|
365
|
+
if (this.#density === "default") {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!(e instanceof PointerEvent)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!e.isPrimary || e.pointerType === "mouse" && e.button !== 0) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (e.pointerType === "mouse") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!e.composedPath().includes(this.#$overflowTrigger)) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
e.stopPropagation();
|
|
382
|
+
this.#suppressOverflowClickUntil = performance.now() + 400;
|
|
383
|
+
this.#toggleOverflowMenu();
|
|
384
|
+
};
|
|
385
|
+
#toggleOverflowMenu() {
|
|
386
|
+
if (this.#$actionsPopover.open) {
|
|
387
|
+
this.#closeOverflowMenu();
|
|
388
|
+
} else {
|
|
389
|
+
this.#$actionsPopover.open = true;
|
|
390
|
+
this.#overflowDismissController = new AbortController();
|
|
391
|
+
document.addEventListener("click", this.#onDocumentClickDismiss, {
|
|
392
|
+
signal: this.#overflowDismissController.signal,
|
|
393
|
+
capture: true
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
#closeOverflowMenu() {
|
|
398
|
+
this.#$actionsPopover.open = false;
|
|
399
|
+
this.#overflowDismissController?.abort();
|
|
400
|
+
this.#overflowDismissController = null;
|
|
401
|
+
}
|
|
402
|
+
#onDocumentClickDismiss = (e) => {
|
|
403
|
+
if (e.composedPath().includes(this.#$rightActionsOverflow)) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.#closeOverflowMenu();
|
|
407
|
+
};
|
|
408
|
+
#onActionsPopoverClose = () => {
|
|
409
|
+
this.#closeOverflowMenu();
|
|
410
|
+
};
|
|
411
|
+
#onCloseReact = (e) => {
|
|
412
|
+
getReactEventHandler(this, "on-close")?.(e);
|
|
413
|
+
};
|
|
414
|
+
#onSelectAllReact = (e) => {
|
|
415
|
+
getReactEventHandler(this, "on-select-all")?.(e);
|
|
416
|
+
};
|
|
417
|
+
#onActionReact = (e) => {
|
|
418
|
+
getReactEventHandler(this, "on-action")?.(e);
|
|
419
|
+
if (e instanceof CustomEvent && typeof e.detail === "string") {
|
|
420
|
+
getReactEventHandler(this, `on-${e.detail}`)?.(e);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
get panelRect() {
|
|
424
|
+
return getRect(this.#$dialog);
|
|
425
|
+
}
|
|
426
|
+
get closeButtonRect() {
|
|
427
|
+
return getRect(this.#$closeBtn);
|
|
428
|
+
}
|
|
429
|
+
get ctaButtonRect() {
|
|
430
|
+
return getRect(this.#$ctaBtn);
|
|
431
|
+
}
|
|
432
|
+
set open(value) {
|
|
433
|
+
updateBooleanAttribute(this, "open", value);
|
|
434
|
+
}
|
|
435
|
+
get open() {
|
|
436
|
+
return getBooleanAttribute(this, "open");
|
|
437
|
+
}
|
|
438
|
+
set noOfItemsSelected(value) {
|
|
439
|
+
updateAttribute(this, "no-of-items-selected", value);
|
|
440
|
+
}
|
|
441
|
+
get noOfItemsSelected() {
|
|
442
|
+
return getAttribute(this, "no-of-items-selected");
|
|
443
|
+
}
|
|
444
|
+
set ctaLabel(value) {
|
|
445
|
+
updateAttribute(this, "cta-label", value);
|
|
446
|
+
}
|
|
447
|
+
get ctaLabel() {
|
|
448
|
+
return getAttribute(this, "cta-label");
|
|
449
|
+
}
|
|
450
|
+
set itemsSelectedSingularLabel(value) {
|
|
451
|
+
updateAttribute(this, "items-selected-singular-label", value);
|
|
452
|
+
}
|
|
453
|
+
get itemsSelectedSingularLabel() {
|
|
454
|
+
return getAttribute(this, "items-selected-singular-label");
|
|
455
|
+
}
|
|
456
|
+
set itemsSelectedPluralLabel(value) {
|
|
457
|
+
updateAttribute(this, "items-selected-plural-label", value);
|
|
458
|
+
}
|
|
459
|
+
get itemsSelectedPluralLabel() {
|
|
460
|
+
return getAttribute(this, "items-selected-plural-label");
|
|
461
|
+
}
|
|
462
|
+
set itemsSelectedCompactLabel(value) {
|
|
463
|
+
updateAttribute(this, "items-selected-compact-label", value);
|
|
464
|
+
}
|
|
465
|
+
get itemsSelectedCompactLabel() {
|
|
466
|
+
return getAttribute(this, "items-selected-compact-label");
|
|
467
|
+
}
|
|
468
|
+
set selectAllLabel(value) {
|
|
469
|
+
updateAttribute(this, "select-all-label", value);
|
|
470
|
+
}
|
|
471
|
+
get selectAllLabel() {
|
|
472
|
+
return getAttribute(this, "select-all-label");
|
|
473
|
+
}
|
|
474
|
+
set selectAllShortLabel(value) {
|
|
475
|
+
updateAttribute(this, "select-all-short-label", value);
|
|
476
|
+
}
|
|
477
|
+
get selectAllShortLabel() {
|
|
478
|
+
return getAttribute(this, "select-all-short-label");
|
|
479
|
+
}
|
|
480
|
+
set clearAllLabel(value) {
|
|
481
|
+
updateAttribute(this, "clear-all-label", value);
|
|
482
|
+
}
|
|
483
|
+
get clearAllLabel() {
|
|
484
|
+
return getAttribute(this, "clear-all-label");
|
|
485
|
+
}
|
|
486
|
+
set allSelected(value) {
|
|
487
|
+
updateBooleanAttribute(this, "all-selected", value);
|
|
488
|
+
}
|
|
489
|
+
get allSelected() {
|
|
490
|
+
return getBooleanAttribute(this, "all-selected");
|
|
491
|
+
}
|
|
492
|
+
set actions(value) {
|
|
493
|
+
updateAttribute(this, "actions", value);
|
|
494
|
+
}
|
|
495
|
+
get actions() {
|
|
496
|
+
return getAttribute(this, "actions");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
defineCustomElement("sinch-floating-panel", FloatingPanel);
|
|
500
|
+
export {
|
|
501
|
+
FloatingPanel
|
|
502
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { NectaryComponentReactByType, NectaryComponentVanillaByType, TRect, NectaryComponentReact, NectaryComponentVanilla } from '../types';
|
|
2
|
+
export type TSinchFloatingPanelAction = {
|
|
3
|
+
name: string;
|
|
4
|
+
icon: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
export type TSinchFloatingPanelProps = {
|
|
8
|
+
/** Whether the panel is visible */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** Number of items currently selected */
|
|
11
|
+
'no-of-items-selected'?: string;
|
|
12
|
+
/** Value substituted for `{count}` in `select-all-label` (e.g. total item count or noun). */
|
|
13
|
+
'cta-label'?: string;
|
|
14
|
+
/** Suffix after the count when exactly one item is selected. Default: `item selected`. */
|
|
15
|
+
'items-selected-singular-label'?: string;
|
|
16
|
+
/** Suffix after the count when zero or many items are selected. Default: `items selected`. */
|
|
17
|
+
'items-selected-plural-label'?: string;
|
|
18
|
+
/** Suffix shown at extra-compact density, regardless of count. Default: `selected`. */
|
|
19
|
+
'items-selected-compact-label'?: string;
|
|
20
|
+
/** CTA text at default density. `{count}` is replaced with `cta-label` (or removed when empty). Default: `Select all {count} items`. */
|
|
21
|
+
'select-all-label'?: string;
|
|
22
|
+
/** CTA text at compact / extra-compact density. Default: `Select all`. */
|
|
23
|
+
'select-all-short-label'?: string;
|
|
24
|
+
/** CTA text when `all-selected` is true. Default: `Clear all`. */
|
|
25
|
+
'clear-all-label'?: string;
|
|
26
|
+
/** JSON array of action buttons: [{name, icon}] — min 1, max 5 */
|
|
27
|
+
actions?: string;
|
|
28
|
+
/** When true, CTA label switches to the `clear-all-label` text. */
|
|
29
|
+
'all-selected'?: boolean;
|
|
30
|
+
readonly panelRect?: TRect;
|
|
31
|
+
readonly closeButtonRect?: TRect;
|
|
32
|
+
readonly ctaButtonRect?: TRect;
|
|
33
|
+
};
|
|
34
|
+
export type TSinchFloatingPanelEvents = {
|
|
35
|
+
'-close'?: (e: CustomEvent<void>) => void;
|
|
36
|
+
'-select-all'?: (e: CustomEvent<void>) => void;
|
|
37
|
+
/** Fires for every action click; `detail` is the action name. */
|
|
38
|
+
'-action'?: (e: CustomEvent<string>) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Per-action convenience events fire as `-${action.name}` for any configured
|
|
41
|
+
* action (custom or built-in). The defaults below are listed for autocomplete;
|
|
42
|
+
* custom actions can be typed via module augmentation.
|
|
43
|
+
*/
|
|
44
|
+
'-delete'?: (e: CustomEvent<void>) => void;
|
|
45
|
+
'-clone'?: (e: CustomEvent<void>) => void;
|
|
46
|
+
'-export'?: (e: CustomEvent<void>) => void;
|
|
47
|
+
};
|
|
48
|
+
export type TSinchFloatingPanelStyle = {
|
|
49
|
+
'--sinch-comp-floating-panel-color-background'?: string;
|
|
50
|
+
'--sinch-comp-floating-panel-color-text'?: string;
|
|
51
|
+
'--sinch-comp-floating-panel-overflow-menu-min-width'?: string;
|
|
52
|
+
'--sinch-comp-floating-panel-overflow-menu-max-width'?: string;
|
|
53
|
+
};
|
|
54
|
+
export type TSinchFloatingPanel = {
|
|
55
|
+
props: TSinchFloatingPanelProps;
|
|
56
|
+
events: TSinchFloatingPanelEvents;
|
|
57
|
+
style: TSinchFloatingPanelStyle;
|
|
58
|
+
};
|
|
59
|
+
export type TSinchFloatingPanelElement = NectaryComponentVanillaByType<TSinchFloatingPanel>;
|
|
60
|
+
export type TSinchFloatingPanelReact = NectaryComponentReactByType<TSinchFloatingPanel>;
|
|
61
|
+
declare global {
|
|
62
|
+
interface NectaryComponentMap {
|
|
63
|
+
'sinch-floating-panel': TSinchFloatingPanel;
|
|
64
|
+
}
|
|
65
|
+
interface HTMLElementTagNameMap {
|
|
66
|
+
'sinch-floating-panel': NectaryComponentVanilla<'sinch-floating-panel'>;
|
|
67
|
+
}
|
|
68
|
+
namespace JSX {
|
|
69
|
+
interface IntrinsicElements {
|
|
70
|
+
'sinch-floating-panel': NectaryComponentReact<'sinch-floating-panel'>;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
declare module 'react' {
|
|
75
|
+
namespace JSX {
|
|
76
|
+
interface IntrinsicElements extends globalThis.JSX.IntrinsicElements {
|
|
77
|
+
'sinch-floating-panel': NectaryComponentReact<'sinch-floating-panel'>;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NectaryElement } from '../utils';
|
|
2
|
+
import '../button';
|
|
3
|
+
export * from './types';
|
|
4
|
+
export declare class FloatingPanelButton extends NectaryElement {
|
|
5
|
+
#private;
|
|
6
|
+
constructor();
|
|
7
|
+
static get observedAttributes(): string[];
|
|
8
|
+
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null): void;
|
|
9
|
+
set text(value: string | null);
|
|
10
|
+
get text(): string | null;
|
|
11
|
+
}
|