@ordergroove/offers 2.45.6-alpha-PR-1241-2.6 → 2.45.6
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 +8 -0
- package/dist/bundle-report.html +27 -31
- package/dist/offers.js +44 -71
- package/dist/offers.js.map +4 -4
- package/package.json +2 -2
- package/src/components/Tooltip.js +15 -126
- package/src/components/__tests__/Tooltip.spec.js +3 -213
- package/src/core/__tests__/experiments.spec.js +5 -1
- package/src/make-api.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordergroove/offers",
|
|
3
|
-
"version": "2.45.6
|
|
3
|
+
"version": "2.45.6",
|
|
4
4
|
"description": "offer state component",
|
|
5
5
|
"author": "Eugenio Lattanzio <eugenio63@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/ordergroove/plush-toys#readme",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"@ordergroove/offers-templates": "^0.9.8",
|
|
50
50
|
"@types/lodash.memoize": "^4.1.9"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "60f759b8f2452b4569e6e5d0f6c8e06c96581d8e"
|
|
53
53
|
}
|
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit-element';
|
|
2
|
-
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
|
3
|
-
|
|
4
|
-
const ACTIVATION_TYPES = {
|
|
5
|
-
AUTOMATIC: 'automatic',
|
|
6
|
-
MANUAL: 'manual'
|
|
7
|
-
};
|
|
8
2
|
|
|
9
3
|
export class Tooltip extends LitElement {
|
|
10
|
-
constructor() {
|
|
11
|
-
super();
|
|
12
|
-
this.triggerLabel = 'Show tooltip';
|
|
13
|
-
this.open = false;
|
|
14
|
-
/** Default is "automatic" for backwards compatibility with existing templates */
|
|
15
|
-
this.activationType = ACTIVATION_TYPES.AUTOMATIC;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
4
|
static get properties() {
|
|
19
5
|
return {
|
|
20
|
-
placement: { type: String, default: 'bottom' }
|
|
21
|
-
/** Set the aria-label attribute of the trigger. */
|
|
22
|
-
triggerLabel: { type: String, attribute: 'trigger-label' },
|
|
23
|
-
/**
|
|
24
|
-
* "automatic" - show tooltip on hover and focus
|
|
25
|
-
* "manual" - show tooltip on hover and click
|
|
26
|
-
*/
|
|
27
|
-
activationType: { type: String, attribute: 'activation-type' },
|
|
28
|
-
/** Whether the tooltip is showing. Internal property; only here so that we re-render when it changes */
|
|
29
|
-
open: { type: Boolean, attribute: false }
|
|
6
|
+
placement: { type: String, default: 'bottom' }
|
|
30
7
|
};
|
|
31
8
|
}
|
|
32
9
|
|
|
@@ -42,27 +19,11 @@ export class Tooltip extends LitElement {
|
|
|
42
19
|
z-index: 9;
|
|
43
20
|
}
|
|
44
21
|
|
|
45
|
-
/* reset default button styles */
|
|
46
|
-
button.trigger {
|
|
47
|
-
all: unset;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* do not reset the button's default focus outline */
|
|
51
|
-
button.trigger:focus {
|
|
52
|
-
outline: revert;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
22
|
.trigger {
|
|
56
23
|
display: block;
|
|
57
24
|
cursor: pointer;
|
|
58
25
|
}
|
|
59
26
|
|
|
60
|
-
/* for manual activation, hide the content completely from screen readers when the tooltip is closed */
|
|
61
|
-
/* otherwise, interactive elements may receive focus even when they are not visible */
|
|
62
|
-
[data-manual] .content {
|
|
63
|
-
visibility: hidden;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
27
|
.content {
|
|
67
28
|
box-sizing: border-box;
|
|
68
29
|
font-family: var(--og-tooltip-family, inherit);
|
|
@@ -191,8 +152,9 @@ export class Tooltip extends LitElement {
|
|
|
191
152
|
border-left: solid var(--og-tooltip-background, #ececec) 10px;
|
|
192
153
|
}
|
|
193
154
|
|
|
194
|
-
.tooltip
|
|
195
|
-
|
|
155
|
+
.tooltip:hover .content,
|
|
156
|
+
.trigger:focus + .content,
|
|
157
|
+
.content:focus-within {
|
|
196
158
|
opacity: 1;
|
|
197
159
|
width: 200px;
|
|
198
160
|
pointer-events: auto;
|
|
@@ -203,21 +165,12 @@ export class Tooltip extends LitElement {
|
|
|
203
165
|
|
|
204
166
|
connectedCallback() {
|
|
205
167
|
super.connectedCallback();
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.addEventListener('mouseenter', this.handleMouseEnter.bind(this), { signal });
|
|
210
|
-
this.addEventListener('mouseleave', this.handleMouseLeave.bind(this), { signal });
|
|
211
|
-
this.addEventListener('focusin', this.handleFocusIn.bind(this), { signal });
|
|
212
|
-
this.addEventListener('focusout', this.handleFocusOut.bind(this), { signal });
|
|
213
|
-
this.addEventListener('keydown', this.handleKeyDown.bind(this), { signal });
|
|
214
|
-
|
|
215
|
-
document.addEventListener('click', this.handleDocumentClick.bind(this), { signal });
|
|
168
|
+
this.recalculatePosition = this.recalculatePosition.bind(this);
|
|
169
|
+
this.addEventListener('mouseenter', this.recalculatePosition);
|
|
170
|
+
this.addEventListener('focusin', this.recalculatePosition);
|
|
216
171
|
}
|
|
217
172
|
|
|
218
|
-
|
|
219
|
-
// wait for state changes to apply
|
|
220
|
-
await this.updateComplete;
|
|
173
|
+
recalculatePosition() {
|
|
221
174
|
const trigger = this.shadowRoot.querySelector('.trigger');
|
|
222
175
|
const triggerRect = trigger.getBoundingClientRect();
|
|
223
176
|
const content = this.shadowRoot.querySelector('.content');
|
|
@@ -228,83 +181,19 @@ export class Tooltip extends LitElement {
|
|
|
228
181
|
content.style.top = `${(-1 * contentRect.height + triggerRect.height) / 2}px`;
|
|
229
182
|
}
|
|
230
183
|
|
|
231
|
-
handleMouseEnter() {
|
|
232
|
-
this.open = true;
|
|
233
|
-
this.recalculatePosition();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
handleMouseLeave() {
|
|
237
|
-
this.open = false;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
handleFocusIn() {
|
|
241
|
-
if (this.activationType !== ACTIVATION_TYPES.AUTOMATIC) return;
|
|
242
|
-
this.open = true;
|
|
243
|
-
this.recalculatePosition();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
handleFocusOut(event) {
|
|
247
|
-
if (this.activationType !== ACTIVATION_TYPES.AUTOMATIC) return;
|
|
248
|
-
// keep the tooltip open if we're moving focus to another element inside the tooltip
|
|
249
|
-
if (!this.contains(event.relatedTarget)) {
|
|
250
|
-
this.open = false;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
handleKeyDown(event) {
|
|
255
|
-
if (this.activationType !== ACTIVATION_TYPES.MANUAL) return;
|
|
256
|
-
// close the tooltip on Escape press
|
|
257
|
-
if (event.key === 'Escape' && this.open) {
|
|
258
|
-
this.open = false;
|
|
259
|
-
event.stopPropagation();
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
handleClick() {
|
|
264
|
-
if (this.activationType !== ACTIVATION_TYPES.MANUAL) return;
|
|
265
|
-
this.open = !this.open;
|
|
266
|
-
this.recalculatePosition();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
handleDocumentClick(event) {
|
|
270
|
-
if (this.activationType !== ACTIVATION_TYPES.MANUAL || !this.open) return;
|
|
271
|
-
// close the tooltip if the user clicks outside of it
|
|
272
|
-
if (!this.contains(event.target)) {
|
|
273
|
-
this.open = false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
184
|
disconnectedCallback() {
|
|
278
185
|
super.disconnectedCallback();
|
|
279
|
-
|
|
280
|
-
this.
|
|
186
|
+
this.removeEventListener('mouseenter', this.recalculatePosition);
|
|
187
|
+
this.removeEventListener('focusin', this.recalculatePosition);
|
|
281
188
|
}
|
|
282
189
|
|
|
283
190
|
render() {
|
|
284
|
-
// allow removing aria-label by setting trigger-label to any falsy value
|
|
285
|
-
// e.g. if the content inside the tooltip is sufficient
|
|
286
|
-
const triggerLabel = this.triggerLabel ? this.triggerLabel : undefined;
|
|
287
|
-
|
|
288
191
|
return html`
|
|
289
|
-
<span class="tooltip"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
aria-label="${ifDefined(triggerLabel)}"
|
|
295
|
-
aria-expanded="${this.open}"
|
|
296
|
-
aria-controls="tooltip-content"
|
|
297
|
-
@click="${this.handleClick}"
|
|
298
|
-
>
|
|
299
|
-
<slot name="trigger">${this.trigger}</slot>
|
|
300
|
-
</button>
|
|
301
|
-
`
|
|
302
|
-
: html`
|
|
303
|
-
<span class="trigger" tabindex="0" aria-label="${ifDefined(triggerLabel)}">
|
|
304
|
-
<slot name="trigger">${this.trigger}</slot>
|
|
305
|
-
</span>
|
|
306
|
-
`}
|
|
307
|
-
<div class="content ${this.placement || 'bottom'}" role="tooltip" id="tooltip-content">
|
|
192
|
+
<span class="tooltip">
|
|
193
|
+
<span class="trigger" tabindex="0">
|
|
194
|
+
<slot name="trigger">${this.trigger}</slot>
|
|
195
|
+
</span>
|
|
196
|
+
<div class="content ${this.placement || 'bottom'}">
|
|
308
197
|
<slot name="content">${this.content}</slot>
|
|
309
198
|
</div>
|
|
310
199
|
</span>
|
|
@@ -16,20 +16,17 @@ describe('Tooltip', () => {
|
|
|
16
16
|
);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const getTooltip = async
|
|
19
|
+
const getTooltip = async placement => {
|
|
20
20
|
const element = new Tooltip();
|
|
21
21
|
if (placement) {
|
|
22
22
|
element.setAttribute('placement', placement);
|
|
23
23
|
}
|
|
24
|
-
if (activationType) {
|
|
25
|
-
element.setAttribute('activation-type', activationType);
|
|
26
|
-
}
|
|
27
24
|
await appendToBody(element);
|
|
28
25
|
return element;
|
|
29
26
|
};
|
|
30
27
|
|
|
31
28
|
const checkContentStyles = async (expectedStyles, placement, contentStyles) => {
|
|
32
|
-
const element = await getTooltip(
|
|
29
|
+
const element = await getTooltip(placement);
|
|
33
30
|
const contentDiv = element.shadowRoot.querySelector('.content');
|
|
34
31
|
|
|
35
32
|
// Modify shadow content
|
|
@@ -39,7 +36,7 @@ describe('Tooltip', () => {
|
|
|
39
36
|
});
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
element.recalculatePosition();
|
|
43
40
|
Object.keys(expectedStyles).forEach(key => expect(contentDiv.style[key]).toEqual(expectedStyles[key]));
|
|
44
41
|
};
|
|
45
42
|
|
|
@@ -78,211 +75,4 @@ describe('Tooltip', () => {
|
|
|
78
75
|
it('should not set style properties if placement is top-left', async () => {
|
|
79
76
|
await checkContentStyles({ top: '', left: '' }, 'top-left');
|
|
80
77
|
});
|
|
81
|
-
|
|
82
|
-
const ACTIVATION_TYPES = ['automatic', 'manual'];
|
|
83
|
-
|
|
84
|
-
ACTIVATION_TYPES.forEach(activationType => {
|
|
85
|
-
it(`${activationType} should show tooltip on mouseenter`, async () => {
|
|
86
|
-
const element = await getTooltip({ activationType });
|
|
87
|
-
expect(element.open).toBe(false);
|
|
88
|
-
|
|
89
|
-
element.dispatchEvent(new Event('mouseenter'));
|
|
90
|
-
await element.updateComplete;
|
|
91
|
-
|
|
92
|
-
expect(element.open).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it(`${activationType} should hide tooltip on mouseleave`, async () => {
|
|
96
|
-
const element = await getTooltip();
|
|
97
|
-
element.open = true;
|
|
98
|
-
await element.updateComplete;
|
|
99
|
-
|
|
100
|
-
element.dispatchEvent(new Event('mouseleave'));
|
|
101
|
-
await element.updateComplete;
|
|
102
|
-
|
|
103
|
-
expect(element.open).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it(`${activationType} should remove aria-label when trigger-label is empty`, async () => {
|
|
107
|
-
const element = await getTooltip({ activationType });
|
|
108
|
-
element.setAttribute('trigger-label', '');
|
|
109
|
-
await element.updateComplete;
|
|
110
|
-
|
|
111
|
-
const trigger = element.shadowRoot.querySelector('.trigger');
|
|
112
|
-
expect(trigger.hasAttribute('aria-label')).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it(`${activationType} should set aria-label when trigger-label has value`, async () => {
|
|
116
|
-
const element = await getTooltip({ activationType });
|
|
117
|
-
element.setAttribute('trigger-label', 'Custom tooltip label');
|
|
118
|
-
await element.updateComplete;
|
|
119
|
-
|
|
120
|
-
const trigger = element.shadowRoot.querySelector('.trigger');
|
|
121
|
-
expect(trigger.getAttribute('aria-label')).toBe('Custom tooltip label');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('automatic activation behavior', () => {
|
|
126
|
-
it('should show tooltip on focusin', async () => {
|
|
127
|
-
const element = await getTooltip();
|
|
128
|
-
expect(element.open).toBe(false);
|
|
129
|
-
|
|
130
|
-
element.dispatchEvent(new Event('focusin'));
|
|
131
|
-
await element.updateComplete;
|
|
132
|
-
|
|
133
|
-
expect(element.open).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should hide tooltip on focusout when focus moves outside', async () => {
|
|
137
|
-
const element = await getTooltip();
|
|
138
|
-
const externalElement = document.createElement('div');
|
|
139
|
-
document.body.appendChild(externalElement);
|
|
140
|
-
|
|
141
|
-
element.open = true;
|
|
142
|
-
await element.updateComplete;
|
|
143
|
-
|
|
144
|
-
const focusOutEvent = new FocusEvent('focusout', { relatedTarget: externalElement });
|
|
145
|
-
element.dispatchEvent(focusOutEvent);
|
|
146
|
-
await element.updateComplete;
|
|
147
|
-
|
|
148
|
-
expect(element.open).toBe(false);
|
|
149
|
-
document.body.removeChild(externalElement);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should keep tooltip open on focusout when focus stays within tooltip', async () => {
|
|
153
|
-
const element = await getTooltip();
|
|
154
|
-
const trigger = element.shadowRoot.querySelector('.trigger');
|
|
155
|
-
|
|
156
|
-
element.open = true;
|
|
157
|
-
await element.updateComplete;
|
|
158
|
-
|
|
159
|
-
// focus moving out of one element and back to the trigger
|
|
160
|
-
const focusOutEvent = new FocusEvent('focusout', { relatedTarget: trigger });
|
|
161
|
-
element.dispatchEvent(focusOutEvent);
|
|
162
|
-
await element.updateComplete;
|
|
163
|
-
|
|
164
|
-
expect(element.open).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should not open tooltip on click', async () => {
|
|
168
|
-
const element = await getTooltip();
|
|
169
|
-
const trigger = element.shadowRoot.querySelector('.trigger');
|
|
170
|
-
|
|
171
|
-
expect(element.open).toBe(false);
|
|
172
|
-
|
|
173
|
-
trigger.click();
|
|
174
|
-
await element.updateComplete;
|
|
175
|
-
|
|
176
|
-
expect(element.open).toBe(false);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should not do anything when clicking outside tooltip', async () => {
|
|
180
|
-
const element = await getTooltip();
|
|
181
|
-
element.open = true;
|
|
182
|
-
await element.updateComplete;
|
|
183
|
-
|
|
184
|
-
const outsideClick = new Event('click', { bubbles: true });
|
|
185
|
-
document.body.dispatchEvent(outsideClick);
|
|
186
|
-
await element.updateComplete;
|
|
187
|
-
|
|
188
|
-
expect(element.open).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('updates attributes', async () => {
|
|
192
|
-
const element = await getTooltip();
|
|
193
|
-
element.open = true;
|
|
194
|
-
await element.updateComplete;
|
|
195
|
-
|
|
196
|
-
let style = getComputedStyle(element.shadowRoot.querySelector('.content'));
|
|
197
|
-
expect(style.opacity).toBe('1');
|
|
198
|
-
|
|
199
|
-
element.open = false;
|
|
200
|
-
await element.updateComplete;
|
|
201
|
-
style = getComputedStyle(element.shadowRoot.querySelector('.content'));
|
|
202
|
-
expect(style.opacity).toBe('0');
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('manual activation behavior', () => {
|
|
207
|
-
it('should toggle tooltip on click', async () => {
|
|
208
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
209
|
-
const trigger = element.shadowRoot.querySelector('.trigger');
|
|
210
|
-
|
|
211
|
-
expect(element.open).toBe(false);
|
|
212
|
-
|
|
213
|
-
trigger.click();
|
|
214
|
-
await element.updateComplete;
|
|
215
|
-
expect(element.open).toBe(true);
|
|
216
|
-
|
|
217
|
-
trigger.click();
|
|
218
|
-
await element.updateComplete;
|
|
219
|
-
expect(element.open).toBe(false);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should not open tooltip on focus', async () => {
|
|
223
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
224
|
-
|
|
225
|
-
expect(element.open).toBe(false);
|
|
226
|
-
|
|
227
|
-
element.dispatchEvent(new Event('focusin'));
|
|
228
|
-
await element.updateComplete;
|
|
229
|
-
|
|
230
|
-
expect(element.open).toBe(false);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should close tooltip when clicking outside', async () => {
|
|
234
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
235
|
-
element.open = true;
|
|
236
|
-
await element.updateComplete;
|
|
237
|
-
|
|
238
|
-
const outsideClick = new Event('click', { bubbles: true });
|
|
239
|
-
document.body.dispatchEvent(outsideClick);
|
|
240
|
-
await element.updateComplete;
|
|
241
|
-
|
|
242
|
-
expect(element.open).toBe(false);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('updates attributes', async () => {
|
|
246
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
247
|
-
element.open = true;
|
|
248
|
-
await element.updateComplete;
|
|
249
|
-
expect(element.shadowRoot.querySelector('.trigger').ariaExpanded).toBe('true');
|
|
250
|
-
let style = getComputedStyle(element.shadowRoot.querySelector('.content'));
|
|
251
|
-
expect(style.visibility).toBe('visible');
|
|
252
|
-
expect(style.opacity).toBe('1');
|
|
253
|
-
|
|
254
|
-
element.open = false;
|
|
255
|
-
await element.updateComplete;
|
|
256
|
-
expect(element.shadowRoot.querySelector('.trigger').ariaExpanded).toBe('false');
|
|
257
|
-
style = getComputedStyle(element.shadowRoot.querySelector('.content'));
|
|
258
|
-
expect(style.visibility).toBe('hidden');
|
|
259
|
-
expect(style.opacity).toBe('0');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('should close tooltip on Escape key', async () => {
|
|
263
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
264
|
-
element.open = true;
|
|
265
|
-
await element.updateComplete;
|
|
266
|
-
|
|
267
|
-
expect(element.open).toBe(true);
|
|
268
|
-
|
|
269
|
-
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' });
|
|
270
|
-
element.dispatchEvent(escapeEvent);
|
|
271
|
-
await element.updateComplete;
|
|
272
|
-
|
|
273
|
-
expect(element.open).toBe(false);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should not close tooltip on other keys', async () => {
|
|
277
|
-
const element = await getTooltip({ activationType: 'manual' });
|
|
278
|
-
element.open = true;
|
|
279
|
-
await element.updateComplete;
|
|
280
|
-
|
|
281
|
-
const enterEvent = new KeyboardEvent('keydown', { key: 'Alt' });
|
|
282
|
-
element.dispatchEvent(enterEvent);
|
|
283
|
-
await element.updateComplete;
|
|
284
|
-
|
|
285
|
-
expect(element.open).toBe(true);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
78
|
});
|
|
@@ -97,7 +97,11 @@ describe('experiments', () => {
|
|
|
97
97
|
}
|
|
98
98
|
inputs.forEach(weights =>
|
|
99
99
|
it(`should roll the dice ${weights}`, () => {
|
|
100
|
-
|
|
100
|
+
const results = test(weights);
|
|
101
|
+
results.forEach((result, index) => {
|
|
102
|
+
// since this is random, we give it +-1 margin of error
|
|
103
|
+
expect(Math.abs(result - weights[index])).toBeLessThanOrEqual(1);
|
|
104
|
+
});
|
|
101
105
|
})
|
|
102
106
|
);
|
|
103
107
|
});
|