@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.
- package/.storybook/Preview.ts +161 -0
- package/.storybook/_container.scss +73 -0
- package/.storybook/container.ts +41 -0
- package/.storybook/main.ts +25 -0
- package/.storybook/manager.ts +13 -0
- package/.storybook/preview-head.html +3 -0
- package/.storybook/templates/with-layer.scss +38 -0
- package/.storybook/templates/with-layer.ts +90 -0
- package/.storybook/theme.ts +12 -0
- package/LICENSE +201 -0
- package/es/components/side-panel/defs.d.ts +39 -0
- package/es/components/side-panel/defs.js +51 -0
- package/es/components/side-panel/defs.js.map +1 -0
- package/es/components/side-panel/index.d.ts +9 -0
- package/es/components/side-panel/index.js +9 -0
- package/es/components/side-panel/index.js.map +1 -0
- package/es/components/side-panel/side-panel.d.ts +539 -0
- package/es/components/side-panel/side-panel.js +837 -0
- package/es/components/side-panel/side-panel.js.map +1 -0
- package/es/components/side-panel/side-panel.scss.js +13 -0
- package/es/components/side-panel/side-panel.scss.js.map +1 -0
- package/es/components/side-panel/side-panel.test.d.ts +7 -0
- package/es/components/side-panel/side-panel.test.js +56 -0
- package/es/components/side-panel/side-panel.test.js.map +1 -0
- package/es/globals/internal/handle.d.ts +18 -0
- package/es/globals/internal/handle.js +8 -0
- package/es/globals/internal/handle.js.map +1 -0
- package/es/globals/settings.d.ts +15 -0
- package/es/globals/settings.js +28 -0
- package/es/globals/settings.js.map +1 -0
- package/es/index.d.ts +9 -0
- package/es/index.js +9 -0
- package/es/index.js.map +1 -0
- package/lib/components/side-panel/defs.d.ts +39 -0
- package/lib/components/side-panel/defs.js +51 -0
- package/lib/components/side-panel/defs.js.map +1 -0
- package/lib/components/side-panel/index.d.ts +9 -0
- package/lib/components/side-panel/side-panel.d.ts +539 -0
- package/lib/components/side-panel/side-panel.test.d.ts +7 -0
- package/lib/globals/internal/handle.d.ts +18 -0
- package/lib/globals/settings.d.ts +15 -0
- package/lib/globals/settings.js +32 -0
- package/lib/globals/settings.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/netlify.toml +8 -0
- package/package.json +96 -0
- package/scss/components/side-panel/side-panel.scss +302 -0
- package/scss/components/side-panel/story-styles.scss +46 -0
- package/src/components/side-panel/defs.ts +46 -0
- package/src/components/side-panel/index.ts +10 -0
- package/src/components/side-panel/side-panel.mdx +106 -0
- package/src/components/side-panel/side-panel.scss +302 -0
- package/src/components/side-panel/side-panel.stories.ts +525 -0
- package/src/components/side-panel/side-panel.test.ts +52 -0
- package/src/components/side-panel/side-panel.ts +980 -0
- package/src/components/side-panel/story-styles.scss +46 -0
- package/src/globals/internal/handle.ts +19 -0
- package/src/globals/settings.ts +22 -0
- package/src/index.ts +10 -0
- package/src/typings/resources.d.ts +26 -0
- package/tasks/build.js +165 -0
- package/tools/rollup-plugin-icon-paths.js +39 -0
- package/tools/rollup-plugin-icons.js +73 -0
- package/tools/rollup-plugin-lit-scss.js +89 -0
- package/tools/svg-result-carbon-icon-loader.js +28 -0
- package/tools/svg-result-carbon-icon.js +42 -0
- package/tools/vite-svg-result-carbon-icon-loader.ts +65 -0
- package/tsconfig.json +36 -0
- 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;
|