@carbon/ibm-products-web-components 0.1.0 → 0.1.1-canary.3605

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. package/.storybook/main.ts +15 -1
  2. package/.storybook/preview-head.html +5 -1
  3. package/.storybook/{theme.ts → theme.js} +2 -2
  4. package/es/components/side-panel/side-panel.d.ts +61 -80
  5. package/es/components/side-panel/side-panel.js +5 -3
  6. package/es/components/side-panel/side-panel.js.map +1 -1
  7. package/es/components/side-panel/side-panel.scss.js +1 -1
  8. package/es/components/tearsheet/defs.d.ts +26 -0
  9. package/es/components/tearsheet/defs.js +39 -0
  10. package/es/components/tearsheet/defs.js.map +1 -0
  11. package/es/components/tearsheet/index.d.ts +9 -0
  12. package/es/components/tearsheet/index.js +9 -0
  13. package/es/components/tearsheet/index.js.map +1 -0
  14. package/es/components/tearsheet/tearsheet.d.ts +490 -0
  15. package/es/components/tearsheet/tearsheet.js +685 -0
  16. package/es/components/tearsheet/tearsheet.js.map +1 -0
  17. package/es/components/tearsheet/tearsheet.scss.js +13 -0
  18. package/es/components/tearsheet/tearsheet.scss.js.map +1 -0
  19. package/es/components/tearsheet/tearsheet.test.d.ts +7 -0
  20. package/es/components/tearsheet/tearsheet.test.js +122 -0
  21. package/es/components/tearsheet/tearsheet.test.js.map +1 -0
  22. package/es/index.d.ts +1 -0
  23. package/es/index.js +1 -0
  24. package/es/index.js.map +1 -1
  25. package/lib/components/side-panel/side-panel.d.ts +61 -80
  26. package/lib/components/tearsheet/defs.d.ts +26 -0
  27. package/lib/components/tearsheet/defs.js +39 -0
  28. package/lib/components/tearsheet/defs.js.map +1 -0
  29. package/lib/components/tearsheet/index.d.ts +9 -0
  30. package/lib/components/tearsheet/tearsheet.d.ts +490 -0
  31. package/lib/components/tearsheet/tearsheet.test.d.ts +7 -0
  32. package/lib/index.d.ts +1 -0
  33. package/package.json +17 -17
  34. package/scss/components/tearsheet/story-styles.scss +23 -0
  35. package/scss/components/tearsheet/tearsheet.scss +318 -0
  36. package/src/components/side-panel/side-panel.ts +5 -5
  37. package/src/components/tearsheet/defs.ts +30 -0
  38. package/src/components/tearsheet/index.ts +10 -0
  39. package/src/components/tearsheet/story-styles.scss +23 -0
  40. package/src/components/tearsheet/tearsheet.mdx +101 -0
  41. package/src/components/tearsheet/tearsheet.scss +318 -0
  42. package/src/components/tearsheet/tearsheet.stories.ts +703 -0
  43. package/src/components/tearsheet/tearsheet.test.ts +118 -0
  44. package/src/components/tearsheet/tearsheet.ts +792 -0
  45. package/src/index.ts +1 -0
  46. package/netlify.toml +0 -8
  47. /package/.storybook/{Preview.ts → preview.ts} +0 -0
@@ -0,0 +1,792 @@
1
+ /**
2
+ * @license
3
+ *
4
+ * Copyright IBM Corp. 2023, 2024
5
+ *
6
+ * This source code is licensed under the Apache-2.0 license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+
10
+ import { LitElement, html } from 'lit';
11
+ import {
12
+ property,
13
+ query,
14
+ queryAssignedElements,
15
+ state,
16
+ } from 'lit/decorators.js';
17
+ import { prefix, carbonPrefix } from '../../globals/settings';
18
+ import HostListener from '@carbon/web-components/es/globals/decorators/host-listener.js';
19
+ import HostListenerMixin from '@carbon/web-components/es/globals/mixins/host-listener.js';
20
+ import styles from './tearsheet.scss?lit';
21
+ import { selectorTabbable } from '../../globals/settings';
22
+ import { carbonElement as customElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js';
23
+ import '@carbon/web-components/es/components/button/index.js';
24
+ import '@carbon/web-components/es/components/layer/index.js';
25
+ import '@carbon/web-components/es/components/button/button-set-base.js';
26
+ import '@carbon/web-components/es/components/modal/index.js';
27
+ import {
28
+ TEARSHEET_INFLUENCER_PLACEMENT,
29
+ TEARSHEET_INFLUENCER_WIDTH,
30
+ TEARSHEET_WIDTH,
31
+ } from './defs';
32
+
33
+ export {
34
+ TEARSHEET_INFLUENCER_PLACEMENT,
35
+ TEARSHEET_INFLUENCER_WIDTH,
36
+ TEARSHEET_WIDTH,
37
+ };
38
+
39
+ const maxStackDepth = 3;
40
+ type StackHandler = (newDepth: number, newPosition: number) => void;
41
+ interface StackState {
42
+ open: StackHandler[];
43
+ all: StackHandler[];
44
+ }
45
+
46
+ // eslint-disable-next-line no-bitwise
47
+ const PRECEDING =
48
+ Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS;
49
+ // eslint-disable-next-line no-bitwise
50
+ const FOLLOWING =
51
+ Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY;
52
+
53
+ const blockClass = `${prefix}--tearsheet`;
54
+ const blockClassModalHeader = `${carbonPrefix}--modal-header`;
55
+ const blockClassActionSet = `${prefix}--action-set`;
56
+
57
+ /**
58
+ * Tries to focus on the given elements and bails out if one of them is successful.
59
+ *
60
+ * @param elements The elements.
61
+ * @param reverse `true` to go through the list in reverse order.
62
+ * @returns `true` if one of the attempts is successful, `false` otherwise.
63
+ */
64
+ function tryFocusElements(elements: NodeListOf<HTMLElement>, reverse: boolean) {
65
+ if (!reverse) {
66
+ for (let i = 0; i < elements.length; ++i) {
67
+ const elem = elements[i];
68
+ elem.focus();
69
+ if (elem.ownerDocument!.activeElement === elem) {
70
+ return true;
71
+ }
72
+ }
73
+ } else {
74
+ for (let i = elements.length - 1; i >= 0; --i) {
75
+ const elem = elements[i];
76
+ elem.focus();
77
+ if (elem.ownerDocument!.activeElement === elem) {
78
+ return true;
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Tearsheet.
87
+ *
88
+ * @element c4p-tearsheet
89
+ * @csspart dialog The dialog.
90
+ * @fires c4p-tearsheet-beingclosed
91
+ * The custom event fired before this tearsheet is being closed upon a user gesture.
92
+ * Cancellation of this event stops the user-initiated action of closing this tearsheet.
93
+ * @fires c4p-tearsheet-closed - The custom event fired after this tearsheet is closed upon a user gesture.
94
+ */
95
+ @customElement(`${prefix}-tearsheet`)
96
+ class CDSTearsheet extends HostListenerMixin(LitElement) {
97
+ /**
98
+ * The element that had focus before this tearsheet gets open.
99
+ */
100
+ private _launcher: Element | null = null;
101
+
102
+ /**
103
+ * Node to track focus going outside of tearsheet content.
104
+ */
105
+ @query('#start-sentinel')
106
+ private _startSentinelNode!: HTMLAnchorElement;
107
+
108
+ /**
109
+ * Node to track focus going outside of tearsheet content.
110
+ */
111
+ @query('#end-sentinel')
112
+ private _endSentinelNode!: HTMLAnchorElement;
113
+
114
+ /**
115
+ * Node to track tearsheet.
116
+ */
117
+ @query(`.${blockClass}__container`)
118
+ private _tearsheet!: HTMLDivElement;
119
+
120
+ @queryAssignedElements({
121
+ slot: 'actions',
122
+ selector: `${carbonPrefix}-button`,
123
+ })
124
+ private _actions!: Array<HTMLElement>;
125
+
126
+ @state()
127
+ _actionsCount = 0;
128
+
129
+ @state()
130
+ _hasHeaderActions = false;
131
+
132
+ @state()
133
+ _hasLabel = false;
134
+
135
+ @state()
136
+ _hasSlug = false;
137
+
138
+ @state()
139
+ _hasTitle = false;
140
+
141
+ @state()
142
+ _hasDescription = false;
143
+
144
+ @state()
145
+ _hasInfluencerLeft = false;
146
+
147
+ @state()
148
+ _hasInfluencerRight = false;
149
+
150
+ @state()
151
+ _isOpen = false;
152
+
153
+ @state()
154
+ _hasHeaderNavigation = false;
155
+
156
+ /**
157
+ * Handles `click` event on this element.
158
+ *
159
+ * @param event The event.
160
+ */
161
+ @HostListener('click')
162
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
163
+ private _handleClick = (event: MouseEvent) => {
164
+ if (
165
+ event.composedPath().indexOf(this.shadowRoot!) < 0 &&
166
+ !this.preventCloseOnClickOutside
167
+ ) {
168
+ this._handleUserInitiatedClose(event.target);
169
+ }
170
+ };
171
+
172
+ /**
173
+ * Handles `blur` event on this element.
174
+ *
175
+ * @param event The event.
176
+ * @param event.target The event target.
177
+ * @param event.relatedTarget The event relatedTarget.
178
+ */
179
+ @HostListener('shadowRoot:focusout')
180
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
181
+ private _handleBlur = async ({ target, relatedTarget }: FocusEvent) => {
182
+ if (!this._topOfStack()) {
183
+ return;
184
+ }
185
+
186
+ const {
187
+ // condensedActions,
188
+ open,
189
+ _startSentinelNode: startSentinelNode,
190
+ _endSentinelNode: endSentinelNode,
191
+ } = this;
192
+
193
+ const oldContains = target !== this && this.contains(target as Node);
194
+ const currentContains =
195
+ relatedTarget !== this &&
196
+ (this.contains(relatedTarget as Node) ||
197
+ (this.shadowRoot?.contains(relatedTarget as Node) &&
198
+ relatedTarget !== (startSentinelNode as Node) &&
199
+ relatedTarget !== (endSentinelNode as Node)));
200
+
201
+ // Performs focus wrapping if _all_ of the following is met:
202
+ // * This tearsheet is open
203
+ // * The viewport still has focus
204
+ // * Tearsheet body used to have focus but no longer has focus
205
+ const { selectorTabbable: selectorTabbableForTearsheet } = this
206
+ .constructor as typeof CDSTearsheet;
207
+
208
+ if (open && relatedTarget && oldContains && !currentContains) {
209
+ const comparisonResult = (target as Node).compareDocumentPosition(
210
+ relatedTarget as Node
211
+ );
212
+ // eslint-disable-next-line no-bitwise
213
+ if (relatedTarget === startSentinelNode || comparisonResult & PRECEDING) {
214
+ await (this.constructor as typeof CDSTearsheet)._delay();
215
+ if (
216
+ !tryFocusElements(
217
+ this.querySelectorAll(selectorTabbableForTearsheet),
218
+ true
219
+ ) &&
220
+ relatedTarget !== this
221
+ ) {
222
+ this.focus();
223
+ }
224
+ }
225
+ // eslint-disable-next-line no-bitwise
226
+ else if (
227
+ relatedTarget === endSentinelNode ||
228
+ comparisonResult & FOLLOWING
229
+ ) {
230
+ await (this.constructor as typeof CDSTearsheet)._delay();
231
+ if (
232
+ !tryFocusElements(
233
+ this.querySelectorAll(selectorTabbableForTearsheet),
234
+ true
235
+ )
236
+ ) {
237
+ this.focus();
238
+ }
239
+ }
240
+ }
241
+ };
242
+
243
+ @HostListener('document:keydown')
244
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
245
+ private _handleKeydown = ({ key, target }: KeyboardEvent) => {
246
+ if ((key === 'Esc' || key === 'Escape') && this._topOfStack()) {
247
+ this._handleUserInitiatedClose(target);
248
+ }
249
+ };
250
+
251
+ private _checkSetHasSlot(e: Event) {
252
+ const t = e.target as HTMLSlotElement;
253
+ const dataPostfix = t.getAttribute('data-postfix');
254
+ const postfix = dataPostfix ? `-${dataPostfix}` : '';
255
+
256
+ // snake `ab-cd-ef` to _has camel case _hasAbCdEf
257
+ const hasName = `_has-${t.name}${postfix}`.replace(/-./g, (c) =>
258
+ c[1].toUpperCase()
259
+ );
260
+ this[hasName] = (t?.assignedElements()?.length ?? 0) > 0;
261
+ }
262
+
263
+ /**
264
+ * Handles `click` event on the modal container.
265
+ *
266
+ * @param event The event.
267
+ */
268
+ private _handleClickContainer(event: MouseEvent) {
269
+ if (
270
+ (event.target as Element).matches(
271
+ (this.constructor as typeof CDSTearsheet).selectorCloseButton
272
+ )
273
+ ) {
274
+ this._handleUserInitiatedClose(event.target);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Handles user-initiated close request of this tearsheet.
280
+ *
281
+ * @param triggeredBy The element that triggered this close request.
282
+ */
283
+ private _handleUserInitiatedClose(triggeredBy: EventTarget | null) {
284
+ if (this.open) {
285
+ const init = {
286
+ bubbles: true,
287
+ cancelable: true,
288
+ composed: true,
289
+ detail: {
290
+ triggeredBy,
291
+ },
292
+ };
293
+ if (
294
+ this.dispatchEvent(
295
+ new CustomEvent(
296
+ (this.constructor as typeof CDSTearsheet).eventBeforeClose,
297
+ init
298
+ )
299
+ )
300
+ ) {
301
+ this.open = false;
302
+ this.dispatchEvent(
303
+ new CustomEvent(
304
+ (this.constructor as typeof CDSTearsheet).eventClose,
305
+ init
306
+ )
307
+ );
308
+ }
309
+ }
310
+ }
311
+
312
+ private _handleSlugChange(e: Event) {
313
+ const childItems = (e.target as HTMLSlotElement).assignedElements();
314
+
315
+ this._hasSlug = childItems.length > 0;
316
+ if (this._hasSlug) {
317
+ childItems[0].setAttribute('size', 'lg');
318
+ this.setAttribute('slug', '');
319
+ } else {
320
+ this.removeAttribute('slug');
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Optional aria label for the tearsheet
326
+ */
327
+ @property({ reflect: true, attribute: 'aria-label' })
328
+ ariaLabel = '';
329
+
330
+ /**
331
+ * Sets the close button icon description
332
+ */
333
+ @property({ reflect: true, attribute: 'close-icon-description' })
334
+ closeIconDescription = 'Close';
335
+
336
+ /**
337
+ * Enable a close icon ('x') in the header area of the tearsheet. By default,
338
+ * (when this prop is omitted, or undefined or null) a tearsheet does not
339
+ * display a close icon if there are navigation actions ("transactional
340
+ * tearsheet") and displays one if there are no navigation actions ("passive
341
+ * tearsheet"), and that behavior can be overridden if required by setting
342
+ * this prop to either true or false.
343
+ */
344
+
345
+ @property({ reflect: true, type: Boolean, attribute: 'has-close-icon' })
346
+ hasCloseIcon = false;
347
+
348
+ /**
349
+ * The placement of the influencer section, 'left' or 'right'.
350
+ */
351
+ @property({ reflect: true, attribute: 'influencer-placement' })
352
+ influencerPlacement = TEARSHEET_INFLUENCER_PLACEMENT.RIGHT;
353
+
354
+ /**
355
+ * The width of the influencer section, 'narrow' or 'wide'.
356
+ */
357
+ @property({ reflect: true, attribute: 'influencer-width' })
358
+ influencerWidth = TEARSHEET_INFLUENCER_WIDTH.NARROW;
359
+
360
+ /**
361
+ * `true` if the tearsheet should be open.
362
+ */
363
+ @property({ type: Boolean, reflect: true })
364
+ open = false;
365
+
366
+ /**
367
+ * Prevent closing on click outside of tearsheet
368
+ */
369
+ @property({ type: Boolean, attribute: 'prevent-close-on-click-outside' })
370
+ preventCloseOnClickOutside = false;
371
+
372
+ /**
373
+ * The initial location of focus in the side panel
374
+ */
375
+ @property({
376
+ reflect: true,
377
+ attribute: 'selector-initial-focus',
378
+ type: String,
379
+ })
380
+ selectorInitialFocus;
381
+
382
+ /**
383
+ * The width of the influencer section, 'narrow' or 'wide'.
384
+ */
385
+ @property({ reflect: true, attribute: 'width' })
386
+ width = TEARSHEET_WIDTH.NARROW;
387
+
388
+ private _checkUpdateActionSizes = () => {
389
+ if (this._actions) {
390
+ for (let i = 0; i < this._actions.length; i++) {
391
+ this._actions[i].setAttribute(
392
+ 'size',
393
+ this.width === 'wide' ? '2xl' : 'xl'
394
+ );
395
+ }
396
+ }
397
+ };
398
+
399
+ private _maxActions = 4;
400
+ private _handleActionsChange(e: Event) {
401
+ const target = e.target as HTMLSlotElement;
402
+ const actions = target?.assignedElements();
403
+ const actionsCount = actions?.length ?? 0;
404
+
405
+ if (actionsCount > this._maxActions) {
406
+ this._actionsCount = this._maxActions;
407
+ console.error(`Too many tearsheet actions, max ${this._maxActions}.`);
408
+ } else {
409
+ this._actionsCount = actionsCount;
410
+ }
411
+
412
+ for (let i = 0; i < actions?.length; i++) {
413
+ if (i + 1 > this._maxActions) {
414
+ // hide excessive tearsheet actions
415
+ actions[i].setAttribute('hidden', 'true');
416
+ actions[i].setAttribute(
417
+ `data-actions-limit-${this._maxActions}-exceeded`,
418
+ `${actions.length}`
419
+ );
420
+ } else {
421
+ actions[i].classList.add(`${blockClassActionSet}__action-button`);
422
+ }
423
+ }
424
+ this._checkUpdateActionSizes();
425
+ }
426
+
427
+ // Data structure to communicate the state of tearsheet stacking
428
+ // (i.e. when more than one tearsheet is open). Each tearsheet supplies a
429
+ // handler to be called whenever the stacking of the tearsheets changes, which
430
+ // happens when a tearsheet opens or closes. The 'open' array contains one
431
+ // handler per OPEN tearsheet ordered from lowest to highest in visual z-order.
432
+ // The 'all' array contains all the handlers for open and closed tearsheets.
433
+
434
+ @state()
435
+ _stackDepth = -1;
436
+
437
+ @state()
438
+ _stackPosition = -1;
439
+
440
+ private _topOfStack = () => {
441
+ return this._stackDepth === this._stackPosition;
442
+ };
443
+
444
+ private static _stack: StackState = {
445
+ open: [],
446
+ all: [],
447
+ };
448
+ private _notifyStack = () => {
449
+ CDSTearsheet._stack.all.forEach(
450
+ (handler: (stackSize: number, position: number) => void) => {
451
+ handler(
452
+ Math.min(CDSTearsheet._stack.open.length, maxStackDepth),
453
+ CDSTearsheet._stack.open.indexOf(handler) + 1
454
+ );
455
+ }
456
+ );
457
+ };
458
+
459
+ private _handleStackChange: StackHandler = (newDepth, newPosition) => {
460
+ this._stackDepth = newDepth;
461
+ this._stackPosition = newPosition;
462
+ if (this._stackDepth > 1 && this._stackPosition > 0) {
463
+ this.setAttribute('stack-position', `${newPosition}`);
464
+ this.setAttribute('stack-depth', `${this._stackDepth}`);
465
+ } else {
466
+ this.removeAttribute('stack-position');
467
+ this.removeAttribute('stack-depth');
468
+ }
469
+ };
470
+
471
+ private _updateStack = () => {
472
+ if (this.open) {
473
+ CDSTearsheet._stack.open.push(this._handleStackChange);
474
+ } else {
475
+ const indexOpen = CDSTearsheet._stack.open.indexOf(
476
+ this._handleStackChange
477
+ );
478
+ if (indexOpen >= 0) {
479
+ CDSTearsheet._stack.open.splice(indexOpen, 1);
480
+ }
481
+ }
482
+ this._notifyStack();
483
+ };
484
+
485
+ actionsMultiple = ['', 'single', 'double', 'triple'][this._actionsCount];
486
+
487
+ connectedCallback() {
488
+ super.connectedCallback();
489
+
490
+ CDSTearsheet._stack.all.push(this._handleStackChange);
491
+ }
492
+
493
+ disconnectedCallback() {
494
+ super.disconnectedCallback();
495
+
496
+ const indexAll = CDSTearsheet._stack.all.indexOf(this._handleStackChange);
497
+ CDSTearsheet._stack.all.splice(indexAll, 1);
498
+ const indexOpen = CDSTearsheet._stack.all.indexOf(this._handleStackChange);
499
+ CDSTearsheet._stack.open.splice(indexOpen, 1);
500
+ }
501
+
502
+ render() {
503
+ const {
504
+ closeIconDescription,
505
+ influencerPlacement,
506
+ influencerWidth,
507
+ open,
508
+ width,
509
+ } = this;
510
+
511
+ const actionsMultiple = ['', 'single', 'double', 'triple'][
512
+ this._actionsCount
513
+ ];
514
+ const headerFieldsTemplate = html`<div
515
+ class=${`${blockClass}__header-fields`}
516
+ >
517
+ <h2 class=${`${blockClassModalHeader}__label`} ?hidden=${!this._hasLabel}>
518
+ <slot name="label" @slotchange=${this._checkSetHasSlot}></slot>
519
+ </h2>
520
+ <h3
521
+ class=${`${blockClassModalHeader}__heading ${blockClass}__heading`}
522
+ ?hidden=${!this._hasTitle}
523
+ >
524
+ <slot name="title" @slotchange=${this._checkSetHasSlot}></slot>
525
+ </h3>
526
+ <div
527
+ class=${`${blockClass}__header-description`}
528
+ ?hidden=${!this._hasDescription}
529
+ >
530
+ <slot name="description" @slotchange=${this._checkSetHasSlot}></slot>
531
+ </div>
532
+ </div>`;
533
+
534
+ const headerActionsTemplate = html` <div
535
+ class=${`${blockClass}__header-actions`}
536
+ ?hidden=${!this._hasHeaderActions || this.width === 'narrow'}
537
+ >
538
+ <slot name="header-actions" @slotchange=${this._checkSetHasSlot}></slot>
539
+ </div>`;
540
+
541
+ const headerTemplate = html` <cds-modal-header
542
+ class=${`${blockClass}__header`}
543
+ ?has-close-icon=${this.hasCloseIcon || this?._actionsCount === 0}
544
+ ?has-navigation=${this._hasHeaderNavigation && this.width === 'wide'}
545
+ ?has-header-actions=${this._hasHeaderActions && this.width === 'wide'}
546
+ ?has-actions=${this?._actionsCount > 0}
547
+ ?has-slug=${this?._hasSlug}
548
+ width=${width}
549
+ >
550
+ ${this.width === TEARSHEET_WIDTH.WIDE
551
+ ? html`<cds-layer level="1" class=${`${blockClass}__header-content`}
552
+ >${headerFieldsTemplate}${headerActionsTemplate}</cds-layer
553
+ >`
554
+ : html`<div>${headerFieldsTemplate}${headerActionsTemplate}</div>`}
555
+
556
+ <div
557
+ class=${`${blockClass}__header-navigation`}
558
+ ?hidden=${!this._hasHeaderNavigation || this.width === 'narrow'}
559
+ >
560
+ <slot
561
+ name="header-navigation"
562
+ @slotchange=${this._checkSetHasSlot}
563
+ ></slot>
564
+ </div>
565
+ <slot name="slug" @slotchange=${this._handleSlugChange}></slot>
566
+ ${this.hasCloseIcon || this?._actionsCount === 0
567
+ ? html`<cds-modal-close-button
568
+ close-button-label=${closeIconDescription}
569
+ @click=${this._handleUserInitiatedClose}
570
+ ></cds-modal-close-button>`
571
+ : ''}
572
+ </cds-modal-header>`;
573
+
574
+ return html`
575
+ <a
576
+ id="start-sentinel"
577
+ class="${prefix}--visually-hidden"
578
+ href="javascript:void 0"
579
+ role="navigation"
580
+ ></a>
581
+ <div
582
+ aria-label=${this.ariaLabel}
583
+ class=${`${blockClass}__container ${carbonPrefix}--modal-container ${carbonPrefix}--modal-container--sm`}
584
+ part="dialog"
585
+ role="complementary"
586
+ ?open=${this._isOpen}
587
+ ?opening=${open && !this._isOpen}
588
+ ?closing=${!open && this._isOpen}
589
+ width=${width}
590
+ stack-position=${this._stackPosition}
591
+ stack-depth=${this._stackDepth}
592
+ @click=${this._handleClickContainer}
593
+ >
594
+ <!-- Header -->
595
+ ${headerTemplate}
596
+
597
+ <!-- Body -->
598
+ <cds-modal-body class=${`${blockClass}__body`} width=${width}>
599
+ <!-- Influencer when on left -->
600
+ ${influencerPlacement !== TEARSHEET_INFLUENCER_PLACEMENT.RIGHT
601
+ ? html`<div
602
+ class=${`${blockClass}__influencer`}
603
+ ?wide=${influencerWidth === 'wide'}
604
+ ?hidden=${!this._hasInfluencerLeft ||
605
+ this.width === TEARSHEET_WIDTH.NARROW}
606
+ >
607
+ <slot
608
+ name="influencer"
609
+ data-postfix="left"
610
+ @slotchange=${this._checkSetHasSlot}
611
+ ></slot>
612
+ </div>`
613
+ : ''}
614
+
615
+ <div class=${`${blockClass}__right`}>
616
+ <div class=${`${blockClass}__main`}>
617
+ <div class=${`${blockClass}__content`}>
618
+ <cds-layer level="0">
619
+ <slot></slot>
620
+ </cds-layer>
621
+ </div>
622
+
623
+ <!-- Influencer when on right -->
624
+ ${influencerPlacement === TEARSHEET_INFLUENCER_PLACEMENT.RIGHT
625
+ ? html`<div
626
+ class=${`${blockClass}__influencer`}
627
+ ?wide=${influencerWidth}
628
+ ?hidden=${!this._hasInfluencerRight ||
629
+ this.width === TEARSHEET_WIDTH.NARROW}
630
+ >
631
+ <slot
632
+ name="influencer"
633
+ data-postfix="right"
634
+ @slotchange=${this._checkSetHasSlot}
635
+ ></slot>
636
+ </div>`
637
+ : ''}
638
+ </div>
639
+ <!-- Action buttons -->
640
+ <cds-button-set-base
641
+ class=${`${blockClass}__buttons ${blockClass}__button-container`}
642
+ actions-multiple=${actionsMultiple}
643
+ ?tearsheet-wide=${width === 'wide'}
644
+ ?hidden=${this._actionsCount === 0}
645
+ >
646
+ <slot
647
+ name="actions"
648
+ @slotchange=${this._handleActionsChange}
649
+ ></slot>
650
+ </cds-button-set-base>
651
+ </div>
652
+ </cds-modal-body>
653
+ </div>
654
+ <a
655
+ id="end-sentinel"
656
+ class="${prefix}--visually-hidden"
657
+ href="javascript:void 0"
658
+ role="navigation"
659
+ ></a>
660
+ `;
661
+ }
662
+
663
+ _checkSetOpen = () => {
664
+ const { _tearsheet: tearsheet } = this;
665
+ if (tearsheet && this._isOpen) {
666
+ // wait until the tearsheet has transitioned off the screen to remove
667
+ tearsheet.addEventListener('transitionend', () => {
668
+ this._isOpen = false;
669
+ });
670
+ } else {
671
+ // allow the html to render before animating in the tearsheet
672
+ window.requestAnimationFrame(() => {
673
+ this._isOpen = this.open;
674
+ });
675
+ }
676
+ };
677
+
678
+ async updated(changedProperties) {
679
+ if (changedProperties.has('width')) {
680
+ this._checkUpdateActionSizes();
681
+ }
682
+
683
+ if (
684
+ process.env.NODE_ENV === 'development' &&
685
+ (changedProperties.has('width') ||
686
+ changedProperties.has('_hasHeaderNavigation') ||
687
+ changedProperties.has('_hasInfluencerLeft') ||
688
+ changedProperties.has('_hasInfluencerRight') ||
689
+ changedProperties.has('_hasHeaderActions'))
690
+ ) {
691
+ if (this.width === 'narrow') {
692
+ if (this._hasHeaderNavigation) {
693
+ console.error(
694
+ `Header navigation is not permitted in narrow Tearsheet.`
695
+ );
696
+ }
697
+ if (this._hasInfluencerLeft || this._hasInfluencerRight) {
698
+ console.error(`Influencer is not permitted in narrow Tearsheet.`);
699
+ }
700
+ if (this._hasHeaderActions) {
701
+ console.error(
702
+ `Header actions are not permitted in narrow Tearsheet.`
703
+ );
704
+ }
705
+ }
706
+ }
707
+
708
+ if (changedProperties.has('open')) {
709
+ this._updateStack();
710
+
711
+ this._checkSetOpen();
712
+ if (this.open) {
713
+ this._launcher = this.ownerDocument!.activeElement;
714
+ const focusNode =
715
+ this.selectorInitialFocus &&
716
+ this.querySelector(this.selectorInitialFocus);
717
+
718
+ await (this.constructor as typeof CDSTearsheet)._delay();
719
+ if (focusNode) {
720
+ // For cases where a `carbon-web-components` component (e.g. `<cds-button>`) being `primaryFocusNode`,
721
+ // where its first update/render cycle that makes it focusable happens after `<c4p-tearsheet>`'s first update/render cycle
722
+ (focusNode as HTMLElement).focus();
723
+ } else if (
724
+ !tryFocusElements(
725
+ this.querySelectorAll(
726
+ (this.constructor as typeof CDSTearsheet).selectorTabbable
727
+ ),
728
+ true
729
+ )
730
+ ) {
731
+ this.focus();
732
+ }
733
+ } else if (
734
+ this._launcher &&
735
+ typeof (this._launcher as HTMLElement).focus === 'function'
736
+ ) {
737
+ (this._launcher as HTMLElement).focus();
738
+ this._launcher = null;
739
+ }
740
+ }
741
+ }
742
+
743
+ /**
744
+ * @param ms The number of milliseconds.
745
+ * @returns A promise that is resolves after the given milliseconds.
746
+ */
747
+ private static _delay(ms = 0) {
748
+ return new Promise((resolve) => {
749
+ setTimeout(resolve, ms);
750
+ });
751
+ }
752
+
753
+ /**
754
+ * A selector selecting buttons that should close this modal.
755
+ */
756
+ static get selectorCloseButton() {
757
+ return `[data-modal-close],${carbonPrefix}-modal-close-button`;
758
+ }
759
+
760
+ /**
761
+ * A selector selecting tabbable nodes.
762
+ */
763
+ static get selectorTabbable() {
764
+ return selectorTabbable;
765
+ }
766
+
767
+ /**
768
+ * The name of the custom event fired before this tearsheet is being closed upon a user gesture.
769
+ * Cancellation of this event stops the user-initiated action of closing this tearsheet.
770
+ */
771
+ static get eventBeforeClose() {
772
+ return `${prefix}-tearsheet-beingclosed`;
773
+ }
774
+
775
+ /**
776
+ * The name of the custom event fired after this tearsheet is closed upon a user gesture.
777
+ */
778
+ static get eventClose() {
779
+ return `${prefix}-tearsheet-closed`;
780
+ }
781
+
782
+ /**
783
+ * The name of the custom event fired on clicking the navigate back button
784
+ */
785
+ static get eventNavigateBack() {
786
+ return `${prefix}-tearsheet-header-navigate-back`;
787
+ }
788
+
789
+ static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
790
+ }
791
+
792
+ export default CDSTearsheet;