diff --git a/.yarn/cache/@carbon-ibm-products-styles-npm-2.20.1-1f210e0807-a069cae6b7.zip b/.yarn/cache/@carbon-ibm-products-styles-npm-2.20.1-1f210e0807-a069cae6b7.zip
new file mode 100644
index 00000000000..2f402d6b5a4
Binary files /dev/null and b/.yarn/cache/@carbon-ibm-products-styles-npm-2.20.1-1f210e0807-a069cae6b7.zip differ
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.babelrc b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.babelrc
new file mode 100644
index 00000000000..74450eed94b
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.babelrc
@@ -0,0 +1,22 @@
+{
+ "presets": [
+ [
+ "@babel/preset-env",
+ {
+ "modules": false,
+ "targets": [
+ "last 1 version",
+ "Firefox ESR",
+ "not opera > 0",
+ "not op_mini > 0",
+ "not op_mob > 0",
+ "not android > 0",
+ "not edge > 0",
+ "not ie > 0",
+ "not ie_mob > 0"
+ ]
+ }
+ ]
+ ],
+ "plugins": [["@babel/plugin-transform-runtime", { "version": "7.3.0" }]]
+}
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.gitignore b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.gitignore
new file mode 100644
index 00000000000..d94d6e13e94
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.gitignore
@@ -0,0 +1,22 @@
+# See https://help.github.com/ignore-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.cache
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.sassrc b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.sassrc
new file mode 100644
index 00000000000..956b9e0a3d8
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/.sassrc
@@ -0,0 +1,6 @@
+{
+ "includePaths": [
+ "node_modules",
+ "../../node_modules"
+ ]
+}
\ No newline at end of file
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/cdn.html b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/cdn.html
new file mode 100644
index 00000000000..22072368242
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/cdn.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+ @carbon/ibmdotcom-web-components example
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Side panel content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Subtitle text which can provide more detail on the content being displayed.
+
+
+ Copy
+
+ ${Settings({ slot: 'icon' })}
+
+
+ ${Trashcan({ slot: 'icon' })}
+
+
+
+ Ghost
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/index.html b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/index.html
new file mode 100644
index 00000000000..c9861dc47b0
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ carbon-web-components example
+
+
+
+
+
+
+
+
+
+
+
+ Side panel content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Subtitle text which can provide more detail on the content being displayed.
+
+
+ Copy
+
+ ${Settings({ slot: 'icon' })}
+
+
+ ${Trashcan({ slot: 'icon' })}
+
+
+
+ Ghost
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/package.json b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/package.json
new file mode 100644
index 00000000000..62007e6f09c
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "carbon-web-components-side-panel-example",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Sample project for getting started with the Web Components from Carbon.",
+ "license": "Apache-2",
+ "main": "index.html",
+ "scripts": {
+ "build": "parcel build *.html --no-minify --public-url ./",
+ "clean": "rimraf node_modules dist .cache",
+ "start": "parcel index.html --port=9000 --no-hmr"
+ },
+ "dependencies": {
+ "@carbon/styles": "^1.34.0",
+ "@carbon/web-components": "latest",
+ "sass": "^1.64.1"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.0.0-0",
+ "parcel-bundler": "1.12.3",
+ "rimraf": "^3.0.2"
+ }
+}
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/sandbox.config.json b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/sandbox.config.json
new file mode 100644
index 00000000000..a4df8557d7b
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/sandbox.config.json
@@ -0,0 +1,3 @@
+{
+ "template": "node"
+}
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/index.js b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/index.js
new file mode 100644
index 00000000000..0f30db202da
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/index.js
@@ -0,0 +1,13 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2020, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import '@carbon/web-components/es/components/text-input/index.js';
+import '@carbon/web-components/es/components/textarea/index.js';
+import '@carbon/web-components/es/components/button/index.js';
+import '@carbon/web-components/es/components/side-panel/index.js';
diff --git a/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/styles.scss b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/styles.scss
new file mode 100644
index 00000000000..5bde587c9b5
--- /dev/null
+++ b/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel/src/styles.scss
@@ -0,0 +1,9 @@
+@use '@carbon/styles/scss/reset';
+@use '@carbon/styles/scss/theme';
+@use '@carbon/styles/scss/themes';
+
+:root {
+ @include theme.theme(themes.$white);
+ background-color: var(--cds-background);
+ color: var(--cds-text-primary);
+}
diff --git a/packages/carbon-web-components/package.json b/packages/carbon-web-components/package.json
index a1a76d993ce..9b46abe368a 100644
--- a/packages/carbon-web-components/package.json
+++ b/packages/carbon-web-components/package.json
@@ -68,6 +68,7 @@
],
"dependencies": {
"@babel/runtime": "^7.16.3",
+ "@carbon/ibm-products-styles": "2.20.1",
"@carbon/styles": "1.48.1",
"flatpickr": "4.6.1",
"lit": "^2.7.6",
diff --git a/packages/carbon-web-components/src/components/button/button-set.ts b/packages/carbon-web-components/src/components/button/button-set.ts
index b12565aa649..3c5b562df9e 100644
--- a/packages/carbon-web-components/src/components/button/button-set.ts
+++ b/packages/carbon-web-components/src/components/button/button-set.ts
@@ -1,7 +1,7 @@
/**
* @license
*
- * Copyright IBM Corp. 2020, 2023
+ * Copyright IBM Corp. 2020, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
@@ -25,7 +25,7 @@ class CDSButtonSet extends LitElement {
*
* @private
*/
- private _handleSlotChange(event: Event) {
+ protected _handleSlotChange(event: Event) {
const childItems = (event.target as HTMLSlotElement)
.assignedNodes()
.filter((elem) =>
diff --git a/packages/carbon-web-components/src/components/button/button.scss b/packages/carbon-web-components/src/components/button/button.scss
index ee15ec37b2f..22005322b98 100644
--- a/packages/carbon-web-components/src/components/button/button.scss
+++ b/packages/carbon-web-components/src/components/button/button.scss
@@ -1,5 +1,5 @@
//
-// Copyright IBM Corp. 2019, 2023
+// Copyright IBM Corp. 2019, 2024
//
// This source code is licensed under the Apache-2.0 license found in the
// LICENSE file in the root directory of this source tree.
@@ -103,7 +103,8 @@ $css--plex: true !default;
outline: none;
}
-:host(#{$prefix}-button-set) {
+:host(#{$prefix}-button-set),
+:host(#{$prefix}-side-panel-button-set) {
@extend .#{$prefix}--btn-set;
::slotted(#{$prefix}-button) {
diff --git a/packages/carbon-web-components/src/components/side-panel/defs.ts b/packages/carbon-web-components/src/components/side-panel/defs.ts
new file mode 100644
index 00000000000..bacc45894bb
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/defs.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2023, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * Modal size.
+ */
+export enum SIDE_PANEL_SIZE {
+ /**
+ * Extra small size.
+ */
+ EXTRA_SMALL = 'xs',
+
+ /**
+ * Small size.
+ */
+ SMALL = 'sm',
+
+ /**
+ * Medium size.
+ */
+ MEDIUM = 'md',
+
+ /**
+ * Large size.
+ */
+ LARGE = 'lg',
+
+ /**
+ * 2X-Large size.
+ */
+ EXTRA_EXTRA_LARGE = '2xl',
+}
+
+export enum SIDE_PANEL_PLACEMENT {
+ /** right / default */
+ RIGHT = 'right',
+
+ /** left */
+ LEFT = 'left',
+}
diff --git a/packages/carbon-web-components/src/components/side-panel/index.ts b/packages/carbon-web-components/src/components/side-panel/index.ts
new file mode 100644
index 00000000000..0ad92359b73
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/index.ts
@@ -0,0 +1,10 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2023, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import './side-panel';
diff --git a/packages/carbon-web-components/src/components/side-panel/side-panel-button-set.ts b/packages/carbon-web-components/src/components/side-panel/side-panel-button-set.ts
new file mode 100644
index 00000000000..da67aa063e6
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/side-panel-button-set.ts
@@ -0,0 +1,28 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2020, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { customElement } from 'lit/decorators.js';
+import { prefix } from '../../globals/settings';
+import CDSButtonSet from '../button/button-set';
+
+/**
+ * Button set.
+ *
+ * @element cds-side-panel-button-set
+ */
+@customElement(`${prefix}-side-panel-button-set`)
+class CDSSidePanelButtonSet extends CDSButtonSet {
+ _handleSlotChange() {
+ // do not re-order button set
+ return;
+ }
+}
+
+/* @__GENERATE_REACT_CUSTOM_ELEMENT_TYPE__ */
+export default CDSSidePanelButtonSet;
diff --git a/packages/carbon-web-components/src/components/side-panel/side-panel-story.mdx b/packages/carbon-web-components/src/components/side-panel/side-panel-story.mdx
new file mode 100644
index 00000000000..7f37264b6cc
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/side-panel-story.mdx
@@ -0,0 +1,75 @@
+import { Props, Description } from '@storybook/addon-docs/blocks';
+import { cdnJs, cdnCss } from '../../globals/internal/storybook-cdn';
+
+# SidePanel
+
+> 💡 Check our
+> [CodeSandbox](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/main/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel)
+> example implementation.
+
+[![Edit carbon-web-components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/carbon-design-system/carbon-for-ibm-dotcom/tree/feat/main/packages/carbon-web-components/examples/codesandbox/basic/components/side-panel)
+
+Side panels keep users in-context of a page while performing tasks like navigating, editing, viewing details, or configuring something new.
+
+## Getting started
+
+Here's a quick example to get you started.
+
+### JS (via import)
+
+```javascript
+import '@carbon/web-components/es/components/side-panel/index.js';
+// The following are used for slotted fields
+import '@carbon/web-components/es/components/text-input/index.js';
+import '@carbon/web-components/es/components/textarea/index.js';
+import '@carbon/web-components/es/components/button/index.js';
+```
+
+
+
+
+### HTML
+
+```html
+
+
+ Side panel content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Subtitle text which can provide more detail on the content being displayed.
+
+
+ Copy
+
+ ${Settings({ slot: 'icon' })}
+
+
+ ${Trashcan({ slot: 'icon' })}
+
+
+
+ Ghost
+
+
+```
+
+## `` attributes, properties and events
+
+Note: For `boolean` attributes, `true` means simply setting the attribute (e.g.
+``) and `false` means not setting the attribute (e.g.
+`` without `open` attribute).
+
+
diff --git a/packages/carbon-web-components/src/components/side-panel/side-panel-story.ts b/packages/carbon-web-components/src/components/side-panel/side-panel-story.ts
new file mode 100644
index 00000000000..73e96082ded
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/side-panel-story.ts
@@ -0,0 +1,434 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2023, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { TemplateResult, html } from 'lit';
+import { boolean, select, text } from '@storybook/addon-knobs';
+import '../button/button';
+import { SIDE_PANEL_SIZE } from './side-panel';
+import './index';
+import '../text-input/index';
+import '../textarea/index';
+import storyDocs from './side-panel-story.mdx';
+import { SIDE_PANEL_PLACEMENT } from './defs';
+import Settings from '@carbon/icons/lib/settings/16';
+import Trashcan from '@carbon/icons/lib/trash-can/16';
+import { prefix } from '../../globals/settings';
+
+import styles from './story-styles.scss';
+import { BUTTON_KIND } from '../button/button';
+const toggleButton = () => {
+ document.querySelector(`${prefix}-side-panel`)?.toggleAttribute('open');
+};
+
+const sizes = {
+ // 'default (md)': null,
+ [`Extra small size (${SIDE_PANEL_SIZE.EXTRA_SMALL})`]:
+ SIDE_PANEL_SIZE.EXTRA_SMALL,
+ [`Small size (${SIDE_PANEL_SIZE.SMALL})`]: SIDE_PANEL_SIZE.SMALL,
+ [`Medium size (default) (${SIDE_PANEL_SIZE.MEDIUM})`]: SIDE_PANEL_SIZE.MEDIUM,
+ [`Large size (${SIDE_PANEL_SIZE.LARGE})`]: SIDE_PANEL_SIZE.LARGE,
+ [`Extra Extra Large size (${SIDE_PANEL_SIZE.EXTRA_EXTRA_LARGE})`]:
+ SIDE_PANEL_SIZE.EXTRA_EXTRA_LARGE,
+};
+
+const placements = {
+ // 'default (right)': null,
+ left: SIDE_PANEL_PLACEMENT.LEFT,
+ 'right (default)': SIDE_PANEL_PLACEMENT.RIGHT,
+};
+
+const contents = {
+ Empty: 0,
+ 'Brief content': 1,
+ 'Longer content': 2,
+};
+
+const storyPrefix = 'side-panel-stories__';
+
+const getContent = (index) => {
+ switch (index) {
+ case 1:
+ return html`
+
+ Section
+
+
+ `;
+ case 2:
+ return html`
+ Section
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+ default:
+ return null;
+ }
+};
+
+const labels = {
+ 'No label': 0,
+ 'Shorter label': 1,
+ 'Longer label': 2,
+};
+
+const getLabel = (index) => {
+ switch (index) {
+ case 1:
+ return 'A short label';
+ case 2:
+ return 'A longer label that might go on for a little bit';
+ default:
+ return '';
+ }
+};
+
+const subtitles = {
+ 'No subtitle': 0,
+ 'Short subtitle': 1,
+ 'Longer subtitle': 2,
+};
+const getSubTitle = (index) => {
+ switch (index) {
+ case 1:
+ return html`This is your subtitle slot.
`;
+ case 2:
+ return html`
+ I am your subtitle slot for adding detail that can be
+ one or two lines.
+
`;
+ default:
+ return null;
+ }
+};
+
+const actionToolbarItems = {
+ 'No action toolbar': 0,
+ 'With action toolbar': 1,
+};
+
+const getActionToolbarItems = (index) => {
+ switch (index) {
+ case 1:
+ return html`
+ Copy
+
+ ${Settings({ slot: 'icon' })}
+
+
+ ${Trashcan({ slot: 'icon' })}
+
+ `;
+ default:
+ return null;
+ }
+};
+
+const actionItems = {
+ 'No actions': 0,
+ 'One button': 1,
+ 'Two buttons with ghost': 2,
+ 'Two buttons with danger': 3,
+ 'Three buttons with ghost': 4,
+ 'Three buttons with danger': 5,
+};
+
+// TODO: There are problems switching this
+const getActionItems = (index) => {
+ switch (index) {
+ case 1:
+ return html`Primary`;
+ case 2:
+ return html`
+ Ghost
+ Primary
+ `;
+ case 3:
+ return html` Danger
+ Primary`;
+ case 4:
+ return html` Ghost
+ Secondary
+ Primary`;
+ case 5:
+ return html`Danger
+ Secondary
+ Primary`;
+ default:
+ return null;
+ }
+};
+
+const slugs = {
+ 'No Slug': 0,
+ 'With Slug': 1,
+};
+
+const getSlug = (index) => {
+ switch (index) {
+ case 1:
+ return html`
+
+
AI Explained
+
84%
+
Confidence score
+
+ Lorem ipsum dolor sit amet, di os consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut fsil labore et dolore magna aliqua.
+
+
+
Model type
+
Foundation model
+
+ `;
+ default:
+ return null;
+ }
+};
+
+export default {
+ title: 'Experimental/SidePanel',
+ decorators: [(story) => html` ${story()} `],
+ parameters: {
+ ...storyDocs.parameters,
+ },
+};
+
+const DefaultTemplate = (argsIn) => {
+ const args = {
+ actionItems: getActionItems(select('Slot (actions)', actionItems, 1)),
+ actionToolbarItems: getActionToolbarItems(
+ select('Slot (action-toolbar)', actionToolbarItems, 0)
+ ),
+ animateTitle: boolean('animate-title (Title animates on scroll)', true),
+ class: text('class', 'a-user-class'),
+ condensedActions: boolean('condensed-actions', false),
+ content: getContent(select('Slot (default), panel contents', contents, 2)),
+ includeOverlay: boolean('include-overlay', true),
+ label: getLabel(select('label', labels, 2)),
+ open: boolean('open', false),
+ placement: select('placement', placements, SIDE_PANEL_PLACEMENT.RIGHT),
+ preventCloseOnClickOutside: boolean(
+ 'prevent-close-on-click-outside',
+ false
+ ),
+ selectorPageContent: text(
+ 'selector-page-content',
+ '#page-content-selector'
+ ),
+ selectorInitialFocus: text('selector-initial-focus', ''),
+ size: select('size', sizes, SIDE_PANEL_SIZE.MEDIUM),
+ slideIn: boolean('slide-in', false),
+ slug: getSlug(select('slug (AI slug)', slugs, 0)),
+ subtitle: getSubTitle(select('Slot (subtitle)', subtitles, 1)),
+ title: text(
+ 'title',
+ 'This title is testing a very long title to see how this behaves with a longer title. It needs to be long enough to trigger overflow when collapsed.'
+ ),
+
+ ...(argsIn?.['cds-side-panel'] ?? {}),
+ };
+
+ return html`
+
+
+
+ Toggle side-panel
+
+
+
+
+ ${args.content}
+
+
+ ${args.subtitle}
+
+
+ ${args.actionToolbarItems}
+
+
+ ${args.actionItems}
+
+
+ ${args.slug}
+
+ `;
+};
+
+type TemplateType = {
+ (args: any): TemplateResult<1>;
+ parameters: { knobs: { [key: string]: any } };
+};
+
+export const SlideOver = DefaultTemplate.bind({}) as TemplateType;
+SlideOver.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({}),
+ },
+};
+
+export const SlideIn = DefaultTemplate.bind({}) as TemplateType;
+SlideIn.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ slideIn: boolean('slide-in', true),
+ }),
+ },
+};
+
+export const WithActionToolbar = DefaultTemplate.bind({}) as TemplateType;
+WithActionToolbar.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ actionToolbarItems: getActionToolbarItems(
+ select('Action toolbar slot', actionToolbarItems, 1)
+ ),
+ }),
+ },
+};
+
+export const SpecifyElementToHaveFocus = DefaultTemplate.bind(
+ {}
+) as TemplateType;
+SpecifyElementToHaveFocus.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ focusSelector: text(
+ 'selector-primary-focus',
+ '#side-panel-story-text-input-a'
+ ),
+ label: getLabel(select('label', labels, 0)),
+ }),
+ },
+};
+
+export const WithStaticTitle = DefaultTemplate.bind({}) as TemplateType;
+WithStaticTitle.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ animateTitle: boolean('animate-title (Title animates on scroll)', false),
+ label: getLabel(select('label', labels, 0)),
+ }),
+ },
+};
+
+export const WithStaticTitleAndActionToolbar = DefaultTemplate.bind(
+ {}
+) as TemplateType;
+WithStaticTitleAndActionToolbar.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ actionToolbarItems: getActionToolbarItems(
+ select('Action toolbar slot', actionToolbarItems, 1)
+ ),
+ animateTitle: boolean('animate-title (Title animates on scroll)', false),
+ label: getLabel(select('label', labels, 0)),
+ }),
+ },
+};
+
+export const WithoutTitle = DefaultTemplate.bind({}) as TemplateType;
+WithoutTitle.parameters = {
+ ...storyDocs.parameters,
+ knobs: {
+ 'cds-side-panel': () => ({
+ label: getLabel(select('label', labels, 0)),
+ title: text('title', ''),
+ }),
+ },
+};
diff --git a/packages/carbon-web-components/src/components/side-panel/side-panel.scss b/packages/carbon-web-components/src/components/side-panel/side-panel.scss
new file mode 100644
index 00000000000..a17bb65cd8b
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/side-panel.scss
@@ -0,0 +1,386 @@
+/*
+* Copyright IBM Corp. 2023, 2024
+*
+* This source code is licensed under the Apache-2.0 license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+$css--plex: true !default;
+
+/* Other Carbon settings. */
+@use '@carbon/styles/scss/reset';
+@use '@carbon/styles/scss/config' as *;
+@use '@carbon/styles/scss/motion' as *;
+@use '@carbon/styles/scss/spacing' as *;
+@use '@carbon/styles/scss/theme' as *;
+@use '@carbon/styles/scss/utilities/ai-gradient' as *;
+@use 'sass:map';
+
+$pkg-prefix: 'cds';
+
+@use '@carbon/ibm-products-styles/scss/config' with (
+ $pkg-prefix: #{$pkg-prefix}
+);
+@use '@carbon/ibm-products-styles/scss/components/ActionSet/index' as *;
+@use '@carbon/ibm-products-styles/scss/components/SidePanel/index' as *;
+@use '@carbon/ibm-products-styles/scss/components/SidePanel/side-panel-variables'
+ as spv;
+
+$block-class: #{$pkg-prefix}--side-panel;
+$block-class-action-set: #{$pkg-prefix}--action-set;
+
+:host(#{$prefix}-side-panel) {
+ /* Replaces use of Framer for slide in / out animation */
+ --#{$block-class}--displaced: 100%;
+
+ * {
+ /* not sure why things are coming out as content-box */
+ box-sizing: border-box;
+ }
+
+ .#{$block-class} {
+ @extend .#{$block-class}__container;
+
+ /* for actions container query - note this changes the size calculation */
+ container-name: side-panel;
+ container-type: inline-size;
+
+ &[placement='left'] {
+ --#{$block-class}--displaced: -100%;
+ }
+
+ &[opening] {
+ transform: translateX(var(--#{$block-class}--displaced));
+ }
+
+ &[open] {
+ transform: translateX(0);
+ transition: all $duration-moderate-02 motion(standard, productive);
+ }
+
+ @media screen and (prefers-reduced-motion: reduce) {
+ &[open] {
+ transition: none;
+ }
+ }
+
+ &[closing] {
+ transform: translateX(var(--#{$block-class}--displaced));
+ transition: all $duration-moderate-01 motion(exit, productive);
+ }
+
+ @media screen and (prefers-reduced-motion: reduce) {
+ &[closing] {
+ transition: none;
+ }
+ }
+
+ &[placement='right'] {
+ @extend .#{$block-class}__container-right-placement;
+
+ /* remove if https://github.com/carbon-design-system/ibm-products/pull/3983 merged */
+ border-inline-end: 1px solid $border-subtle-02;
+ inset-inline-end: 0;
+ }
+
+ /* stylelint-disable-next-line no-duplicate-selectors -- disabled to keep close to 'right' setting */
+ &[placement='left'] {
+ @extend .#{$block-class}__container-left-placement;
+
+ /* remove if https://github.com/carbon-design-system/ibm-products/pull/3983 merged */
+ border-inline-end: 1px solid $border-subtle-02;
+ inset-inline-start: 0;
+ }
+
+ @each $size, $size_value in spv.$side-panel-sizes {
+ &[size='#{$size}'] {
+ @extend .#{$block-class}__container--#{$size};
+ }
+ }
+
+ &:not([overlay]) {
+ @extend .#{$block-class}__container-without-overlay;
+ }
+ }
+
+ .#{$block-class}__overlay {
+ @extend .#{$block-class}__overlay;
+
+ &[opening] {
+ opacity: 0;
+ }
+ @media screen and (prefers-reduced-motion: reduce) {
+ &[open] {
+ opacity: 1;
+ transition: none;
+ }
+ }
+
+ &[open] {
+ opacity: 1;
+ transition: all $duration-moderate-02 motion(standard, productive);
+ }
+ @media screen and (prefers-reduced-motion: reduce) {
+ &[closing] {
+ opacity: 0;
+ transition: none;
+ }
+ }
+
+ &[closing] {
+ opacity: 0;
+ transition: all $duration-moderate-01 motion(exit, productive);
+ }
+ }
+
+ .#{$block-class}__title-container {
+ @extend .#{$block-class}__title-container;
+
+ &::before {
+ z-index: 99; /* must be higher than action toolbar */
+ }
+
+ &[detail-step] {
+ @extend .#{$block-class}__on-detail-step;
+ }
+
+ &:not([has-title]) {
+ @extend .#{$block-class}__title-container-without-title;
+ }
+
+ &[detail-step]:not([has-title]) {
+ @extend .#{$block-class}__on-detail-step-without-title;
+ }
+
+ &[no-title-animation] {
+ @extend .#{$block-class}__title-container--no-title-animation;
+ // inset-block-start: 0;
+ }
+
+ &[reduced-motion] {
+ @extend .#{$block-class}__title-container--reduced-motion;
+ }
+
+ &[has-action-toolbar] {
+ margin-block-end: $spacing-03;
+ padding-block-end: 0;
+ }
+ }
+
+ .#{$block-class}
+ .#{$block-class}__title-container[has-action-toolbar]::before {
+ content: initial; /* remove border below */
+ }
+
+ .#{$block-class}__nav-back-button {
+ @extend .#{$block-class}__navigation-back-button;
+ }
+
+ .#{$block-class}__collapsed-title {
+ @extend .#{$block-class}__collapsed-title-text;
+ }
+
+ .#{$block-class}__title {
+ @extend .#{$block-class}__title-text;
+ }
+
+ .#{$block-class} .#{$block-class}__title {
+ margin-block-end: calc(-1 * var(--#{$block-class}--reduce-titles-by));
+ }
+
+ .#{$block-class}__slug-and-close {
+ @extend .#{$block-class}__slug-and-close;
+ }
+
+ .#{$block-class}__close-button {
+ @extend .#{$block-class}__close-button;
+ }
+
+ .#{$block-class}__label {
+ @extend .#{$block-class}__label-text;
+
+ // opacity: var(--#{$block-class}--subtitle-opacity, 1);
+ }
+
+ .#{$block-class}__subtitle {
+ @extend .#{$block-class}__subtitle-text;
+
+ &[hidden] {
+ @extend .#{$prefix}--visually-hidden;
+ }
+
+ &[no-title-animation] {
+ @extend .#{$block-class}__subtitle-text-no-animation;
+ }
+
+ &[no-title-animation][no-action-toolbar] {
+ /* not working @extend .#{$block-class}__subtitle-text-no-animation-no-action-toolbar; */
+ border-block-end: 1px solid $layer-active-01;
+ margin-block-end: $spacing-05;
+ }
+
+ &[no-title] {
+ @extend .#{$block-class}__subtitle-without-title;
+ }
+ }
+
+ .#{$block-class}__action-toolbar {
+ @extend .#{$block-class}__action-toolbar;
+
+ margin-block-end: 0;
+ padding-inline-start: 0;
+
+ &[hidden] {
+ @extend .#{$prefix}--visually-hidden;
+ }
+
+ &[no-title-animation] {
+ @extend .#{$block-class}__action-toolbar-no-animation;
+ }
+
+ &::after {
+ position: absolute;
+ background-color: $border-subtle-02;
+ block-size: 1px;
+ content: '';
+ inline-size: 100%;
+ inset-block-end: 0;
+ inset-inline-start: 0;
+ opacity: var(--#{$block-class}--divider-opacity);
+ }
+ }
+
+ .#{$block-class} .#{$block-class}__action-toolbar[hidden] {
+ @extend .#{$prefix}--visually-hidden;
+ }
+
+ .#{$block-class}__inner-content {
+ @extend .#{$block-class}__inner-content;
+
+ block-size: calc(
+ 100% - var(--#{$block-class}--actions-height, $spacing-09)
+ );
+
+ &[no-title-animation] {
+ @extend .#{$block-class}__static-inner-content;
+ }
+
+ &[has-actions] {
+ @extend .#{$block-class}__inner-content-with-actions;
+ }
+
+ &[no-title-animation]:not([has-actions]) {
+ @extend .#{$block-class}__static-inner-content-no-actions;
+ }
+ }
+
+ .#{$block-class}[has-slug] .#{$block-class}__inner-content {
+ @include callout-gradient('default');
+ }
+
+ .#{$block-class}__body-content {
+ @extend .#{$block-class}__body-content;
+ }
+
+ @mixin actions-placement() {
+ --flex-direction: row;
+ /* non carbon for IBM Products adjustment */
+ flex-direction: var(--flex-direction);
+
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ ::slotted(cds-button) {
+ flex: 0 1 25%;
+ max-inline-size: to-rem(232px);
+ }
+
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ ::slotted(cds-button[kind='ghost']) {
+ flex: 1 1 25%;
+ max-inline-size: none;
+ }
+
+ // -1 in @container query is for 1px left border
+ @container (width <= #{map.get(spv.$side-panel-sizes, lg)}) {
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ &:not([actions-multiple='triple']) ::slotted(cds-button) {
+ // double and single on lg use 50%
+ flex: 0 1 50%;
+ max-inline-size: none;
+ }
+ }
+
+ // -1 in @container query is for 1px left border
+ @container (width <= #{map.get(spv.$side-panel-sizes, md)}) {
+ // md is 50% for two and 100% for one
+ // column for triple
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ &[actions-multiple] ::slotted(cds-button) {
+ flex: 0 0 100%;
+ max-inline-size: none;
+ }
+
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ &[actions-multiple='double'] ::slotted(cds-button) {
+ flex: 0 1 50%;
+ max-inline-size: none;
+ }
+
+ &[actions-multiple='triple'] {
+ --flex-direction: column;
+ }
+ }
+
+ // -1 in @container query is for 1px left border
+ @container (width <= #{map.get(spv.$side-panel-sizes, sm)}) {
+ --flex-direction: column;
+
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ &[actions-multiple] ::slotted(cds-button) {
+ flex: 0 0 100%;
+ max-inline-size: none;
+ }
+ }
+ }
+
+ .#{$block-class}__actions {
+ @extend .#{$block-class}__actions-container;
+ @extend .#{$block-class-action-set};
+ @include actions-placement();
+
+ display: flex;
+ inline-size: 100%;
+
+ /* stylelint-disable-next-line selector-type-no-unknown */
+ ::slotted(cds-button) {
+ @extend .#{$block-class-action-set}__action-button;
+ }
+
+ &[hidden] {
+ @extend .#{$prefix}--visually-hidden;
+
+ display: none;
+ }
+
+ &[condensed] {
+ @extend .#{$block-class}__actions-container-condensed;
+ }
+
+ $multiples: 'single' 'double' 'triple';
+ @each $m in $multiples {
+ &[actions-multiple='#{$m}'] {
+ @extend .#{$prefix}--action-set--row-#{$m};
+ }
+ }
+
+ $sizes: 'xs' 'sm';
+ @each $s in $sizes {
+ &[size='#{$s}'] {
+ @extend .#{$block-class-action-set}--#{$s} !optional;
+ }
+ }
+ }
+
+ .sentinel {
+ @extend .#{$prefix}--visually-hidden;
+ }
+}
diff --git a/packages/carbon-web-components/src/components/side-panel/side-panel.ts b/packages/carbon-web-components/src/components/side-panel/side-panel.ts
new file mode 100644
index 00000000000..a5867c04bbc
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/side-panel.ts
@@ -0,0 +1,1045 @@
+/**
+ * @license
+ *
+ * Copyright IBM Corp. 2023, 2024
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { LitElement, html } from 'lit';
+import { property, query, state } from 'lit/decorators.js';
+import { prefix } from '../../globals/settings';
+import HostListener from '../../globals/decorators/host-listener';
+import HostListenerMixin from '../../globals/mixins/host-listener';
+import { SIDE_PANEL_SIZE, SIDE_PANEL_PLACEMENT } from './defs';
+import styles from './side-panel.scss';
+import { selectorTabbable } from '../../globals/settings';
+import { carbonElement as customElement } from '../../globals/decorators/carbon-element';
+import ArrowLeft16 from '@carbon/icons/lib/arrow--left/16';
+import Close20 from '@carbon/icons/lib/close/20';
+import { moderate02 } from '@carbon/motion';
+import '../button/index';
+import '../layer/index';
+import Handle from '../../globals/internal/handle';
+import './side-panel-button-set';
+
+export { SIDE_PANEL_SIZE };
+
+// eslint-disable-next-line no-bitwise
+const PRECEDING =
+ Node.DOCUMENT_POSITION_PRECEDING | Node.DOCUMENT_POSITION_CONTAINS;
+// eslint-disable-next-line no-bitwise
+const FOLLOWING =
+ Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY;
+
+const blockClass = `${prefix}--side-panel`;
+const blockClassActionSet = `${prefix}--action-set`;
+
+/**
+ * Observes resize of the given element with the given resize observer.
+ *
+ * @param observer The resize observer.
+ * @param elem The element to observe the resize.
+ */
+const observeResize = (observer: ResizeObserver, elem: Element) => {
+ if (!elem) {
+ return null;
+ }
+ observer.observe(elem);
+ return {
+ release() {
+ observer.unobserve(elem);
+ return null;
+ },
+ } as Handle;
+};
+
+/**
+ * Tries to focus on the given elements and bails out if one of them is successful.
+ *
+ * @param elems The elements.
+ * @param reverse `true` to go through the list in reverse order.
+ * @returns `true` if one of the attempts is successful, `false` otherwise.
+ */
+function tryFocusElems(elems: NodeListOf, reverse: boolean) {
+ if (!reverse) {
+ for (let i = 0; i < elems.length; ++i) {
+ const elem = elems[i];
+ elem.focus();
+ if (elem.ownerDocument!.activeElement === elem) {
+ return true;
+ }
+ }
+ } else {
+ for (let i = elems.length - 1; i >= 0; --i) {
+ const elem = elems[i];
+ elem.focus();
+ if (elem.ownerDocument!.activeElement === elem) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * SidePanel.
+ *
+ * @element cds-side-panel
+ * @csspart dialog The dialog.
+ * @fires cds-side-panel-beingclosed
+ * The custom event fired before this side-panel is being closed upon a user gesture.
+ * Cancellation of this event stops the user-initiated action of closing this side-panel.
+ * @fires cds-side-panel-closed - The custom event fired after this side-panel is closed upon a user gesture.
+ */
+@customElement(`${prefix}-side-panel`)
+class CDSSidePanel extends HostListenerMixin(LitElement) {
+ /**
+ * The handle for observing resize of the parent element of this element.
+ */
+ private _hObserveResize: Handle | null = null;
+
+ /**
+ * The element that had focus before this side-panel gets open.
+ */
+ private _launcher: Element | null = null;
+
+ /**
+ * Node to track focus going outside of side-panel content.
+ */
+ @query('#start-sentinel')
+ private _startSentinelNode!: HTMLAnchorElement;
+
+ /**
+ * Node to track focus going outside of side-panel content.
+ */
+ @query('#end-sentinel')
+ private _endSentinelNode!: HTMLAnchorElement;
+
+ /**
+ * Node to track side panel.
+ */
+ @query(`.${blockClass}`)
+ private _sidePanel!: HTMLDivElement;
+
+ /**
+ * Node to track size of actions
+ */
+ @query(`.${blockClass}__actions`)
+ private _actions!: HTMLElement;
+
+ @query(`.${blockClass}__label`)
+ private _label!: HTMLElement;
+
+ @query(`.${blockClass}__title-container`)
+ private _titleContainer!: HTMLElement;
+
+ @query(`.${blockClass}__title`)
+ private _title!: HTMLElement;
+
+ @query(`.${blockClass}__subtitle`)
+ private _subtitle!: HTMLElement;
+
+ @query(`.${blockClass}__action-toolbar`)
+ private _actionToolbar!: HTMLElement;
+
+ @query(`.${blockClass}__inner-content`)
+ private _innerContent!: HTMLElement;
+
+ @query(`.${blockClass}__body-content`)
+ private _bodyContent!: HTMLElement;
+
+ @state()
+ _isOpen = false;
+
+ @state()
+ _containerScrollTop = -16;
+
+ @state()
+ _hasSubtitle = false;
+
+ @state()
+ _hasSlug = false;
+
+ @state()
+ _hasActionToolbar = false;
+
+ @state()
+ _actionsCount = 0;
+
+ @state()
+ _slugCloseSize = 'sm';
+
+ /**
+ * Handles `blur` event on this element.
+ *
+ * @param event The event.
+ * @param event.target The event target.
+ * @param event.relatedTarget The event relatedTarget.
+ */
+ @HostListener('shadowRoot:focusout')
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
+ private _handleBlur = async ({ target, relatedTarget }: FocusEvent) => {
+ const {
+ // condensedActions,
+ open,
+ _startSentinelNode: startSentinelNode,
+ _endSentinelNode: endSentinelNode,
+ } = this;
+
+ const oldContains = target !== this && this.contains(target as Node);
+ const currentContains =
+ relatedTarget !== this &&
+ (this.contains(relatedTarget as Node) ||
+ (this.shadowRoot?.contains(relatedTarget as Node) &&
+ relatedTarget !== (startSentinelNode as Node) &&
+ relatedTarget !== (endSentinelNode as Node)));
+
+ // Performs focus wrapping if _all_ of the following is met:
+ // * This side-panel is open
+ // * The viewport still has focus
+ // * SidePanel body used to have focus but no longer has focus
+ const { selectorTabbable: selectorTabbableForSidePanel } = this
+ .constructor as typeof CDSSidePanel;
+
+ if (open && relatedTarget && oldContains && !currentContains) {
+ const comparisonResult = (target as Node).compareDocumentPosition(
+ relatedTarget as Node
+ );
+ // eslint-disable-next-line no-bitwise
+ if (relatedTarget === startSentinelNode || comparisonResult & PRECEDING) {
+ await (this.constructor as typeof CDSSidePanel)._delay();
+ if (
+ !tryFocusElems(
+ this.querySelectorAll(selectorTabbableForSidePanel),
+ true
+ ) &&
+ relatedTarget !== this
+ ) {
+ this.focus();
+ }
+ }
+ // eslint-disable-next-line no-bitwise
+ else if (
+ relatedTarget === endSentinelNode ||
+ comparisonResult & FOLLOWING
+ ) {
+ await (this.constructor as typeof CDSSidePanel)._delay();
+ if (
+ !tryFocusElems(
+ this.querySelectorAll(selectorTabbableForSidePanel),
+ true
+ )
+ ) {
+ this.focus();
+ }
+ }
+ }
+ };
+
+ @HostListener('document:keydown')
+ // @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
+ private _handleKeydown = ({ key, target }: KeyboardEvent) => {
+ if (key === 'Esc' || key === 'Escape') {
+ this._handleUserInitiatedClose(target);
+ }
+ };
+
+ private _reducedMotion =
+ typeof window !== 'undefined' && window?.matchMedia
+ ? window.matchMedia('(prefers-reduced-motion: reduce)')
+ : { matches: true };
+
+ /**
+ * Handles `click` event on the side-panel container.
+ *
+ * @param event The event.
+ */
+ private _handleClickOnOverlay(event: MouseEvent) {
+ if (!this.preventCloseOnClickOutside) {
+ this._handleUserInitiatedClose(event.target);
+ }
+ }
+
+ /**
+ * Handles `click` event on the side-panel container.
+ *
+ * @param event The event.
+ */
+ private _handleCloseClick(event: MouseEvent) {
+ this._handleUserInitiatedClose(event.target);
+ }
+
+ /**
+ * Handles user-initiated close request of this side-panel.
+ *
+ * @param triggeredBy The element that triggered this close request.
+ */
+ private _handleUserInitiatedClose(triggeredBy: EventTarget | null) {
+ if (this.open) {
+ const init = {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ detail: {
+ triggeredBy,
+ },
+ };
+ if (
+ this.dispatchEvent(
+ new CustomEvent(
+ (this.constructor as typeof CDSSidePanel).eventBeforeClose,
+ init
+ )
+ )
+ ) {
+ this.open = false;
+ this.dispatchEvent(
+ new CustomEvent(
+ (this.constructor as typeof CDSSidePanel).eventClose,
+ init
+ )
+ );
+ }
+ }
+ }
+
+ private _handleNavigateBack(triggeredBy: EventTarget | null) {
+ this.dispatchEvent(
+ new CustomEvent(
+ (this.constructor as typeof CDSSidePanel).eventNavigateBack,
+ {
+ composed: true,
+ detail: {
+ triggeredBy,
+ },
+ }
+ )
+ );
+ }
+
+ private _checkUpdateIconButtonSizes = () => {
+ const slug = this.querySelector('cds-slug');
+ const otherButtons = this?.shadowRoot?.querySelectorAll(
+ '#nav-back-button, #close-button'
+ );
+
+ let iconButtonSize = 'sm';
+
+ if (slug || otherButtons?.length) {
+ const actions = this?.querySelectorAll?.('cds-button[slot="actions"]');
+
+ if (actions?.length && /l/.test(this.size)) {
+ iconButtonSize = 'md';
+ }
+ }
+
+ if (slug) {
+ slug?.setAttribute('size', iconButtonSize);
+ }
+
+ if (otherButtons) {
+ [...otherButtons].forEach((btn) => {
+ btn.setAttribute('size', iconButtonSize);
+ });
+ }
+ };
+
+ private _handleSlugChange(e: Event) {
+ this._checkUpdateIconButtonSizes();
+ const childItems = (e.target as HTMLSlotElement).assignedNodes();
+
+ this._hasSlug = childItems.length > 0;
+ }
+
+ private _handleSubtitleChange(e: Event) {
+ const target = e.target as HTMLSlotElement;
+ const subtitle = target?.assignedNodes();
+
+ this._hasSubtitle = subtitle.length > 0;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ private _handleActionToolbarChange(e: Event) {
+ const target = e.target as HTMLSlotElement;
+ const toolbarActions = target?.assignedElements();
+
+ this._hasActionToolbar = toolbarActions && toolbarActions.length > 0;
+
+ if (this._hasActionToolbar) {
+ for (const toolbarAction of toolbarActions) {
+ // toolbar actions size should always be sm
+ toolbarAction.setAttribute('size', 'sm');
+ }
+ }
+ }
+
+ private _checkUpdateBottomPadding = () => {
+ const actionHeightPx = this._actions?.offsetHeight + 16; // add additional 1rem spacing to bottom padding
+ const actionsHeight = `${Math.round(actionHeightPx / 16)}rem`;
+
+ this._sidePanel.style?.setProperty(
+ `--${blockClass}--content-bottom-padding`,
+ actionsHeight
+ );
+ };
+
+ private _handleActionsChange(e: Event) {
+ const target = e.target as HTMLSlotElement;
+ const actions = target?.assignedElements();
+
+ // update slug size
+ this._checkUpdateIconButtonSizes();
+
+ const actionsCount = actions?.length ?? 0;
+ if (actionsCount === 0) {
+ return;
+ } else if (actionsCount > 3) {
+ this._actionsCount = 3;
+ console.warn(`Too many side-panel actions, max 3.`);
+ } else {
+ this._actionsCount = actionsCount;
+ }
+
+ for (let i = 0; i < actionsCount; i++) {
+ if (i > 3) {
+ // hide excessive side panel actions
+ actions[i].setAttribute('hidden', '');
+ actions[i].setAttribute(
+ 'data-actions-limit-3-exceeded',
+ `${actions.length}`
+ );
+ } else {
+ actions[i].setAttribute('size', this.condensedActions ? 'lg' : 'xl');
+ actions[i].classList.add(`${blockClassActionSet}__action-button`);
+ }
+ }
+
+ setTimeout(() => {
+ // update after the updates above are applied
+ this._checkUpdateBottomPadding();
+ }, 1);
+ }
+
+ /**
+ * The `ResizeObserver` instance for observing element resizes for re-positioning floating menu position.
+ */
+ // TODO: Wait for `.d.ts` update to support `ResizeObserver`
+ // @ts-ignore
+ private _resizeObserver = new ResizeObserver(() => {
+ if (this._sidePanel) {
+ this._checkUpdateBottomPadding();
+ }
+ });
+
+ private _measurements: any = {};
+
+ private _setMeasuredCustomProperties = async (reason, scrollY = 0) => {
+ await this.updateComplete;
+
+ if (!this._sidePanel || (!this.open && !this._innerContent)) {
+ return;
+ }
+
+ await (this.constructor as typeof CDSSidePanel)._delay(); // measure after brief delay for render
+ this._measurements.subtitleHeight = this._subtitle?.offsetHeight || 0; // set default subtitle height if a subtitle is not provided to enable scrolling animation
+ this._sidePanel?.style.setProperty(
+ `--${blockClass}--subtitle-container-height`,
+ `${this._measurements.subtitleHeight}px`
+ );
+
+ if (reason !== 'scroll') {
+ this._measurements.panelHeight = this._sidePanel?.offsetHeight || 0;
+ this._measurements.scrollSectionHeight =
+ this._bodyContent?.offsetHeight || 0;
+ this._measurements.titleContainerHeight =
+ this._titleContainer?.offsetHeight || 0;
+ this._measurements.titleHeight = this._title?.offsetHeight || 0;
+ this._measurements.labelHeight = this._label?.offsetHeight || 0;
+ this._measurements.totalScrollingHeight =
+ this._measurements.titleContainerHeight +
+ this._measurements.subtitleHeight +
+ this._measurements.scrollSectionHeight;
+ this._measurements.actionToolbarHeight =
+ this?._actionToolbar?.offsetHeight || 0;
+
+ this._sidePanel?.style.setProperty(
+ `--${blockClass}--title-text-height`,
+ this.animateTitle ? '0' : `${this._measurements.titleHeight + 24}px`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--action-bar-container-height`,
+ this.animateTitle ? '0' : `${this._measurements.actionToolbarHeight}px`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--label-text-height`,
+ `${this._measurements.labelHeight}px`
+ );
+ if (this.animateTitle) {
+ this._measurements.titlePaddingTop = parseInt(
+ (this._titleContainer &&
+ window &&
+ window?.getComputedStyle?.(this._titleContainer)?.[
+ 'padding-top'
+ ]) ??
+ '0',
+ 10
+ );
+
+ this._measurements.transitionDistance =
+ -1 *
+ Math.max(
+ this._measurements.titleHeight +
+ this._measurements.actionToolbarHeight +
+ this._measurements.titlePaddingTop -
+ this._measurements.titleContainerHeight,
+ this._innerContent?.offsetHeight - this._innerContent?.scrollHeight
+ );
+
+ // if the difference between the total scrolling height and the panel height is less than
+ // the subtitleElement height OR if the subtitle element height is 0, use that difference
+ // as the length of the scroll animation (otherwise the animation will not be able to complete
+ // because there is not enough scrolling distance to complete it).
+ this._measurements.subtitleHeight =
+ this._measurements.totalScrollingHeight -
+ this._measurements.panelHeight <
+ this._measurements.subtitleHeight
+ ? this._measurements.totalScrollingHeight -
+ this._measurements.panelHeight
+ : this._measurements.subtitleHeight === 0
+ ? 16
+ : this._measurements.subtitleHeight;
+ this._measurements.subtitleHeight =
+ this._measurements.subtitleHeight < 0
+ ? this._innerContent?.scrollHeight -
+ this._innerContent?.clientHeight
+ : this._measurements.subtitleHeight;
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--action-bar-container-height`,
+ `${this._measurements.actionToolbarHeight || 0}px`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--title-height`,
+ `${this._measurements.titleHeight + 16}px`
+ );
+ }
+ } else {
+ if (this.animateTitle) {
+ const scrollAnimationProgress =
+ this._measurements.transitionDistance &&
+ this._measurements.transitionDistance > scrollY
+ ? scrollY / this._measurements.transitionDistance
+ : 1;
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--scroll-y`,
+ `${scrollY}px`
+ );
+
+ const scrolled = scrollY > 0;
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--subtitle-opacity`,
+ !scrolled
+ ? '1'
+ : `${
+ 1 -
+ Math.min(this._measurements.transitionDistance, scrollY) /
+ this._measurements.transitionDistance
+ }`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--divider-opacity`,
+ !scrolled ? '0' : `${scrollAnimationProgress}`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--title-y-position`,
+ !scrolled
+ ? '0rem'
+ : `${-Math.abs(
+ Math.min(1, scrollY / this._measurements.subtitleHeight)
+ )}rem`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--collapsed-title-y-position`,
+ !scrolled
+ ? '1rem'
+ : `${
+ Math.max(0, this._measurements.subtitleHeight - scrollY) /
+ this._measurements.subtitleHeight
+ }rem`
+ );
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--title-container-height`,
+ !scrolled ? '0px' : `${this._measurements.titleContainerHeight}px`
+ );
+
+ const reduceTitleContainerHeightAmount =
+ ((this._measurements.labelHeight * scrollAnimationProgress) /
+ this._measurements.titleContainerHeight) *
+ 100;
+
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--reduce-titles-by`,
+ !scrolled && !this.animateTitle
+ ? '0px'
+ : `${Math.trunc(reduceTitleContainerHeightAmount)}px`
+ );
+ } else {
+ this._sidePanel.style.setProperty(
+ `--${blockClass}--reduce-titles-by`,
+ '0px'
+ );
+ }
+ }
+ };
+
+ private _scrollObserver = (event) => {
+ this._setMeasuredCustomProperties('scroll', event.target.scrollTop);
+ };
+
+ /**
+ * Determines if the title will animate on scroll
+ */
+ @property({ reflect: true, attribute: 'animate-title', type: Boolean })
+ animateTitle = true;
+
+ /**
+ * Sets the close button icon description
+ */
+ @property({ reflect: true, attribute: 'close-icon-description' })
+ closeIconDescription = 'Close';
+
+ /**
+ * Determines whether the side panel should render the condensed version (affects action buttons primarily)
+ */
+ @property({ type: Boolean, reflect: true, attribute: 'condensed-actions' })
+ condensedActions = false;
+
+ /**
+ * Sets the current step of the side panel
+ */
+ @property({ reflect: true, attribute: 'current-step', type: Number })
+ currentStep;
+
+ /**
+ * Determines whether the side panel should render with an overlay
+ */
+ @property({ attribute: 'include-overlay', type: Boolean, reflect: true })
+ includeOverlay = false;
+
+ /**
+ * Sets the label text which will display above the title text
+ */
+ @property({ reflect: true, attribute: 'label-text' })
+ labelText;
+
+ /**
+ * Sets the icon description for the navigation back icon button
+ */
+ @property({ reflect: true, attribute: 'navigation-back-icon-description' })
+ navigationBackIconDescription = 'Back';
+
+ /**
+ * `true` if the side-panel should be open.
+ */
+ @property({ type: Boolean, reflect: true })
+ open = false;
+
+ /**
+ * SidePanel placement.
+ */
+ @property({ reflect: true, type: String })
+ placement = SIDE_PANEL_PLACEMENT.RIGHT;
+
+ /**
+ * Prevent closing on click outside of side-panel
+ */
+ @property({ type: Boolean, attribute: 'prevent-close-on-click-outside' })
+ preventCloseOnClickOutside = false;
+
+ /**
+ * The initial location of focus in the side panel
+ */
+ @property({
+ reflect: true,
+ attribute: 'selector-initial-focus',
+ type: String,
+ })
+ selectorInitialFocus;
+
+ /**
+ * Selector for page content, used to push content to side except
+ */
+ @property({ reflect: true, attribute: 'selector-page-content' })
+ selectorPageContent = '';
+
+ /**
+ * SidePanel size.
+ */
+ @property({ reflect: true, type: String })
+ size = SIDE_PANEL_SIZE.MEDIUM;
+
+ /**
+ * Determines if this panel slides in
+ */
+ @property({ attribute: 'slide-in', type: Boolean, reflect: true })
+ slideIn = false;
+
+ /**
+ * Sets the title text
+ */
+ @property({ reflect: false, type: String })
+ title;
+
+ async connectObservers() {
+ await this.updateComplete;
+ this._hObserveResize = observeResize(this._resizeObserver, this._sidePanel);
+ }
+
+ disconnectObservers() {
+ if (this._hObserveResize) {
+ this._hObserveResize = this._hObserveResize.release();
+ }
+
+ if (this._innerContent) {
+ this._innerContent.removeEventListener('scroll', this._scrollObserver);
+ }
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.disconnectObservers();
+ this.connectObservers();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.disconnectObservers();
+ }
+
+ render() {
+ const {
+ animateTitle,
+ closeIconDescription,
+ condensedActions,
+ currentStep,
+ includeOverlay,
+ labelText,
+ navigationBackIconDescription,
+ open,
+ placement,
+ size,
+ slideIn,
+ title,
+ } = this;
+
+ if (!open && !this._isOpen) {
+ return html``;
+ }
+
+ const actionsMultiple = ['', 'single', 'double', 'triple'][
+ this._actionsCount
+ ];
+
+ const titleTemplate = html`
+ 0}
+ ?has-title=${!!title}
+ ?no-title-animation=${!animateTitle}
+ ?reduced-motion=${this._reducedMotion.matches}
+ ?has-action-toolbar=${this._hasActionToolbar}>
+
+ ${currentStep > 0
+ ? html`
+ ${ArrowLeft16({ slot: 'icon' })}
+ `
+ : ''}
+
+
+ ${title?.length && labelText?.length
+ ? html`
${labelText}
`
+ : ''}
+
+
+ ${animateTitle && title?.length && !this._reducedMotion.matches
+ ? html`
+ ${title}
+
`
+ : ''}
+
+
+ ${title?.length
+ ? html`
+ ${title}
+
`
+ : ''}
+
+
+
+
+
+
+
+ ${Close20({ slot: 'icon' })}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ return html`
+
+
+
+
+ ${!animateTitle ? titleTemplate : ''}
+
+ 0}>
+ ${animateTitle ? titleTemplate : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${includeOverlay
+ ? html``
+ : ''}
+ `;
+ }
+
+ checkSetOpen = () => {
+ const { _sidePanel: sidePanel } = this;
+ if (sidePanel && this._isOpen) {
+ // wait until the side panel has transitioned off the screen to remove
+ sidePanel.addEventListener('transitionend', () => {
+ this._isOpen = false;
+ });
+ } else {
+ // allow the html to render before animating in the side panel
+ window.requestAnimationFrame(() => {
+ this._isOpen = this.open;
+ });
+ }
+ };
+
+ adjustPageContent = () => {
+ // sets/resets styles based on slideIn property and selectorPageContent;
+ if (this.selectorPageContent) {
+ const pageContentEl: HTMLElement | null = document.querySelector(
+ this.selectorPageContent
+ );
+
+ if (pageContentEl) {
+ const newValues = {
+ marginInlineStart: '',
+ marginInlineEnd: '',
+ inlineSize: '',
+ transition: this._reducedMotion.matches
+ ? 'none'
+ : `all ${moderate02}`,
+ transitionProperty: 'margin-inline-start, margin-inline-end',
+ };
+ if (this.open) {
+ newValues.inlineSize = 'auto';
+ if (this.placement === 'left') {
+ newValues.marginInlineStart = `${this._sidePanel.offsetWidth}px`;
+ } else {
+ newValues.marginInlineEnd = `${this._sidePanel.offsetWidth}px`;
+ }
+ }
+
+ Object.keys(newValues).forEach((key) => {
+ pageContentEl.style[key] = newValues[key];
+ });
+ }
+ }
+ };
+
+ firstUpdated() {
+ this.checkSetOpen();
+ this.adjustPageContent();
+ this._setMeasuredCustomProperties('first update');
+ }
+
+ async updated(changedProperties) {
+ this.checkSetOpen();
+
+ if (
+ changedProperties.has('slide-in') ||
+ changedProperties.has('open') ||
+ changedProperties.has('include-overlay')
+ ) {
+ this.adjustPageContent();
+ }
+ if (changedProperties.has('open')) {
+ this.disconnectObservers();
+ if (this.open) {
+ this.connectObservers();
+ this._setMeasuredCustomProperties('update');
+
+ this._launcher = this.ownerDocument!.activeElement;
+ const focusNode =
+ this.selectorInitialFocus &&
+ this.querySelector(this.selectorInitialFocus);
+
+ await (this.constructor as typeof CDSSidePanel)._delay();
+ if (focusNode) {
+ // For cases where a `carbon-web-components` component (e.g. ``) being `primaryFocusNode`,
+ // where its first update/render cycle that makes it focusable happens after ``'s first update/render cycle
+ (focusNode as HTMLElement).focus();
+ } else if (
+ !tryFocusElems(
+ this.querySelectorAll(
+ (this.constructor as typeof CDSSidePanel).selectorTabbable
+ ),
+ true
+ )
+ ) {
+ this.focus();
+ }
+ } else if (
+ this._launcher &&
+ typeof (this._launcher as HTMLElement).focus === 'function'
+ ) {
+ (this._launcher as HTMLElement).focus();
+ this._launcher = null;
+ }
+
+ // monitor scroll
+ if (this._innerContent) {
+ this._innerContent.addEventListener('scroll', this._scrollObserver);
+ }
+ }
+ }
+
+ /**
+ * @param ms The number of milliseconds.
+ * @returns A promise that is resolves after the given milliseconds.
+ */
+ private static _delay(ms = 0) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+ }
+
+ /**
+ * A selector selecting tabbable nodes.
+ */
+ static get selectorTabbable() {
+ return selectorTabbable;
+ }
+
+ /**
+ * The name of the custom event fired before this side-panel is being closed upon a user gesture.
+ * Cancellation of this event stops the user-initiated action of closing this side-panel.
+ */
+ static get eventBeforeClose() {
+ return `${prefix}-side-panel-beingclosed`;
+ }
+
+ /**
+ * The name of the custom event fired after this side-panel is closed upon a user gesture.
+ */
+ static get eventClose() {
+ return `${prefix}-side-panel-closed`;
+ }
+
+ /**
+ * The name of the custom event fired on clicking the navigate back button
+ */
+ static get eventNavigateBack() {
+ return `${prefix}-side-panel-header-navigate-back`;
+ }
+
+ static styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
+}
+
+export default CDSSidePanel;
diff --git a/packages/carbon-web-components/src/components/side-panel/story-styles.scss b/packages/carbon-web-components/src/components/side-panel/story-styles.scss
new file mode 100644
index 00000000000..1bf12373ef5
--- /dev/null
+++ b/packages/carbon-web-components/src/components/side-panel/story-styles.scss
@@ -0,0 +1,46 @@
+/*
+* Copyright IBM Corp. 2023, 2024
+*
+* This source code is licensed under the Apache-2.0 license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+@use '@carbon/styles/scss/theme' as *;
+
+$story-prefix: 'side-panel-stories__';
+
+.#{$story-prefix}body-content {
+ display: flex;
+ flex-direction: column;
+ padding: 1rem;
+ gap: 1rem;
+}
+
+.#{$story-prefix}text-inputs {
+ display: flex;
+ gap: 1rem;
+
+ > * {
+ flex-basis: 50%;
+ }
+}
+
+.#{$story-prefix}story-container {
+ position: fixed;
+ display: grid;
+ block-size: 100vh;
+ grid-template-rows: 3rem 1fr;
+ inline-size: 100vw;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+}
+
+.#{$story-prefix}story-header {
+ background: $background-inverse;
+}
+
+.#{$story-prefix}story-content {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/packages/carbon-web-components/src/globals/settings.ts b/packages/carbon-web-components/src/globals/settings.ts
index b2c7199f20c..f8da0407465 100644
--- a/packages/carbon-web-components/src/globals/settings.ts
+++ b/packages/carbon-web-components/src/globals/settings.ts
@@ -52,6 +52,7 @@ const selectorTabbable = `
${prefix}-tab,
${prefix}-filter-tag,
${prefix}-textarea,
+ ${prefix}-text-input,
${prefix}-clickable-tile,
${prefix}-expandable-tile,
${prefix}-radio-tile,
diff --git a/packages/utilities/src/utilities/settings/settings.js b/packages/utilities/src/utilities/settings/settings.js
index c5e42071627..258e9b2146d 100644
--- a/packages/utilities/src/utilities/settings/settings.js
+++ b/packages/utilities/src/utilities/settings/settings.js
@@ -1,5 +1,5 @@
/**
- * Copyright IBM Corp. 2020, 2023
+ * Copyright IBM Corp. 2020, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
diff --git a/tasks/get-changelog.js b/tasks/get-changelog.js
index 234badf399f..a190d6bd878 100644
--- a/tasks/get-changelog.js
+++ b/tasks/get-changelog.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
- * Copyright IBM Corp. 2020, 2023
+ * Copyright IBM Corp. 2020, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
diff --git a/yarn.lock b/yarn.lock
index 840073d7902..87d693151d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3112,6 +3112,19 @@ __metadata:
languageName: node
linkType: hard
+"@carbon/ibm-products-styles@npm:2.20.1":
+ version: 2.20.1
+ resolution: "@carbon/ibm-products-styles@npm:2.20.1"
+ peerDependencies:
+ "@carbon/grid": ^11.21.1
+ "@carbon/layout": ^11.20.1
+ "@carbon/motion": ^11.16.1
+ "@carbon/themes": ^11.28.0
+ "@carbon/type": ^11.25.1
+ checksum: 10/a069cae6b7a4eb23d8b2480ac28bea80660f0893cd810c80d6ad148276d2f9f772c1a965983efcea853bcccdb794039c9c555893b294544b98df218fd14e6fbd
+ languageName: node
+ linkType: hard
+
"@carbon/ibmdotcom-services-store@npm:1.53.0, @carbon/ibmdotcom-services-store@workspace:packages/services-store":
version: 0.0.0-use.local
resolution: "@carbon/ibmdotcom-services-store@workspace:packages/services-store"
@@ -3630,6 +3643,7 @@ __metadata:
"@babel/runtime": "npm:^7.16.3"
"@babel/template": "npm:~7.12.0"
"@babel/traverse": "npm:~7.23.7"
+ "@carbon/ibm-products-styles": "npm:2.20.1"
"@carbon/icon-helpers": "npm:10.45.1"
"@carbon/icons": "npm:11.34.1"
"@carbon/styles": "npm:1.48.1"