@carbon/ibm-products-web-components 0.0.1-canary.3564

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.
Files changed (69) hide show
  1. package/.storybook/Preview.ts +161 -0
  2. package/.storybook/_container.scss +73 -0
  3. package/.storybook/container.ts +41 -0
  4. package/.storybook/main.ts +25 -0
  5. package/.storybook/manager.ts +13 -0
  6. package/.storybook/preview-head.html +3 -0
  7. package/.storybook/templates/with-layer.scss +38 -0
  8. package/.storybook/templates/with-layer.ts +90 -0
  9. package/.storybook/theme.ts +12 -0
  10. package/LICENSE +201 -0
  11. package/es/components/side-panel/defs.d.ts +39 -0
  12. package/es/components/side-panel/defs.js +51 -0
  13. package/es/components/side-panel/defs.js.map +1 -0
  14. package/es/components/side-panel/index.d.ts +9 -0
  15. package/es/components/side-panel/index.js +9 -0
  16. package/es/components/side-panel/index.js.map +1 -0
  17. package/es/components/side-panel/side-panel.d.ts +539 -0
  18. package/es/components/side-panel/side-panel.js +837 -0
  19. package/es/components/side-panel/side-panel.js.map +1 -0
  20. package/es/components/side-panel/side-panel.scss.js +13 -0
  21. package/es/components/side-panel/side-panel.scss.js.map +1 -0
  22. package/es/components/side-panel/side-panel.test.d.ts +7 -0
  23. package/es/components/side-panel/side-panel.test.js +56 -0
  24. package/es/components/side-panel/side-panel.test.js.map +1 -0
  25. package/es/globals/internal/handle.d.ts +18 -0
  26. package/es/globals/internal/handle.js +8 -0
  27. package/es/globals/internal/handle.js.map +1 -0
  28. package/es/globals/settings.d.ts +15 -0
  29. package/es/globals/settings.js +28 -0
  30. package/es/globals/settings.js.map +1 -0
  31. package/es/index.d.ts +9 -0
  32. package/es/index.js +9 -0
  33. package/es/index.js.map +1 -0
  34. package/lib/components/side-panel/defs.d.ts +39 -0
  35. package/lib/components/side-panel/defs.js +51 -0
  36. package/lib/components/side-panel/defs.js.map +1 -0
  37. package/lib/components/side-panel/index.d.ts +9 -0
  38. package/lib/components/side-panel/side-panel.d.ts +539 -0
  39. package/lib/components/side-panel/side-panel.test.d.ts +7 -0
  40. package/lib/globals/internal/handle.d.ts +18 -0
  41. package/lib/globals/settings.d.ts +15 -0
  42. package/lib/globals/settings.js +32 -0
  43. package/lib/globals/settings.js.map +1 -0
  44. package/lib/index.d.ts +9 -0
  45. package/netlify.toml +8 -0
  46. package/package.json +96 -0
  47. package/scss/components/side-panel/side-panel.scss +302 -0
  48. package/scss/components/side-panel/story-styles.scss +46 -0
  49. package/src/components/side-panel/defs.ts +46 -0
  50. package/src/components/side-panel/index.ts +10 -0
  51. package/src/components/side-panel/side-panel.mdx +106 -0
  52. package/src/components/side-panel/side-panel.scss +302 -0
  53. package/src/components/side-panel/side-panel.stories.ts +525 -0
  54. package/src/components/side-panel/side-panel.test.ts +52 -0
  55. package/src/components/side-panel/side-panel.ts +980 -0
  56. package/src/components/side-panel/story-styles.scss +46 -0
  57. package/src/globals/internal/handle.ts +19 -0
  58. package/src/globals/settings.ts +22 -0
  59. package/src/index.ts +10 -0
  60. package/src/typings/resources.d.ts +26 -0
  61. package/tasks/build.js +165 -0
  62. package/tools/rollup-plugin-icon-paths.js +39 -0
  63. package/tools/rollup-plugin-icons.js +73 -0
  64. package/tools/rollup-plugin-lit-scss.js +89 -0
  65. package/tools/svg-result-carbon-icon-loader.js +28 -0
  66. package/tools/svg-result-carbon-icon.js +42 -0
  67. package/tools/vite-svg-result-carbon-icon-loader.ts +65 -0
  68. package/tsconfig.json +36 -0
  69. package/vite.config.ts +32 -0
@@ -0,0 +1,980 @@
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 { SIDE_PANEL_SIZE, SIDE_PANEL_PLACEMENT } from './defs';
21
+ import styles from './side-panel.scss?lit';
22
+ import { selectorTabbable } from '@carbon/web-components/es/globals/settings.js';
23
+ import { carbonElement as customElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js';
24
+ import ArrowLeft16 from '@carbon/icons/lib/arrow--left/16';
25
+ import Close20 from '@carbon/icons/lib/close/20';
26
+ import { moderate02 } from '@carbon/motion';
27
+ import Handle from '../../globals/internal/handle';
28
+ import '@carbon/web-components/es/components/button/index.js';
29
+ import '@carbon/web-components/es/components/button/button-set-base.js';
30
+ import '@carbon/web-components/es/components/icon-button/index.js';
31
+ import '@carbon/web-components/es/components/layer/index.js';
32
+
33
+ export { SIDE_PANEL_SIZE, SIDE_PANEL_PLACEMENT };
34
+
35
+ const blockClass = `${prefix}--side-panel`;
36
+ const blockClassActionSet = `${prefix}--action-set`;
37
+
38
+ /**
39
+ * Observes resize of the given element with the given resize observer.
40
+ *
41
+ * @param observer The resize observer.
42
+ * @param elem The element to observe the resize.
43
+ */
44
+ const observeResize = (observer: ResizeObserver, elem: Element) => {
45
+ if (!elem) {
46
+ return null;
47
+ }
48
+ observer.observe(elem);
49
+ return {
50
+ release() {
51
+ observer.unobserve(elem);
52
+ return null;
53
+ },
54
+ } as Handle;
55
+ };
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
+ * SidePanel.
87
+ *
88
+ * @element cds-side-panel
89
+ * @csspart dialog The dialog.
90
+ * @fires cds-side-panel-beingclosed
91
+ * The custom event fired before this side-panel is being closed upon a user gesture.
92
+ * Cancellation of this event stops the user-initiated action of closing this side-panel.
93
+ * @fires cds-side-panel-closed - The custom event fired after this side-panel is closed upon a user gesture.
94
+ * @fires cds-side-panel-navigate-back - custom event fired when clicking navigate back (available when step > 0)
95
+ */
96
+ @customElement(`${prefix}-side-panel`)
97
+ class CDSSidePanel extends HostListenerMixin(LitElement) {
98
+ /**
99
+ * The handle for observing resize of the parent element of this element.
100
+ */
101
+ private _hObserveResize: Handle | null = null;
102
+
103
+ /**
104
+ * The element that had focus before this side-panel gets open.
105
+ */
106
+ private _launcher: Element | null = null;
107
+
108
+ /**
109
+ * Node to track focus going outside of side-panel content.
110
+ */
111
+ @query('#start-sentinel')
112
+ private _startSentinelNode!: HTMLAnchorElement;
113
+
114
+ /**
115
+ * Node to track focus going outside of side-panel content.
116
+ */
117
+ @query('#end-sentinel')
118
+ private _endSentinelNode!: HTMLAnchorElement;
119
+
120
+ /**
121
+ * Node to track side panel.
122
+ */
123
+ @query(`.${blockClass}`)
124
+ private _sidePanel!: HTMLDivElement;
125
+
126
+ @query(`.${blockClass}__animated-scroll-wrapper`)
127
+ private _animateScrollWrapper?: HTMLElement;
128
+
129
+ @query(`.${blockClass}__label-text`)
130
+ private _label!: HTMLElement;
131
+
132
+ @query(`.${blockClass}__title-text`)
133
+ private _title!: HTMLElement;
134
+
135
+ @query(`.${blockClass}__subtitle-text`)
136
+ private _subtitle!: HTMLElement;
137
+
138
+ @query(`.${blockClass}__inner-content`)
139
+ private _innerContent!: HTMLElement;
140
+
141
+ @queryAssignedElements({
142
+ slot: 'actions',
143
+ selector: `${carbonPrefix}-button`,
144
+ })
145
+ private _actions!: Array<HTMLElement>;
146
+
147
+ @state()
148
+ _doAnimateTitle = true;
149
+
150
+ @state()
151
+ _isOpen = false;
152
+
153
+ @state()
154
+ _containerScrollTop = -16;
155
+
156
+ @state()
157
+ _hasSubtitle = false;
158
+
159
+ @state()
160
+ _hasSlug = false;
161
+
162
+ @state()
163
+ _hasActionToolbar = false;
164
+
165
+ @state()
166
+ _actionsCount = 0;
167
+
168
+ @state()
169
+ _slugCloseSize = 'sm';
170
+
171
+ /**
172
+ * Handles `blur` event on this element.
173
+ *
174
+ * @param event The event.
175
+ * @param event.target The event target.
176
+ * @param event.relatedTarget The event relatedTarget.
177
+ */
178
+ @HostListener('shadowRoot:focusout')
179
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
180
+ private _handleBlur = async ({ target, relatedTarget }: FocusEvent) => {
181
+ const {
182
+ open,
183
+ _startSentinelNode: startSentinelNode,
184
+ _endSentinelNode: endSentinelNode,
185
+ } = this;
186
+
187
+ const oldContains = target !== this && this.contains(target as Node);
188
+ const currentContains =
189
+ relatedTarget !== this &&
190
+ (this.contains(relatedTarget as Node) ||
191
+ (this.shadowRoot?.contains(relatedTarget as Node) &&
192
+ relatedTarget !== (startSentinelNode as Node) &&
193
+ relatedTarget !== (endSentinelNode as Node)));
194
+
195
+ // Performs focus wrapping if _all_ of the following is met:
196
+ // * This side-panel is open
197
+ // * The viewport still has focus
198
+ // * SidePanel body used to have focus but no longer has focus
199
+ const { selectorTabbable: selectorTabbableForSidePanel } = this
200
+ .constructor as typeof CDSSidePanel;
201
+
202
+ if (open && relatedTarget && oldContains && !currentContains) {
203
+ const comparisonResult = (target as Node).compareDocumentPosition(
204
+ relatedTarget as Node
205
+ );
206
+ // eslint-disable-next-line no-bitwise
207
+ if (relatedTarget === startSentinelNode || comparisonResult) {
208
+ await (this.constructor as typeof CDSSidePanel)._delay();
209
+ if (
210
+ !tryFocusElements(
211
+ this.querySelectorAll(selectorTabbableForSidePanel),
212
+ true
213
+ ) &&
214
+ relatedTarget !== this
215
+ ) {
216
+ this.focus();
217
+ }
218
+ }
219
+ // eslint-disable-next-line no-bitwise
220
+ else if (relatedTarget === endSentinelNode || comparisonResult) {
221
+ await (this.constructor as typeof CDSSidePanel)._delay();
222
+ if (
223
+ !tryFocusElements(
224
+ this.querySelectorAll(selectorTabbableForSidePanel),
225
+ true
226
+ )
227
+ ) {
228
+ this.focus();
229
+ }
230
+ }
231
+ }
232
+ };
233
+
234
+ @HostListener('document:keydown')
235
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
236
+ private _handleKeydown = ({ key, target }: KeyboardEvent) => {
237
+ if (key === 'Esc' || key === 'Escape') {
238
+ this._handleUserInitiatedClose(target);
239
+ }
240
+ };
241
+
242
+ private _reducedMotion =
243
+ typeof window !== 'undefined' && window?.matchMedia
244
+ ? window.matchMedia('(prefers-reduced-motion: reduce)')
245
+ : { matches: true };
246
+
247
+ /**
248
+ * Handles `click` event on the side-panel container.
249
+ *
250
+ * @param event The event.
251
+ */
252
+ private _handleClickOnOverlay(event: MouseEvent) {
253
+ if (!this.preventCloseOnClickOutside) {
254
+ this._handleUserInitiatedClose(event.target);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Handles `click` event on the side-panel container.
260
+ *
261
+ * @param event The event.
262
+ */
263
+ private _handleCloseClick(event: MouseEvent) {
264
+ this._handleUserInitiatedClose(event.target);
265
+ }
266
+
267
+ /**
268
+ * Handles user-initiated close request of this side-panel.
269
+ *
270
+ * @param triggeredBy The element that triggered this close request.
271
+ */
272
+ private _handleUserInitiatedClose(triggeredBy: EventTarget | null) {
273
+ if (this.open) {
274
+ const init = {
275
+ bubbles: true,
276
+ cancelable: true,
277
+ composed: true,
278
+ detail: {
279
+ triggeredBy,
280
+ },
281
+ };
282
+ if (
283
+ this.dispatchEvent(
284
+ new CustomEvent(
285
+ (this.constructor as typeof CDSSidePanel).eventBeforeClose,
286
+ init
287
+ )
288
+ )
289
+ ) {
290
+ this.open = false;
291
+ this.dispatchEvent(
292
+ new CustomEvent(
293
+ (this.constructor as typeof CDSSidePanel).eventClose,
294
+ init
295
+ )
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ private _handleNavigateBack(triggeredBy: EventTarget | null) {
302
+ this.dispatchEvent(
303
+ new CustomEvent(
304
+ (this.constructor as typeof CDSSidePanel).eventNavigateBack,
305
+ {
306
+ composed: true,
307
+ detail: {
308
+ triggeredBy,
309
+ },
310
+ }
311
+ )
312
+ );
313
+ }
314
+
315
+ private _adjustPageContent = () => {
316
+ // sets/resets styles based on slideIn property and selectorPageContent;
317
+ if (this.selectorPageContent) {
318
+ const pageContentEl: HTMLElement | null = document.querySelector(
319
+ this.selectorPageContent
320
+ );
321
+
322
+ if (pageContentEl) {
323
+ const newValues = {
324
+ marginInlineStart: '',
325
+ marginInlineEnd: '',
326
+ inlineSize: '',
327
+ transition: this._reducedMotion.matches
328
+ ? 'none'
329
+ : `all ${moderate02}`,
330
+ transitionProperty: 'margin-inline-start, margin-inline-end',
331
+ };
332
+ if (this.open) {
333
+ newValues.inlineSize = 'auto';
334
+ if (this.placement === 'left') {
335
+ newValues.marginInlineStart = `${this?._sidePanel?.offsetWidth}px`;
336
+ } else {
337
+ newValues.marginInlineEnd = `${this?._sidePanel?.offsetWidth}px`;
338
+ }
339
+ }
340
+
341
+ Object.keys(newValues).forEach((key) => {
342
+ pageContentEl.style[key] = newValues[key];
343
+ });
344
+ }
345
+ }
346
+ };
347
+
348
+ private _checkSetOpen = () => {
349
+ const { _sidePanel: sidePanel } = this;
350
+ if (sidePanel && this._isOpen) {
351
+ if (this._reducedMotion) {
352
+ this._isOpen = false;
353
+ } else {
354
+ // wait until the side panel has transitioned off the screen to remove
355
+ sidePanel.addEventListener('transitionend', () => {
356
+ this._isOpen = false;
357
+ });
358
+ }
359
+ } else {
360
+ // allow the html to render before animating in the side panel
361
+ this._isOpen = this.open;
362
+ }
363
+ };
364
+
365
+ private _checkUpdateIconButtonSizes = () => {
366
+ const slug = this.querySelector(`${prefix}-slug`);
367
+ const otherButtons = this?.shadowRoot?.querySelectorAll(
368
+ '#nav-back-button, #close-button'
369
+ );
370
+
371
+ let iconButtonSize = 'sm';
372
+
373
+ if (slug || otherButtons?.length) {
374
+ const actions = this?.querySelectorAll?.(
375
+ `${prefix}-button[slot='actions']`
376
+ );
377
+
378
+ if (actions?.length && /l/.test(this.size)) {
379
+ iconButtonSize = 'md';
380
+ }
381
+ }
382
+
383
+ if (slug) {
384
+ slug?.setAttribute('size', iconButtonSize);
385
+ }
386
+
387
+ if (otherButtons) {
388
+ [...otherButtons].forEach((btn) => {
389
+ btn.setAttribute('size', iconButtonSize);
390
+ });
391
+ }
392
+ };
393
+
394
+ private _handleSlugChange(e: Event) {
395
+ this._checkUpdateIconButtonSizes();
396
+ const childItems = (e.target as HTMLSlotElement).assignedElements();
397
+
398
+ this._hasSlug = childItems.length > 0;
399
+ }
400
+
401
+ private _handleSubtitleChange(e: Event) {
402
+ const target = e.target as HTMLSlotElement;
403
+ const subtitle = target?.assignedElements();
404
+
405
+ this._hasSubtitle = subtitle.length > 0;
406
+ }
407
+
408
+ // eslint-disable-next-line class-methods-use-this
409
+ private _handleActionToolbarChange(e: Event) {
410
+ const target = e.target as HTMLSlotElement;
411
+ const toolbarActions = target?.assignedElements();
412
+
413
+ this._hasActionToolbar = toolbarActions && toolbarActions.length > 0;
414
+
415
+ if (this._hasActionToolbar) {
416
+ for (const toolbarAction of toolbarActions) {
417
+ // toolbar actions size should always be sm
418
+ toolbarAction.setAttribute('size', 'sm');
419
+ }
420
+ }
421
+ }
422
+
423
+ private _checkUpdateActionSizes = () => {
424
+ if (this._actions) {
425
+ for (let i = 0; i < this._actions.length; i++) {
426
+ this._actions[i].setAttribute(
427
+ 'size',
428
+ this.condensedActions ? 'lg' : 'xl'
429
+ );
430
+ }
431
+ }
432
+ };
433
+
434
+ private _maxActions = 3;
435
+ private _handleActionsChange(e: Event) {
436
+ const target = e.target as HTMLSlotElement;
437
+ const actions = target?.assignedElements();
438
+
439
+ // update slug size
440
+ this._checkUpdateIconButtonSizes();
441
+
442
+ const actionsCount = actions?.length ?? 0;
443
+ if (actionsCount > this._maxActions) {
444
+ this._actionsCount = this._maxActions;
445
+ if (process.env.NODE_ENV === 'development') {
446
+ console.error(`Too many side-panel actions, max ${this._maxActions}.`);
447
+ }
448
+ } else {
449
+ this._actionsCount = actionsCount;
450
+ }
451
+
452
+ for (let i = 0; i < actions?.length; i++) {
453
+ if (i + 1 > this._maxActions) {
454
+ // hide excessive side panel actions
455
+ actions[i].setAttribute('hidden', 'true');
456
+ actions[i].setAttribute(
457
+ `data-actions-limit-${this._maxActions}-exceeded`,
458
+ `${actions.length}`
459
+ );
460
+ } else {
461
+ actions[i].classList.add(`${blockClassActionSet}__action-button`);
462
+ }
463
+ }
464
+ this._checkUpdateActionSizes();
465
+ }
466
+
467
+ private _checkSetDoAnimateTitle = () => {
468
+ let canDoAnimateTitle = false;
469
+
470
+ if (
471
+ this._sidePanel &&
472
+ this.open &&
473
+ this.animateTitle &&
474
+ this?.title?.length &&
475
+ !this._reducedMotion.matches
476
+ ) {
477
+ const scrollAnimationDistance = this._getScrollAnimationDistance();
478
+ // used to calculate the header moves
479
+ this?._sidePanel?.style?.setProperty(
480
+ `--${blockClass}--scroll-animation-distance`,
481
+ `${scrollAnimationDistance}`
482
+ );
483
+
484
+ let scrollEl = this._animateScrollWrapper;
485
+ if (!scrollEl && this.animateTitle && !this._doAnimateTitle) {
486
+ scrollEl = this._innerContent;
487
+ }
488
+
489
+ if (scrollEl) {
490
+ const innerComputed = window?.getComputedStyle(this._innerContent);
491
+ const innerPaddingHeight = innerComputed
492
+ ? parseFloat(innerComputed?.paddingTop) +
493
+ parseFloat(innerComputed?.paddingBottom)
494
+ : 0;
495
+
496
+ canDoAnimateTitle =
497
+ (!!this.labelText || !!this._hasActionToolbar || this._hasSubtitle) &&
498
+ scrollEl.scrollHeight - scrollEl.clientHeight >=
499
+ scrollAnimationDistance + innerPaddingHeight;
500
+ }
501
+ }
502
+
503
+ this._doAnimateTitle = canDoAnimateTitle;
504
+ };
505
+
506
+ /**
507
+ * The `ResizeObserver` instance for observing element resizes for re-positioning floating menu position.
508
+ */
509
+ // TODO: Wait for `.d.ts` update to support `ResizeObserver`
510
+ // @ts-ignore
511
+ private _resizeObserver = new ResizeObserver(() => {
512
+ if (this._sidePanel) {
513
+ this._checkSetDoAnimateTitle();
514
+ }
515
+ });
516
+
517
+ private _getScrollAnimationDistance = () => {
518
+ const labelHeight = this?._label?.offsetHeight ?? 0;
519
+ const subtitleHeight = this?._subtitle?.offsetHeight ?? 0;
520
+ const titleVerticalBorder = this._hasActionToolbar
521
+ ? this._title.offsetHeight - this._title.clientHeight
522
+ : 0;
523
+
524
+ return labelHeight + subtitleHeight + titleVerticalBorder;
525
+ };
526
+
527
+ private _scrollObserver = () => {
528
+ const scrollTop = this._animateScrollWrapper?.scrollTop ?? 0;
529
+ const scrollAnimationDistance = this._getScrollAnimationDistance();
530
+ this?._sidePanel?.style?.setProperty(
531
+ `--${blockClass}--scroll-animation-progress`,
532
+ `${
533
+ Math.min(scrollTop, scrollAnimationDistance) / scrollAnimationDistance
534
+ }`
535
+ );
536
+ };
537
+
538
+ private _handleCurrentStepUpdate = () => {
539
+ const scrollable = this._animateScrollWrapper ?? this._innerContent;
540
+ if (scrollable) {
541
+ scrollable.scrollTop = 0;
542
+ }
543
+ };
544
+
545
+ /**
546
+ * Determines if the title will animate on scroll
547
+ */
548
+ @property({ reflect: true, attribute: 'animate-title', type: Boolean })
549
+ animateTitle = true;
550
+
551
+ /**
552
+ * Sets the close button icon description
553
+ */
554
+ @property({ reflect: true, attribute: 'close-icon-description' })
555
+ closeIconDescription = 'Close';
556
+
557
+ /**
558
+ * Determines whether the side panel should render the condensed version (affects action buttons primarily)
559
+ */
560
+ @property({ type: Boolean, reflect: true, attribute: 'condensed-actions' })
561
+ condensedActions = false;
562
+
563
+ /**
564
+ * Sets the current step of the side panel
565
+ */
566
+ @property({ reflect: true, attribute: 'current-step', type: Number })
567
+ currentStep;
568
+
569
+ /**
570
+ * Determines whether the side panel should render with an overlay
571
+ */
572
+ @property({ attribute: 'include-overlay', type: Boolean, reflect: true })
573
+ includeOverlay = false;
574
+
575
+ /**
576
+ * Sets the label text which will display above the title text
577
+ */
578
+ @property({ reflect: true, attribute: 'label-text' })
579
+ labelText;
580
+
581
+ /**
582
+ * Sets the icon description for the navigation back icon button
583
+ */
584
+ @property({ reflect: true, attribute: 'navigation-back-icon-description' })
585
+ navigationBackIconDescription = 'Back';
586
+
587
+ /**
588
+ * `true` if the side-panel should be open.
589
+ */
590
+ @property({ type: Boolean, reflect: true })
591
+ open = false;
592
+
593
+ /**
594
+ * SidePanel placement.
595
+ */
596
+ @property({ reflect: true, type: String })
597
+ placement = SIDE_PANEL_PLACEMENT.RIGHT;
598
+
599
+ /**
600
+ * Prevent closing on click outside of side-panel
601
+ */
602
+ @property({ type: Boolean, attribute: 'prevent-close-on-click-outside' })
603
+ preventCloseOnClickOutside = false;
604
+
605
+ /**
606
+ * The initial location of focus in the side panel
607
+ */
608
+ @property({
609
+ reflect: true,
610
+ attribute: 'selector-initial-focus',
611
+ type: String,
612
+ })
613
+ selectorInitialFocus;
614
+
615
+ /**
616
+ * Selector for page content, used to push content to side except
617
+ */
618
+ @property({ reflect: true, attribute: 'selector-page-content' })
619
+ selectorPageContent = '';
620
+
621
+ /**
622
+ * SidePanel size.
623
+ */
624
+ @property({ reflect: true, type: String })
625
+ size = SIDE_PANEL_SIZE.MEDIUM;
626
+
627
+ /**
628
+ * Determines if this panel slides in
629
+ */
630
+ @property({ attribute: 'slide-in', type: Boolean, reflect: true })
631
+ slideIn = false;
632
+
633
+ /**
634
+ * Sets the title text
635
+ */
636
+ @property({ reflect: false, type: String })
637
+ title;
638
+
639
+ async connectObservers() {
640
+ await this.updateComplete;
641
+ this._hObserveResize = observeResize(this._resizeObserver, this._sidePanel);
642
+ }
643
+
644
+ disconnectObservers() {
645
+ if (this._hObserveResize) {
646
+ this._hObserveResize = this._hObserveResize.release();
647
+ }
648
+ }
649
+
650
+ connectedCallback() {
651
+ super.connectedCallback();
652
+ this.disconnectObservers();
653
+ this.connectObservers();
654
+ }
655
+
656
+ disconnectedCallback() {
657
+ super.disconnectedCallback();
658
+ this.disconnectObservers();
659
+ }
660
+
661
+ render() {
662
+ const {
663
+ closeIconDescription,
664
+ condensedActions,
665
+ currentStep,
666
+ includeOverlay,
667
+ labelText,
668
+ navigationBackIconDescription,
669
+ open,
670
+ placement,
671
+ size,
672
+ slideIn,
673
+ title,
674
+ } = this;
675
+
676
+ if (!open && !this._isOpen) {
677
+ return html``;
678
+ }
679
+
680
+ const actionsMultiple = ['', 'single', 'double', 'triple'][
681
+ this._actionsCount
682
+ ];
683
+
684
+ const titleTemplate = html`<div
685
+ class=${`${blockClass}__title`}
686
+ ?no-label=${!!labelText}
687
+ >
688
+ <h2 class=${title ? `${blockClass}__title-text` : ''} title=${title}>
689
+ ${title}
690
+ </h2>
691
+
692
+ ${this._doAnimateTitle
693
+ ? html`<h2
694
+ class=${`${blockClass}__collapsed-title-text`}
695
+ title=${title}
696
+ aria-hidden="true"
697
+ >
698
+ ${title}
699
+ </h2>`
700
+ : ''}
701
+ </div>`;
702
+
703
+ const headerHasTitleClass = this.title
704
+ ? ` ${blockClass}__header--has-title `
705
+ : '';
706
+ const headerTemplate = html`
707
+ <div
708
+ class=${`${blockClass}__header${headerHasTitleClass}`}
709
+ ?detail-step=${currentStep > 0}
710
+ ?no-title-animation=${!this._doAnimateTitle}
711
+ ?reduced-motion=${this._reducedMotion.matches}
712
+ >
713
+ <!-- render back button -->
714
+ ${currentStep > 0
715
+ ? html`<cds-icon-button
716
+ align="bottom-left"
717
+ aria-label=${navigationBackIconDescription}
718
+ kind="ghost"
719
+ size="sm"
720
+ class=${`${prefix}--btn ${blockClass}__navigation-back-button`}
721
+ @click=${this._handleNavigateBack}
722
+ >
723
+ ${ArrowLeft16({ slot: 'icon' })}
724
+ <span slot="tooltip-content">
725
+ ${navigationBackIconDescription}
726
+ </span>
727
+ </cds-icon-button>`
728
+ : ''}
729
+
730
+ <!-- render title label -->
731
+ ${title?.length && labelText?.length
732
+ ? html` <p class=${`${blockClass}__label-text`}>${labelText}</p>`
733
+ : ''}
734
+
735
+ <!-- title -->
736
+ ${title ? titleTemplate : ''}
737
+
738
+ <!-- render slug and close button area -->
739
+ <div class=${`${blockClass}__slug-and-close`}>
740
+ <slot name="slug" @slotchange=${this._handleSlugChange}></slot>
741
+ <!-- {normalizedSlug} -->
742
+ <cds-icon-button
743
+ align="bottom-right"
744
+ aria-label=${closeIconDescription}
745
+ kind="ghost"
746
+ size="sm"
747
+ class=${`${blockClass}__close-button`}
748
+ @click=${this._handleCloseClick}
749
+ >
750
+ ${Close20({ slot: 'icon' })}
751
+ <span slot="tooltip-content"> ${closeIconDescription} </span>
752
+ </cds-icon-button>
753
+ </div>
754
+
755
+ <!-- render sub title -->
756
+ <p
757
+ class=${this._hasSubtitle ? `${blockClass}__subtitle-text` : ''}
758
+ ?hidden=${!this._hasSubtitle}
759
+ ?no-title-animation=${!this._doAnimateTitle}
760
+ ?no-action-toolbar=${!this._hasActionToolbar}
761
+ ?no-title=${!title}
762
+ >
763
+ <slot
764
+ name="subtitle"
765
+ @slotchange=${this._handleSubtitleChange}
766
+ ></slot>
767
+ </p>
768
+
769
+ <div
770
+ class=${this._hasActionToolbar ? `${blockClass}__action-toolbar` : ''}
771
+ ?hidden=${!this._hasActionToolbar}
772
+ ?no-title-animation=${!this._doAnimateTitle}
773
+ >
774
+ <slot
775
+ name="action-toolbar"
776
+ @slotchange=${this._handleActionToolbarChange}
777
+ ></slot>
778
+ </div>
779
+ </div>
780
+ `;
781
+
782
+ const mainTemplate = html`<div
783
+ class=${`${blockClass}__inner-content`}
784
+ ?scrolls=${!this._doAnimateTitle}
785
+ >
786
+ <cds-layer level="1">
787
+ <slot></slot>
788
+ </cds-layer>
789
+ </div> `;
790
+
791
+ const sidePanelAnimateTitleClass = this._doAnimateTitle
792
+ ? ` ${blockClass}--animated-title`
793
+ : '';
794
+
795
+ return html`
796
+ <div
797
+ class=${`${blockClass}${sidePanelAnimateTitleClass}`}
798
+ part="dialog"
799
+ role="complementary"
800
+ placement="${placement}"
801
+ ?has-slug=${this._hasSlug}
802
+ ?open=${this._isOpen}
803
+ ?opening=${open && !this._isOpen}
804
+ ?closing=${!open && this._isOpen}
805
+ ?condensed-actions=${condensedActions}
806
+ ?overlay=${includeOverlay || slideIn}
807
+ ?slide-in=${slideIn}
808
+ size=${size}
809
+ >
810
+ <a
811
+ id="start-sentinel"
812
+ class="sentinel"
813
+ hidden
814
+ href="javascript:void 0"
815
+ role="navigation"
816
+ ></a>
817
+
818
+ ${this._doAnimateTitle
819
+ ? html`<div class=${`${blockClass}__animated-scroll-wrapper`} scrolls>
820
+ ${headerTemplate} ${mainTemplate}
821
+ </div>`
822
+ : html` ${headerTemplate} ${mainTemplate}`}
823
+
824
+ <cds-button-set-base
825
+ class=${`${blockClass}__actions-container`}
826
+ ?hidden=${this._actionsCount === 0}
827
+ ?condensed=${condensedActions}
828
+ actions-multiple=${actionsMultiple}
829
+ size=${size}
830
+ >
831
+ <slot name="actions" @slotchange=${this._handleActionsChange}></slot>
832
+ </cds-button-set-base>
833
+
834
+ <a
835
+ id="end-sentinel"
836
+ class="sentinel"
837
+ hidden
838
+ href="javascript:void 0"
839
+ role="navigation"
840
+ ></a>
841
+ </div>
842
+
843
+ ${includeOverlay
844
+ ? html`<div
845
+ ?slide-in=${slideIn}
846
+ class=${`${blockClass}__overlay`}
847
+ ?open=${this.open}
848
+ ?opening=${open && !this._isOpen}
849
+ ?closing=${!open && this._isOpen}
850
+ tabindex="-1"
851
+ @click=${this._handleClickOnOverlay}
852
+ ></div>`
853
+ : ''}
854
+ `;
855
+ }
856
+
857
+ async updated(changedProperties) {
858
+ if (changedProperties.has('condensedActions')) {
859
+ this._checkUpdateActionSizes();
860
+ }
861
+
862
+ if (changedProperties.has('currentStep')) {
863
+ this._handleCurrentStepUpdate();
864
+ }
865
+
866
+ if (changedProperties.has('_doAnimateTitle')) {
867
+ this?._animateScrollWrapper?.removeEventListener(
868
+ 'scroll',
869
+ this._scrollObserver
870
+ );
871
+
872
+ if (this._doAnimateTitle) {
873
+ this?._animateScrollWrapper?.addEventListener(
874
+ 'scroll',
875
+ this._scrollObserver
876
+ );
877
+ } else {
878
+ this?._sidePanel?.style?.setProperty(
879
+ `--${blockClass}--scroll-animation-progress`,
880
+ '0'
881
+ );
882
+ }
883
+ }
884
+
885
+ if (
886
+ changedProperties.has('_isOpen') ||
887
+ changedProperties.has('animateTitle')
888
+ ) {
889
+ /* @state property changed */
890
+ this._checkSetDoAnimateTitle();
891
+ }
892
+
893
+ if (
894
+ changedProperties.has('slideIn') ||
895
+ changedProperties.has('open') ||
896
+ changedProperties.has('includeOverlay')
897
+ ) {
898
+ this._adjustPageContent();
899
+ }
900
+
901
+ if (changedProperties.has('open')) {
902
+ this._checkSetOpen();
903
+
904
+ this.disconnectObservers();
905
+ if (this.open) {
906
+ this.connectObservers();
907
+
908
+ this._launcher = this.ownerDocument!.activeElement;
909
+ const focusNode =
910
+ this.selectorInitialFocus &&
911
+ this.querySelector(this.selectorInitialFocus);
912
+
913
+ await (this.constructor as typeof CDSSidePanel)._delay();
914
+ if (focusNode) {
915
+ // For cases where a `carbon-web-components` component (e.g. `<cds-button>`) being `primaryFocusNode`,
916
+ // where its first update/render cycle that makes it focusable happens after `<cds-side-panel>`'s first update/render cycle
917
+ (focusNode as HTMLElement).focus();
918
+ } else if (
919
+ !tryFocusElements(
920
+ this.querySelectorAll(
921
+ (this.constructor as typeof CDSSidePanel).selectorTabbable
922
+ ),
923
+ true
924
+ )
925
+ ) {
926
+ this.focus();
927
+ }
928
+ } else if (
929
+ this._launcher &&
930
+ typeof (this._launcher as HTMLElement).focus === 'function'
931
+ ) {
932
+ (this._launcher as HTMLElement).focus();
933
+ this._launcher = null;
934
+ }
935
+ }
936
+ }
937
+
938
+ /**
939
+ * @param ms The number of milliseconds.
940
+ * @returns A promise that is resolves after the given milliseconds.
941
+ */
942
+ private static _delay(ms = 0) {
943
+ return new Promise((resolve) => {
944
+ setTimeout(resolve, ms);
945
+ });
946
+ }
947
+
948
+ /**
949
+ * A selector selecting tabbable nodes.
950
+ */
951
+ static get selectorTabbable() {
952
+ return selectorTabbable;
953
+ }
954
+
955
+ /**
956
+ * The name of the custom event fired before this side-panel is being closed upon a user gesture.
957
+ * Cancellation of this event stops the user-initiated action of closing this side-panel.
958
+ */
959
+ static get eventBeforeClose() {
960
+ return `${prefix}-side-panel-beingclosed`;
961
+ }
962
+
963
+ /**
964
+ * The name of the custom event fired after this side-panel is closed upon a user gesture.
965
+ */
966
+ static get eventClose() {
967
+ return `${prefix}-side-panel-closed`;
968
+ }
969
+
970
+ /**
971
+ * The name of the custom event fired on clicking the navigate back button
972
+ */
973
+ static get eventNavigateBack() {
974
+ return `${prefix}-side-panel-navigate-back`;
975
+ }
976
+
977
+ static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
978
+ }
979
+
980
+ export default CDSSidePanel;