@nyaruka/temba-components 0.167.0 → 0.168.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/CHANGELOG.md +21 -0
- package/dist/locales/es.js +3 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +3 -1
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +3 -1
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +2791 -1668
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/Icons.ts +2 -0
- package/src/display/Button.ts +8 -6
- package/src/display/Chat.ts +541 -191
- package/src/display/Dropdown.ts +40 -17
- package/src/display/Label.ts +25 -4
- package/src/display/TembaDate.ts +11 -2
- package/src/display/Tip.ts +157 -18
- package/src/events/eventRenderers.ts +678 -268
- package/src/events.ts +16 -1
- package/src/flow/NodeEditor.ts +82 -9
- package/src/flow/actions/add_contact_urn.ts +1 -0
- package/src/flow/actions/play_audio.ts +1 -0
- package/src/flow/actions/send_broadcast.ts +17 -1
- package/src/flow/actions/send_msg.ts +12 -1
- package/src/flow/actions/set_run_result.ts +1 -0
- package/src/flow/actions/start_session.ts +10 -1
- package/src/flow/nodes/shared-rules.ts +2 -0
- package/src/flow/nodes/split_by_airtime.ts +6 -0
- package/src/flow/nodes/split_by_expression.ts +1 -0
- package/src/flow/nodes/split_by_llm.ts +2 -0
- package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
- package/src/flow/nodes/split_by_random.ts +2 -1
- package/src/flow/nodes/split_by_resthook.ts +4 -1
- package/src/flow/nodes/split_by_webhook.ts +9 -1
- package/src/flow/types.ts +3 -0
- package/src/flow/utils.ts +49 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/layout/Card.ts +9 -0
- package/src/layout/CardLayout.ts +426 -21
- package/src/layout/CardStack.ts +4 -1
- package/src/list/ContactList.ts +22 -17
- package/src/list/ContentList.ts +632 -29
- package/src/list/MsgList.ts +4 -2
- package/src/list/SortableList.ts +5 -1
- package/src/list/TembaMenu.ts +67 -6
- package/src/live/ContactChat.ts +655 -336
- package/src/live/ContactDetails.ts +961 -88
- package/src/live/ContactFieldEditor.ts +46 -11
- package/src/live/ContactFields.ts +57 -16
- package/src/live/ContactStoreElement.ts +15 -8
- package/src/locales/es.ts +3 -2
- package/src/locales/fr.ts +3 -2
- package/src/locales/pt.ts +3 -2
- package/src/simulator/Simulator.ts +28 -0
- package/src/styles/pillVariants.ts +13 -1
- package/web-test-runner.config.mjs +2 -0
- package/xliff/es.xlf +6 -3
- package/xliff/fr.xlf +6 -3
- package/xliff/pt.xlf +6 -3
package/src/display/Dropdown.ts
CHANGED
|
@@ -70,6 +70,10 @@ export class Dropdown extends RapidElement {
|
|
|
70
70
|
transform: translateY(0.5em) scale(1);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
.open .dropdown.up {
|
|
74
|
+
transform: translateY(-0.5em) scale(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
.mask {
|
|
74
78
|
position: absolute;
|
|
75
79
|
left: 0;
|
|
@@ -109,10 +113,22 @@ export class Dropdown extends RapidElement {
|
|
|
109
113
|
@property({ type: Boolean })
|
|
110
114
|
mask = false;
|
|
111
115
|
|
|
116
|
+
// prefer opening above the toggle, falling back to below if there's no room
|
|
117
|
+
@property({ type: Boolean })
|
|
118
|
+
up = false;
|
|
119
|
+
|
|
120
|
+
// optional selector for the element inside the toggle slot to position
|
|
121
|
+
// against, e.g. the visible button rather than its padded wrapper
|
|
122
|
+
@property({ type: String })
|
|
123
|
+
anchor = '';
|
|
124
|
+
|
|
112
125
|
dropdownStyle: Record<string, string> = {};
|
|
113
126
|
|
|
114
127
|
arrowStyle: Record<string, string> = {};
|
|
115
128
|
|
|
129
|
+
// whether the last position calculation opened us above the toggle
|
|
130
|
+
private openedUp = false;
|
|
131
|
+
|
|
116
132
|
constructor() {
|
|
117
133
|
super();
|
|
118
134
|
this.calculatePosition = this.calculatePosition.bind(this);
|
|
@@ -150,6 +166,7 @@ export class Dropdown extends RapidElement {
|
|
|
150
166
|
// reset position so calculatePosition() sees the natural bounds
|
|
151
167
|
this.dropdownStyle = {};
|
|
152
168
|
this.arrowStyle = {};
|
|
169
|
+
this.openedUp = this.up;
|
|
153
170
|
|
|
154
171
|
this.open = true;
|
|
155
172
|
this.dormant = false;
|
|
@@ -175,6 +192,12 @@ export class Dropdown extends RapidElement {
|
|
|
175
192
|
this.blur();
|
|
176
193
|
}
|
|
177
194
|
|
|
195
|
+
public close() {
|
|
196
|
+
if (this.open) {
|
|
197
|
+
this.closeDropdown();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
178
201
|
public updated(changedProperties: Map<string, any>) {
|
|
179
202
|
super.updated(changedProperties);
|
|
180
203
|
|
|
@@ -191,19 +214,14 @@ export class Dropdown extends RapidElement {
|
|
|
191
214
|
) as HTMLDivElement;
|
|
192
215
|
const toggle = this.querySelector('*[slot="toggle"]');
|
|
193
216
|
|
|
194
|
-
const arrow = this.shadowRoot.querySelector('.arrow') as HTMLDivElement;
|
|
195
|
-
|
|
196
217
|
let bumpedUp = false;
|
|
197
218
|
let bumpedLeft = false;
|
|
198
219
|
|
|
199
220
|
if (dropdown && toggle) {
|
|
221
|
+
const anchor =
|
|
222
|
+
(this.anchor && toggle.querySelector(this.anchor)) || toggle;
|
|
200
223
|
const dropdownBounds = dropdown.getBoundingClientRect();
|
|
201
|
-
const toggleBounds =
|
|
202
|
-
const arrowBounds = arrow.getBoundingClientRect();
|
|
203
|
-
|
|
204
|
-
if (!toggle) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
224
|
+
const toggleBounds = anchor.getBoundingClientRect();
|
|
207
225
|
|
|
208
226
|
// Anchor the dropdown to the toggle's viewport coordinates.
|
|
209
227
|
// The dropdown is `position: fixed`, so viewport coords are
|
|
@@ -223,20 +241,24 @@ export class Dropdown extends RapidElement {
|
|
|
223
241
|
if (toggleBounds.left + dropdownBounds.width > window.innerWidth) {
|
|
224
242
|
dropdownStyle['left'] =
|
|
225
243
|
toggleBounds.right - dropdownBounds.width + 'px';
|
|
226
|
-
delete dropdownStyle['right'];
|
|
227
244
|
bumpedLeft = true;
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
// if off
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
// if preferred up and there's room, or we'd fall off the bottom, bump
|
|
248
|
+
// it up, leaving room for the arrow to point at the top of the toggle
|
|
249
|
+
if (
|
|
250
|
+
(this.up &&
|
|
251
|
+
toggleBounds.top > dropdownBounds.height + this.arrowSize) ||
|
|
252
|
+
toggleBounds.bottom + dropdownBounds.height > window.innerHeight
|
|
253
|
+
) {
|
|
254
|
+
dropdownStyle['top'] =
|
|
255
|
+
toggleBounds.top - dropdownBounds.height - this.arrowSize + 'px';
|
|
234
256
|
bumpedUp = true;
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
// if our arrow is aligned with the left of the dropdown, scootch
|
|
238
260
|
// the dropdown left a pinch so our arrow still overlaps properly
|
|
239
|
-
let arrowLeft = toggleBounds.width / 2 -
|
|
261
|
+
let arrowLeft = toggleBounds.width / 2 - this.arrowSize;
|
|
240
262
|
if (arrowLeft <= 0) {
|
|
241
263
|
dropdownStyle['marginLeft'] = '-10px';
|
|
242
264
|
arrowLeft = 10;
|
|
@@ -272,13 +294,13 @@ export class Dropdown extends RapidElement {
|
|
|
272
294
|
}
|
|
273
295
|
|
|
274
296
|
if (bumpedLeft) {
|
|
275
|
-
arrowStyle['right'] =
|
|
276
|
-
toggleBounds.width / 2 - arrowBounds.width / 2 + 'px';
|
|
297
|
+
arrowStyle['right'] = toggleBounds.width / 2 - this.arrowSize + 'px';
|
|
277
298
|
delete arrowStyle['left'];
|
|
278
299
|
}
|
|
279
300
|
|
|
280
301
|
this.arrowStyle = arrowStyle;
|
|
281
302
|
this.dropdownStyle = dropdownStyle;
|
|
303
|
+
this.openedUp = bumpedUp;
|
|
282
304
|
}
|
|
283
305
|
this.requestUpdate();
|
|
284
306
|
}
|
|
@@ -310,7 +332,8 @@ export class Dropdown extends RapidElement {
|
|
|
310
332
|
<div
|
|
311
333
|
class="${getClasses({
|
|
312
334
|
dropdown: true,
|
|
313
|
-
dormant: this.dormant
|
|
335
|
+
dormant: this.dormant,
|
|
336
|
+
up: this.openedUp
|
|
314
337
|
})}"
|
|
315
338
|
style=${styleMap(this.dropdownStyle)}
|
|
316
339
|
tabindex="0"
|
package/src/display/Label.ts
CHANGED
|
@@ -22,6 +22,12 @@ export default class Label extends LitElement {
|
|
|
22
22
|
rather than overflowing. The slot/mask below have the
|
|
23
23
|
min-width:0 needed to let the ellipsis engage. */
|
|
24
24
|
max-width: 100%;
|
|
25
|
+
/* As a flex item the host's automatic minimum size is its
|
|
26
|
+
full nowrap text width, which refuses to shrink and
|
|
27
|
+
stretches content-sized ancestors (e.g. the Next Up card)
|
|
28
|
+
instead of ellipsizing. min-width:0 lets the pill shrink
|
|
29
|
+
to its container so the slot ellipsis can engage. */
|
|
30
|
+
min-width: 0;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
slot {
|
|
@@ -29,10 +35,13 @@ export default class Label extends LitElement {
|
|
|
29
35
|
overflow-x: hidden;
|
|
30
36
|
text-overflow: ellipsis;
|
|
31
37
|
display: block;
|
|
32
|
-
/*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
/* min-width:0 lets the slot — as a flex item inside .mask —
|
|
39
|
+
shrink below its content size so the overflow/ellipsis can
|
|
40
|
+
engage. The default is 0 so every label can collapse fully;
|
|
41
|
+
a consumer that wants a squeezed pill to keep a couple of
|
|
42
|
+
characters visible (rather than collapsing to bare chrome)
|
|
43
|
+
opts in by setting --label-min-width (e.g. 2em). */
|
|
44
|
+
min-width: var(--label-min-width, 0);
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
.mask {
|
|
@@ -198,6 +207,15 @@ export default class Label extends LitElement {
|
|
|
198
207
|
@property({ type: String })
|
|
199
208
|
icon: string;
|
|
200
209
|
|
|
210
|
+
/**
|
|
211
|
+
* An additional icon rendered ahead of the main icon — e.g. a +/−
|
|
212
|
+
* qualifier on a group pill ("added to" vs "removed from"). Unlike
|
|
213
|
+
* `icon` it never falls back to the type's default, so it only
|
|
214
|
+
* renders when explicitly set.
|
|
215
|
+
*/
|
|
216
|
+
@property({ type: String, attribute: 'prefix-icon' })
|
|
217
|
+
prefixIcon: string;
|
|
218
|
+
|
|
201
219
|
/**
|
|
202
220
|
* Design-system pill variant — `flow`, `group`, `contact`, `field`,
|
|
203
221
|
* `keyword`, `label`, or `neutral`. When set, switches the chrome to
|
|
@@ -290,6 +308,9 @@ export default class Label extends LitElement {
|
|
|
290
308
|
<temba-icon name="x" size="0.85"></temba-icon>
|
|
291
309
|
</button>`
|
|
292
310
|
: null}
|
|
311
|
+
${this.prefixIcon
|
|
312
|
+
? html`<temba-icon name=${this.prefixIcon} />`
|
|
313
|
+
: null}
|
|
293
314
|
${resolvedIcon ? html`<temba-icon name=${resolvedIcon} />` : null}
|
|
294
315
|
<slot></slot>
|
|
295
316
|
</div>
|
package/src/display/TembaDate.ts
CHANGED
|
@@ -92,8 +92,17 @@ export class TembaDate extends RapidElement {
|
|
|
92
92
|
formatted = this.store.getShortDuration(this.datetime);
|
|
93
93
|
} else if (this.display === Display.countdown) {
|
|
94
94
|
formatted = this.store.getCountdown(this.datetime);
|
|
95
|
-
} else if (this.display ===
|
|
96
|
-
|
|
95
|
+
} else if (this.display === 'day') {
|
|
96
|
+
// the display attribute carries the key ('day'), and Display.day
|
|
97
|
+
// is a luxon format string ('LLL d') rather than an Intl options
|
|
98
|
+
// object, so it must go through toFormat — matching how the
|
|
99
|
+
// timedate branch renders the same format above. The short
|
|
100
|
+
// month-day form is only unambiguous for dates in the current
|
|
101
|
+
// year; anything else falls back to the locale date with year.
|
|
102
|
+
formatted =
|
|
103
|
+
this.datetime.year === DateTime.now().year
|
|
104
|
+
? this.datetime.toFormat(Display.day)
|
|
105
|
+
: this.datetime.toLocaleString(Display.date);
|
|
97
106
|
} else {
|
|
98
107
|
formatted = this.datetime.toLocaleString(Display[this.display]);
|
|
99
108
|
}
|
package/src/display/Tip.ts
CHANGED
|
@@ -8,7 +8,9 @@ export class Tip extends RapidElement {
|
|
|
8
8
|
static get styles() {
|
|
9
9
|
return css`
|
|
10
10
|
.tip {
|
|
11
|
-
transition:
|
|
11
|
+
transition:
|
|
12
|
+
opacity 120ms cubic-bezier(0.2, 0, 0, 1),
|
|
13
|
+
transform 120ms cubic-bezier(0.2, 0, 0, 1);
|
|
12
14
|
margin: 0px;
|
|
13
15
|
position: fixed;
|
|
14
16
|
opacity: 0;
|
|
@@ -44,11 +46,40 @@ export class Tip extends RapidElement {
|
|
|
44
46
|
transition: none;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/* hidden tips sit a few px toward their anchor so showing slides
|
|
50
|
+
them into position — up, down, or over based on the side */
|
|
51
|
+
.tip,
|
|
52
|
+
.tip.side-top {
|
|
53
|
+
transform: translateY(6px);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tip.side-bottom {
|
|
57
|
+
transform: translateY(-6px);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tip.side-left {
|
|
61
|
+
transform: translateX(6px);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tip.side-right {
|
|
65
|
+
transform: translateX(-6px);
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
.show {
|
|
48
69
|
opacity: 1;
|
|
49
70
|
z-index: 2147483647;
|
|
50
71
|
}
|
|
51
72
|
|
|
73
|
+
.tip.show {
|
|
74
|
+
transform: translate(0, 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* interactive tips accept the mouse while shown so their
|
|
78
|
+
contents can be selected or clicked */
|
|
79
|
+
.tip.interactive.show {
|
|
80
|
+
pointer-events: auto;
|
|
81
|
+
}
|
|
82
|
+
|
|
52
83
|
.slot {
|
|
53
84
|
display: flex;
|
|
54
85
|
flex-direction: column;
|
|
@@ -61,20 +92,27 @@ export class Tip extends RapidElement {
|
|
|
61
92
|
line-height: 0px;
|
|
62
93
|
}
|
|
63
94
|
|
|
95
|
+
/* squash the glyphs along their pointing axis (and widen the
|
|
96
|
+
base a touch) so the arrows read fat and stubby rather than
|
|
97
|
+
long and pointy */
|
|
64
98
|
.◀ {
|
|
65
99
|
text-shadow: -1px 2px 2px rgba(0, 0, 0, 0.1);
|
|
100
|
+
transform: scale(0.6, 1.15);
|
|
66
101
|
}
|
|
67
102
|
|
|
68
103
|
.▶ {
|
|
69
104
|
text-shadow: 1px 2px 2px rgba(0, 0, 0, 0.1);
|
|
105
|
+
transform: scale(0.6, 1.15);
|
|
70
106
|
}
|
|
71
107
|
|
|
72
108
|
.▼ {
|
|
73
109
|
text-shadow: 0px 3px 3px rgba(0, 0, 0, 0.1);
|
|
110
|
+
transform: scale(1.15, 0.6);
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
.▲ {
|
|
77
114
|
text-shadow: 0px -1px 1px rgba(0, 0, 0, 0.1);
|
|
115
|
+
transform: scale(1.15, 0.6);
|
|
78
116
|
}
|
|
79
117
|
`;
|
|
80
118
|
}
|
|
@@ -91,6 +129,20 @@ export class Tip extends RapidElement {
|
|
|
91
129
|
@property({ type: String })
|
|
92
130
|
position = 'auto';
|
|
93
131
|
|
|
132
|
+
// gap in px between the anchor and the tip edge; when unset, each
|
|
133
|
+
// side keeps its historical default (10-12px)
|
|
134
|
+
@property({ type: Number })
|
|
135
|
+
distance: number = null;
|
|
136
|
+
|
|
137
|
+
// font-size of the arrow glyph; offsets scale with it
|
|
138
|
+
@property({ type: Number, attribute: 'arrow-size' })
|
|
139
|
+
arrowSize = 10;
|
|
140
|
+
|
|
141
|
+
// interactive tips stay open while the mouse is over them so their
|
|
142
|
+
// contents can be selected or clicked
|
|
143
|
+
@property({ type: Boolean })
|
|
144
|
+
interactive = false;
|
|
145
|
+
|
|
94
146
|
@property({ type: Boolean })
|
|
95
147
|
hideOnChange: boolean;
|
|
96
148
|
|
|
@@ -114,6 +166,10 @@ export class Tip extends RapidElement {
|
|
|
114
166
|
arrowLeft: number;
|
|
115
167
|
arrowDirection: string;
|
|
116
168
|
|
|
169
|
+
// the side the tip last popped on, driving which direction it
|
|
170
|
+
// slides in from
|
|
171
|
+
side: string;
|
|
172
|
+
|
|
117
173
|
showTimer = 0;
|
|
118
174
|
hideTimer = 0;
|
|
119
175
|
|
|
@@ -122,6 +178,50 @@ export class Tip extends RapidElement {
|
|
|
122
178
|
if (changed.has('text') && this.hideOnChange) {
|
|
123
179
|
this.visible = false;
|
|
124
180
|
}
|
|
181
|
+
|
|
182
|
+
// stamp the placement side before the show transition starts so
|
|
183
|
+
// the very first pop already slides in from the correct direction
|
|
184
|
+
if (changed.has('visible') && this.visible) {
|
|
185
|
+
this.prepareSide();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Measures where the tip will pop and applies the matching side-*
|
|
191
|
+
* class while the tip is still hidden, flushing styles so the slide
|
|
192
|
+
* animation starts from that side's offset rather than the default.
|
|
193
|
+
*/
|
|
194
|
+
private prepareSide() {
|
|
195
|
+
const tipEl = this.getDiv('.tip') as HTMLElement;
|
|
196
|
+
const anchorEl = this.getDiv('.slot') as HTMLElement;
|
|
197
|
+
if (!tipEl || !anchorEl) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const side = this.chooseSide(
|
|
202
|
+
this.position,
|
|
203
|
+
anchorEl.getBoundingClientRect(),
|
|
204
|
+
tipEl.getBoundingClientRect(),
|
|
205
|
+
8
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (side !== this.side) {
|
|
209
|
+
this.side = side;
|
|
210
|
+
// snap (not animate) the hidden tip to the new side's offset so
|
|
211
|
+
// the show transition starts from there — without suppressing
|
|
212
|
+
// the transition, the class swap itself animates and the show
|
|
213
|
+
// retargets from the old offset mid-flight
|
|
214
|
+
tipEl.style.transition = 'none';
|
|
215
|
+
tipEl.classList.remove(
|
|
216
|
+
'side-top',
|
|
217
|
+
'side-bottom',
|
|
218
|
+
'side-left',
|
|
219
|
+
'side-right'
|
|
220
|
+
);
|
|
221
|
+
tipEl.classList.add(`side-${side}`);
|
|
222
|
+
void tipEl.offsetWidth;
|
|
223
|
+
tipEl.style.transition = '';
|
|
224
|
+
}
|
|
125
225
|
}
|
|
126
226
|
|
|
127
227
|
public updated(changed: Map<string, any>) {
|
|
@@ -144,38 +244,46 @@ export class Tip extends RapidElement {
|
|
|
144
244
|
tipBounds,
|
|
145
245
|
viewportPadding
|
|
146
246
|
);
|
|
247
|
+
this.side = side;
|
|
147
248
|
|
|
148
249
|
this.arrowLeft = 0;
|
|
149
250
|
this.arrowTop = 0;
|
|
150
251
|
|
|
252
|
+
// arrow offsets scale with the glyph size; the constants
|
|
253
|
+
// reproduce the historical values at the default size of 10
|
|
254
|
+
const arrow = this.arrowSize;
|
|
255
|
+
|
|
151
256
|
if (side === 'left') {
|
|
152
|
-
this.left = anchorBounds.left - tipBounds.width - 10;
|
|
257
|
+
this.left = anchorBounds.left - tipBounds.width - (this.distance ?? 10);
|
|
153
258
|
this.top = getMiddle(anchorBounds, tipBounds);
|
|
154
259
|
|
|
155
|
-
//
|
|
260
|
+
// center the glyph box on the tip edge so the arrow always
|
|
261
|
+
// touches the tip regardless of glyph metrics
|
|
156
262
|
this.arrowTop = tipBounds.height / 2;
|
|
157
|
-
this.arrowLeft = tipBounds.width
|
|
263
|
+
this.arrowLeft = tipBounds.width - arrow * 0.5;
|
|
158
264
|
this.arrow = '▶';
|
|
159
265
|
} else if (side === 'right') {
|
|
160
|
-
this.left = anchorBounds.right + 12;
|
|
266
|
+
this.left = anchorBounds.right + (this.distance ?? 12);
|
|
161
267
|
this.top = getMiddle(anchorBounds, tipBounds);
|
|
162
268
|
|
|
163
269
|
this.arrowTop = tipBounds.height / 2;
|
|
164
|
-
this.arrowLeft = -
|
|
270
|
+
this.arrowLeft = -arrow * 0.5;
|
|
165
271
|
this.arrow = '◀';
|
|
166
272
|
} else if (side === 'top') {
|
|
167
|
-
this.top = anchorBounds.top - tipBounds.height - 12;
|
|
273
|
+
this.top = anchorBounds.top - tipBounds.height - (this.distance ?? 12);
|
|
168
274
|
this.left = getCenter(anchorBounds, tipBounds);
|
|
169
275
|
|
|
170
|
-
|
|
171
|
-
|
|
276
|
+
// tucked in slightly so the squashed glyph's base stays
|
|
277
|
+
// buried under the tip edge
|
|
278
|
+
this.arrowTop = tipBounds.height + arrow * 0.1;
|
|
279
|
+
this.arrowLeft = tipBounds.width / 2 - arrow * 0.4;
|
|
172
280
|
this.arrow = '▼';
|
|
173
281
|
} else if (side === 'bottom') {
|
|
174
|
-
this.top = anchorBounds.bottom + 10;
|
|
282
|
+
this.top = anchorBounds.bottom + (this.distance ?? 10);
|
|
175
283
|
this.left = getCenter(anchorBounds, tipBounds);
|
|
176
284
|
|
|
177
|
-
this.arrowTop = -
|
|
178
|
-
this.arrowLeft = tipBounds.width / 2 - 3;
|
|
285
|
+
this.arrowTop = -arrow * 0.1;
|
|
286
|
+
this.arrowLeft = tipBounds.width / 2 - arrow * 0.3;
|
|
179
287
|
this.arrow = '▲';
|
|
180
288
|
}
|
|
181
289
|
|
|
@@ -194,7 +302,11 @@ export class Tip extends RapidElement {
|
|
|
194
302
|
|
|
195
303
|
if (side === 'top' || side === 'bottom') {
|
|
196
304
|
this.arrowLeft -= leftDelta;
|
|
197
|
-
this.arrowLeft = this.clamp(
|
|
305
|
+
this.arrowLeft = this.clamp(
|
|
306
|
+
this.arrowLeft,
|
|
307
|
+
8,
|
|
308
|
+
tipBounds.width - (arrow + 4)
|
|
309
|
+
);
|
|
198
310
|
} else {
|
|
199
311
|
this.arrowTop -= topDelta;
|
|
200
312
|
this.arrowTop = this.clamp(this.arrowTop, 8, tipBounds.height - 8);
|
|
@@ -207,6 +319,13 @@ export class Tip extends RapidElement {
|
|
|
207
319
|
this.arrowLeft = Math.round(this.arrowLeft);
|
|
208
320
|
|
|
209
321
|
// directly update DOM to avoid scheduling another update
|
|
322
|
+
tipEl.classList.remove(
|
|
323
|
+
'side-top',
|
|
324
|
+
'side-bottom',
|
|
325
|
+
'side-left',
|
|
326
|
+
'side-right'
|
|
327
|
+
);
|
|
328
|
+
tipEl.classList.add(`side-${side}`);
|
|
210
329
|
tipEl.style.top = `${this.top}px`;
|
|
211
330
|
tipEl.style.left = `${this.left}px`;
|
|
212
331
|
arrowEl.style.top = `${this.arrowTop}px`;
|
|
@@ -291,15 +410,27 @@ export class Tip extends RapidElement {
|
|
|
291
410
|
private handleMouseLeave() {
|
|
292
411
|
window.clearTimeout(this.showTimer);
|
|
293
412
|
window.clearTimeout(this.hideTimer);
|
|
294
|
-
|
|
413
|
+
|
|
414
|
+
// interactive tips linger long enough for the mouse to travel from
|
|
415
|
+
// the anchor into the tip itself
|
|
416
|
+
const hideDelay = this.interactive
|
|
417
|
+
? Math.max(this.hideDelay, 300)
|
|
418
|
+
: this.hideDelay;
|
|
419
|
+
|
|
420
|
+
if (hideDelay > 0) {
|
|
295
421
|
this.hideTimer = window.setTimeout(() => {
|
|
296
422
|
this.visible = false;
|
|
297
|
-
},
|
|
423
|
+
}, hideDelay);
|
|
298
424
|
return;
|
|
299
425
|
}
|
|
300
426
|
this.visible = false;
|
|
301
427
|
}
|
|
302
428
|
|
|
429
|
+
private handleTipMouseEnter() {
|
|
430
|
+
// the mouse made it into the tip — cancel any pending hide
|
|
431
|
+
window.clearTimeout(this.hideTimer);
|
|
432
|
+
}
|
|
433
|
+
|
|
303
434
|
public render(): TemplateResult {
|
|
304
435
|
const tipStyle: any = {
|
|
305
436
|
top: this.top ? `${this.top}px` : '0px',
|
|
@@ -308,7 +439,8 @@ export class Tip extends RapidElement {
|
|
|
308
439
|
|
|
309
440
|
const arrowStyle: any = {
|
|
310
441
|
top: this.arrowTop ? `${this.arrowTop}px` : '0px',
|
|
311
|
-
left: this.arrowLeft ? `${this.arrowLeft}px` : '0px'
|
|
442
|
+
left: this.arrowLeft ? `${this.arrowLeft}px` : '0px',
|
|
443
|
+
fontSize: `${this.arrowSize}px`
|
|
312
444
|
};
|
|
313
445
|
|
|
314
446
|
if (this.width) {
|
|
@@ -319,7 +451,9 @@ export class Tip extends RapidElement {
|
|
|
319
451
|
tip: true,
|
|
320
452
|
show: this.visible,
|
|
321
453
|
top: this.poppedTop,
|
|
322
|
-
|
|
454
|
+
interactive: this.interactive,
|
|
455
|
+
'hide-on-change': this.hideOnChange,
|
|
456
|
+
[`side-${this.side}`]: !!this.side
|
|
323
457
|
});
|
|
324
458
|
|
|
325
459
|
return html`
|
|
@@ -331,7 +465,12 @@ export class Tip extends RapidElement {
|
|
|
331
465
|
>
|
|
332
466
|
<slot></slot>
|
|
333
467
|
</div>
|
|
334
|
-
<div
|
|
468
|
+
<div
|
|
469
|
+
class="${classes}"
|
|
470
|
+
style=${styleMap(tipStyle)}
|
|
471
|
+
@mouseenter=${this.handleTipMouseEnter}
|
|
472
|
+
@mouseleave=${this.handleMouseLeave}
|
|
473
|
+
>
|
|
335
474
|
${this.content ?? this.text}
|
|
336
475
|
<div class="arrow" style=${styleMap(arrowStyle)}></div>
|
|
337
476
|
</div>
|