@brightspace-ui/labs 1.1.0 → 1.2.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.
@@ -0,0 +1,501 @@
1
+ import '@brightspace-ui/core/components/button/button.js';
2
+ import '@brightspace-ui/core/components/colors/colors.js';
3
+ import '@brightspace-ui/core/components/icons/icon.js';
4
+ import '@brightspace-ui/core/components/html-block/html-block.js';
5
+ import './opt-out-dialog.js';
6
+ import { bodyStandardStyles, heading1Styles } from '@brightspace-ui/core/components/typography/styles.js';
7
+ import { css, html, LitElement, nothing } from 'lit';
8
+ import { classMap } from 'lit/directives/class-map.js';
9
+ import { composeMixins } from '@brightspace-ui/core/helpers/composeMixins.js';
10
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
11
+ import { RtlMixin } from '@brightspace-ui/core/mixins/rtl-mixin.js';
12
+
13
+ const VISIBLE_STATES = Object.freeze({
14
+ opened: 'OPENED',
15
+ opening: 'OPENING',
16
+ closed: 'CLOSED',
17
+ closing: 'CLOSING'
18
+ });
19
+
20
+ class FlyoutImplementation extends composeMixins(
21
+ LitElement,
22
+ LocalizeLabsElement,
23
+ RtlMixin
24
+ ) {
25
+
26
+ static get properties() {
27
+ return {
28
+ optOut: { type: Boolean, attribute: 'opt-out' },
29
+ open: { type: Boolean, reflect: true },
30
+ title: { type: String },
31
+ shortDescription: { type: String, attribute: 'short-description' },
32
+ longDescription: { type: String, attribute: 'long-description' },
33
+ tabPosition: { type: String, attribute: 'tab-position' },
34
+ noTransform: { type: Boolean, attribute: 'no-transform' },
35
+ tutorialLink: { type: String, attribute: 'tutorial-link' },
36
+ helpDocsLink: { type: String, attribute: 'help-docs-link' },
37
+ hideReason: { type: Boolean, attribute: 'hide-reason' },
38
+ hideFeedback: { type: Boolean, attribute: 'hide-feedback' },
39
+ _optOutDialogOpen: { state: true },
40
+ _primaryButtonText: { state: true },
41
+ _secondaryButtonText: { state: true },
42
+ _visibleState: { state: true },
43
+ _ariaLabel: { state: true },
44
+ _tabIndex: { state: true }
45
+ };
46
+ }
47
+
48
+ static get styles() {
49
+ return [
50
+ bodyStandardStyles,
51
+ heading1Styles,
52
+ css`
53
+ :host {
54
+ height: 100%;
55
+ overflow: hidden;
56
+ pointer-events: none;
57
+ position: absolute;
58
+ width: 100%;
59
+ }
60
+
61
+ #flyout {
62
+ background-color: white;
63
+ border-bottom: 1px solid var(--d2l-color-mica);
64
+ box-sizing: border-box;
65
+ overflow: visible;
66
+ padding-bottom: 2rem;
67
+ pointer-events: auto;
68
+ position: var(--d2l-flyout-custom-element-position, absolute);
69
+ top: var(--d2l-flyout-custom-element-top, 0);
70
+ width: 100%;
71
+ z-index: var(--d2l-flyout-custom-element-z-index, 900);
72
+ }
73
+
74
+ #flyout.flyout-opened {
75
+ transition: transform 0.2s ease-out;
76
+ }
77
+
78
+ #flyout.flyout-closed {
79
+ transition: transform 0.2s ease-in;
80
+ }
81
+
82
+ .flyout-opened {
83
+ transform: translateY(0);
84
+ }
85
+ .flyout-closed {
86
+ transform: translateY(-100%);
87
+ }
88
+
89
+ .flyout-content {
90
+ text-align: center;
91
+ }
92
+
93
+ .flyout-content h1 {
94
+ margin-bottom: 1.2rem;
95
+ }
96
+
97
+ .flyout-text {
98
+ align-items: center;
99
+ display: flex;
100
+ flex-direction: column;
101
+ margin-bottom: 1.5rem;
102
+ }
103
+
104
+ #short-description {
105
+ margin-bottom: 0.5rem;
106
+ }
107
+
108
+ #long-description {
109
+ max-width: 800px;
110
+ }
111
+
112
+ .flyout-tutorial {
113
+ margin: auto;
114
+ margin-top: 0.5em;
115
+ }
116
+
117
+ .flyout-tutorial a {
118
+ color: var(--d2l-color-celestine);
119
+ text-decoration: none;
120
+ }
121
+
122
+ .flyout-tutorial a:hover, .flyout-tutorial a:focus {
123
+ text-decoration: underline;
124
+ }
125
+
126
+ .flyout-buttons {
127
+ margin-left: auto;
128
+ margin-right: auto;
129
+ }
130
+
131
+ .flyout-buttons d2l-button {
132
+ margin-left: 0.5rem;
133
+ margin-right: 0.5rem;
134
+ }
135
+
136
+ .flyout-tab-container {
137
+ height: 1.2rem;
138
+ left: 50%;
139
+ max-width: 1230px;
140
+ overflow: hidden;
141
+ pointer-events: none;
142
+ position: absolute;
143
+ top: 100%;
144
+ transform: translateX(-50%);
145
+ width: 100%;
146
+ }
147
+
148
+ .flyout-tab {
149
+ background-color: white;
150
+ border: 1px solid var(--d2l-color-mica);
151
+ border-radius: 0 0 8px 8px;
152
+ border-top: none;
153
+ box-sizing: border-box;
154
+ cursor: pointer;
155
+ height: 1rem;
156
+ min-height: 0;
157
+ padding: 1px;
158
+ pointer-events: auto;
159
+ position: absolute;
160
+ text-align: center;
161
+ top: 0;
162
+ width: 5rem;
163
+ }
164
+
165
+ .flyout-tab:hover, .flyout-tab:focus {
166
+ background-color: var(--d2l-color-gypsum);
167
+ }
168
+
169
+ .flyout-tab:focus {
170
+ border-color: rgba(0, 111, 191, 0.4);
171
+ border-style: solid;
172
+ border-width: 0 1px 1px 1px;
173
+ box-shadow: 0 0 0 4px rgba(0, 111, 191, 0.3);
174
+ }
175
+
176
+ .flyout-tab:active, .flyout-tab:focus {
177
+ outline: 0;
178
+ }
179
+
180
+ .flyout-tab > d2l-icon {
181
+ margin: auto;
182
+ vertical-align: top !important;
183
+ }
184
+ `
185
+ ];
186
+ }
187
+
188
+ constructor() {
189
+ super();
190
+ this.tabPosition = 'right';
191
+ this._visibleState = VISIBLE_STATES.closed;
192
+ }
193
+
194
+ connectedCallback() {
195
+ super.connectedCallback();
196
+ this._visibleState = this.open ? VISIBLE_STATES.opened : VISIBLE_STATES.closed;
197
+ }
198
+
199
+ render() {
200
+ return html`
201
+ ${this._renderOptInDialog()}
202
+ <div id="flyout" class="${classMap(this._getFlyoutClasses())}" @transitionend="${this._onTransitionComplete}">
203
+ <span tabindex="${this._getTabIndex()}" @focus="${this._shiftToLast}"></span>
204
+ ${this._renderFlyoutContent()}
205
+ <div class="flyout-tab-container">
206
+ <button id="flyout-tab" class="flyout-tab" style="${this._getTabStyle()}" tabindex="0" aria-label="${this._getAriaLabelForTab()}" @click="${this._clickTab}">
207
+ <d2l-icon icon="${this._getTabIcon()}"></d2l-icon>
208
+ </button>
209
+ </div>
210
+ <span tabindex="${this._getTabIndex()}" @focus="${this._shiftToFirst}"></span>
211
+ </div>
212
+ `;
213
+ }
214
+
215
+ updated(changedProperties) {
216
+ super.updated(changedProperties);
217
+ if (changedProperties.has('open')) {
218
+ this._openChanged();
219
+ }
220
+ }
221
+
222
+ focus() {
223
+ this._shiftToFirst();
224
+ }
225
+
226
+ _cancelOptOut(event) {
227
+ this._optOutDialogOpen = false;
228
+ this.shadowRoot.querySelector('#opt-out-button').focus();
229
+ event.stopPropagation();
230
+ }
231
+
232
+ _checkNumberOfLinks(expectedNumber) {
233
+ let result;
234
+ if (this.tutorialLink && this.helpDocsLink) {
235
+ result = 2;
236
+ } else if (this.tutorialLink || this.helpDocsLink) {
237
+ result = 1;
238
+ } else {
239
+ result = 0;
240
+ }
241
+ return result === expectedNumber;
242
+ }
243
+
244
+ _clickOptIn() {
245
+ this._fireEvent('opt-in');
246
+ this.open = false;
247
+ }
248
+
249
+ _clickOptOut() {
250
+ if (this.optOut && (!this.hideReason || !this.hideFeedback)) {
251
+ this._optOutDialogOpen = true;
252
+ } else {
253
+ this._fireEvent('opt-out');
254
+ this.open = false;
255
+ }
256
+ }
257
+
258
+ _clickTab() {
259
+ if (this._visibleState === VISIBLE_STATES.opened || this._visibleState === VISIBLE_STATES.closed) {
260
+ this.open = !this.open;
261
+ }
262
+ }
263
+
264
+ _confirmOptOut(event) {
265
+ this._optOutDialogOpen = false;
266
+ this._fireEvent('opt-out', event.detail);
267
+ this.open = false;
268
+ event.stopPropagation();
269
+ }
270
+
271
+ _fireEvent(name, details) {
272
+ this.dispatchEvent(
273
+ new CustomEvent(
274
+ name, {
275
+ bubbles: true,
276
+ composed: true,
277
+ detail: details
278
+ }
279
+ )
280
+ );
281
+ }
282
+
283
+ _getAriaLabelForTab() {
284
+ if (this.open) {
285
+ return this.localize('components:optInFlyout:close');
286
+ }
287
+ return this.localize(this.optOut ? 'components:optInFlyout:openOptOut' : 'components:optInFlyout:openOptIn');
288
+ }
289
+
290
+ _getDescription() {
291
+ return this.shortDescription ? 'short-description' : (this.longDescription ? 'long-description' : '');
292
+ }
293
+
294
+ _getFlyoutClasses() {
295
+ const openState = this._visibleState === VISIBLE_STATES.opening || this._visibleState === VISIBLE_STATES.opened;
296
+ return {
297
+ 'flyout-opened': openState,
298
+ 'flyout-closed': !openState,
299
+ 'd2l-body-standard': true
300
+ };
301
+ }
302
+
303
+ _getPrimaryButtonText() {
304
+ return this.localize(this.optOut ? 'components:optInFlyout:leaveOn' : 'components:optInFlyout:turnOn');
305
+ }
306
+
307
+ _getSecondaryButtonText() {
308
+ return this.localize(this.optOut ? 'components:optInFlyout:turnOff' : 'components:optInFlyout:leaveOff');
309
+ }
310
+
311
+ _getTabIcon() {
312
+ if (this._visibleState === VISIBLE_STATES.closed || this._visibleState === VISIBLE_STATES.closing) {
313
+ return 'tier1:chevron-down';
314
+ } else {
315
+ return 'tier1:chevron-up';
316
+ }
317
+ }
318
+
319
+ _getTabIndex() {
320
+ return this.open ? 0 : -1;
321
+ }
322
+
323
+ _getTabStyle() {
324
+ let rtl = this.dir === 'rtl';
325
+ let position = '';
326
+ if (this.tabPosition === 'left') {
327
+ position = 'calc(2.5rem + 15px)';
328
+ } else if (this.tabPosition === 'right' || this.tabPosition === 'default' || !this.tabPosition) {
329
+ position = 'calc(2.5rem + 15px)';
330
+ rtl = !rtl;
331
+ } else if (this.tabPosition === 'center' || this.tabPosition === 'centre') {
332
+ position = '50%';
333
+ } else if (!/^\d+(?:\.\d+)?%$/.test(this.tabPosition)) {
334
+ /* eslint-disable no-console */
335
+ console.warn('Invalid position supplied to opt-in flyout');
336
+ position = 'calc(2.5rem + 15px)';
337
+ rtl = !rtl;
338
+ }
339
+
340
+ const side = rtl ? 'right' : 'left';
341
+ const shift = rtl ? '50%' : '-50%';
342
+
343
+ const tabStyle = `${side}: ${position};`;
344
+
345
+ if (this.noTransform) {
346
+ return tabStyle;
347
+ }
348
+
349
+ return `${tabStyle} transform: translateX(${shift});`;
350
+ }
351
+
352
+ _getTutorialLink(i) {
353
+ if (this.tutorialLink && this.helpDocsLink) {
354
+ const translation = this.localize('components:optInFlyout:tutorialAndHelpMessage');
355
+ const videoFirst = translation.indexOf('*') < translation.indexOf('~');
356
+
357
+ const links = videoFirst ? [this.tutorialLink, this.helpDocsLink] : [this.helpDocsLink, this.tutorialLink];
358
+ return links[i];
359
+ }
360
+ return this.tutorialLink || this.helpDocsLink || null;
361
+ }
362
+
363
+ _getTutorialTextPart(textPart) {
364
+ if (this.tutorialLink && this.helpDocsLink) {
365
+ const tutorialHelpMessage = this.localize('components:optInFlyout:tutorialAndHelpMessage');
366
+ return tutorialHelpMessage.split(/\*|~/)[textPart] || '';
367
+ } else if (this.tutorialLink || this.helpDocsLink) {
368
+ const individualMessage = this.localize(this.tutorialLink ? 'components:optInFlyout:tutorialMessage' : 'components:optInFlyout:helpMessage');
369
+ return individualMessage.split('*')[textPart] || '';
370
+ } else {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ _onTransitionComplete(event) {
376
+ if (event.target.id !== 'flyout' || event.propertyName !== 'transform') {
377
+ return null;
378
+ }
379
+
380
+ if (this._visibleState === VISIBLE_STATES.opening) {
381
+ this._visibleState = VISIBLE_STATES.opened;
382
+ this.shadowRoot.querySelector('#primary-button').focus();
383
+ } else if (this._visibleState === VISIBLE_STATES.closing) {
384
+ this._visibleState = VISIBLE_STATES.closed;
385
+ this.shadowRoot.querySelector('#flyout-tab').focus();
386
+ }
387
+ }
388
+
389
+ _openChanged() {
390
+ if (this.open && this._visibleState === VISIBLE_STATES.closed || this._visibleState === VISIBLE_STATES.closing) {
391
+ this._visibleState = VISIBLE_STATES.opening;
392
+ } else if (!this.open && this._visibleState === VISIBLE_STATES.opened || this._visibleState === VISIBLE_STATES.opening) {
393
+ this._visibleState = VISIBLE_STATES.closing;
394
+ }
395
+ this._fireEvent(this.open ? 'flyout-opened' : 'flyout-closed');
396
+ }
397
+
398
+ _renderFlyoutContent() {
399
+ if (this._visibleState === VISIBLE_STATES.closed) {
400
+ return nothing;
401
+ }
402
+ return html`
403
+ <div id="flyout-content" role="dialog" aria-labelledby="title" aria-describedby="${this._getDescription()}" class="flyout-content">
404
+ <div class="flyout-text">
405
+ <h1 class="d2l-heading-1" id="title">${this.title}</h1>
406
+ ${this._renderShortDescription()}
407
+ ${this._renderLongDescription()}
408
+ ${this._renderLinksText()}
409
+ </div>
410
+ <div class="flyout-buttons">
411
+ <d2l-button id="primary-button" primary="" @click="${this._clickOptIn}">${this._getPrimaryButtonText()}</d2l-button>
412
+ <d2l-button id="opt-out-button" @click="${this._clickOptOut}">${this._getSecondaryButtonText()}</d2l-button>
413
+ </div>
414
+ </div>
415
+ `;
416
+ }
417
+
418
+ _renderLinksText() {
419
+ if (this._checkNumberOfLinks(1)) {
420
+ return html`
421
+ <div class="flyout-tutorial">
422
+ <span>${this._getTutorialTextPart(0)}</span>
423
+ <a id="tutorial-link-1" href="${this._getTutorialLink(0)}" target="_blank" rel="noopener">
424
+ ${this._getTutorialTextPart(1)}
425
+ </a>
426
+ <span>${this._getTutorialTextPart(2)}</span>
427
+ </div>
428
+ `;
429
+ } else if (this._checkNumberOfLinks(2)) {
430
+ return html`
431
+ <div class="flyout-tutorial">
432
+ <span>${this._getTutorialTextPart(0)}</span>
433
+ <a id="tutorial-link-2" href="${this._getTutorialLink(0)}" target="_blank" rel="noopener">
434
+ ${this._getTutorialTextPart(1)}
435
+ </a>
436
+ <span>${this._getTutorialTextPart(2)}</span>
437
+ <a href="${this._getTutorialLink(1)}" target="_blank" rel="noopener">
438
+ ${this._getTutorialTextPart(3)}
439
+ </a>
440
+ <span>${this._getTutorialTextPart(4)}</span>
441
+ </div>
442
+ `;
443
+ }
444
+ }
445
+
446
+ _renderLongDescription() {
447
+ if (!this.longDescription) {
448
+ return nothing;
449
+ }
450
+ return html`
451
+ <div id="long-description">
452
+ <d2l-html-block html="${this.longDescription}"></d2l-html-block>
453
+ </div>
454
+ `;
455
+ }
456
+
457
+ _renderOptInDialog() {
458
+ if (!this._optOutDialogOpen) {
459
+ return nothing;
460
+ }
461
+ return html`
462
+ <d2l-labs-opt-out-dialog
463
+ @cancel="${this._cancelOptOut}"
464
+ @confirm="${this._confirmOptOut}"
465
+ ?hide-reason="${this.hideReason}"
466
+ ?hide-feedback="${this.hideFeedback}">
467
+ <slot></slot>
468
+ </d2l-labs-opt-out-dialog>
469
+ `;
470
+ }
471
+
472
+ _renderShortDescription() {
473
+ if (!this.shortDescription) {
474
+ return nothing;
475
+ }
476
+ return html`
477
+ <div id="short-description">
478
+ <d2l-html-block html="${this.shortDescription}"></d2l-html-block>
479
+ </div>
480
+ `;
481
+ }
482
+
483
+ _shiftToFirst() {
484
+ if (this.tutorialLink && this.helpDocsLink) {
485
+ this.shadowRoot.querySelector('#tutorial-link-2').focus();
486
+ } else if (this.tutorialLink || this.helpDocsLink) {
487
+ this.shadowRoot.querySelector('#tutorial-link-1').focus();
488
+ } else {
489
+ this.shadowRoot.querySelector('#primary-button').focus();
490
+ }
491
+ }
492
+
493
+ _shiftToLast() {
494
+ if (this.open) {
495
+ this.shadowRoot.querySelector('#flyout-tab').focus();
496
+ }
497
+ }
498
+
499
+ }
500
+
501
+ customElements.define('d2l-labs-opt-in-flyout-impl', FlyoutImplementation);
@@ -0,0 +1,59 @@
1
+ import '@brightspace-ui/core/components/typography/typography.js';
2
+ import './flyout-impl.js';
3
+ import { css, html, LitElement } from 'lit';
4
+
5
+ class OptInFlyout extends LitElement {
6
+
7
+ static get properties() {
8
+ return {
9
+ open: { type: Boolean, reflect: true },
10
+ title: { type: String },
11
+ shortDescription: { type: String, attribute: 'short-description' },
12
+ longDescription: { type: String, attribute: 'long-description' },
13
+ tabPosition: { type: String, attribute: 'tab-position' },
14
+ tutorialLink: { type: String, attribute: 'tutorial-link' },
15
+ helpDocsLink: { type: String, attribute: 'help-docs-link' }
16
+ };
17
+ }
18
+
19
+ static get styles() {
20
+ return css`
21
+ d2l-labs-opt-in-flyout-impl {
22
+ font-size: 20px;
23
+ }
24
+ `;
25
+ }
26
+
27
+ constructor() {
28
+ super();
29
+ this.open = false;
30
+ }
31
+
32
+ render() {
33
+ return html`
34
+ <d2l-labs-opt-in-flyout-impl
35
+ class="d2l-typography"
36
+ title="${this.title}"
37
+ short-description="${this.shortDescription}"
38
+ long-description="${this.longDescription}"
39
+ tab-position="${this.tabPosition}"
40
+ tutorial-link="${this.tutorialLink}"
41
+ help-docs-link="${this.helpDocsLink}"
42
+ ?open="${this.open}"
43
+ @flyout-opened="${this._handleOpened}"
44
+ @flyout-closed="${this._handleClosed}">
45
+ </d2l-labs-opt-in-flyout-impl>
46
+ `;
47
+ }
48
+
49
+ _handleClosed() {
50
+ this.open = false;
51
+ }
52
+
53
+ _handleOpened() {
54
+ this.open = true;
55
+ }
56
+
57
+ }
58
+
59
+ customElements.define('d2l-labs-opt-in-flyout', OptInFlyout);