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

Sign up to get free protection for your applications and to get access to all the features.
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;