+ {panelPosition === 'side-end' && wrappedMainContent}
+
+ {panelPosition === 'side-start' && wrappedPanelContent}
+ {resizable && display === 'all' && handle}
+ {panelPosition === 'side-end' && wrappedPanelContent}
+
+ {panelPosition === 'side-start' && wrappedMainContent}
+
+ );
+ }
+);
+
+export default InternalPanelLayout;
diff --git a/src/panel-layout/styles.scss b/src/panel-layout/styles.scss
new file mode 100644
index 0000000000..c58f9f8158
--- /dev/null
+++ b/src/panel-layout/styles.scss
@@ -0,0 +1,63 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+@use '../internal/styles/tokens' as awsui;
+@use '../internal/styles' as styles;
+@use '@cloudscape-design/component-toolkit/internal/focus-visible' as focus-visible;
+
+// Our standard focus rings don't work for this use-case because the element
+// is scrollable. This renders a ring inside the element, that is sticky and
+// overlays the content and scrollbars as expected.
+@mixin container-inner-focus-ring {
+ $width: 2px;
+ border-start-start-radius: calc(#{awsui.$border-radius-control-default-focus-ring} + #{$width});
+ border-start-end-radius: calc(#{awsui.$border-radius-control-default-focus-ring} + #{$width});
+ border-end-start-radius: calc(#{awsui.$border-radius-control-default-focus-ring} + #{$width});
+ border-end-end-radius: calc(#{awsui.$border-radius-control-default-focus-ring} + #{$width});
+ outline: $width solid awsui.$color-border-item-focused;
+ outline-offset: calc(-1 * #{$width});
+}
+
+.root {
+ @include styles.styles-reset;
+ block-size: 100%;
+ overflow: hidden;
+ display: flex;
+}
+
+.panel {
+ display: flex;
+ flex-shrink: 0;
+ > .handle {
+ display: flex;
+ align-items: center;
+ }
+ > .panel-content {
+ overflow-y: auto;
+ overflow-x: visible;
+ flex-grow: 1;
+ @include focus-visible.when-visible {
+ @include container-inner-focus-ring;
+ }
+ }
+ .display-main-only > & {
+ display: none;
+ }
+ .display-panel-only > & {
+ flex: 1;
+ }
+}
+
+.content {
+ overflow-y: auto;
+ flex-grow: 1;
+ flex-shrink: 1;
+ .display-panel-only > & {
+ display: none;
+ }
+ @include focus-visible.when-visible {
+ @include container-inner-focus-ring;
+ }
+}
diff --git a/src/panel-layout/test-classes/styles.scss b/src/panel-layout/test-classes/styles.scss
new file mode 100644
index 0000000000..d7bfe2f491
--- /dev/null
+++ b/src/panel-layout/test-classes/styles.scss
@@ -0,0 +1,11 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ SPDX-License-Identifier: Apache-2.0
+*/
+
+.root,
+.panel,
+.content,
+.resize-handle {
+ /* used in test-utils */
+}
diff --git a/src/test-utils/dom/panel-layout/index.ts b/src/test-utils/dom/panel-layout/index.ts
new file mode 100644
index 0000000000..6748b4083d
--- /dev/null
+++ b/src/test-utils/dom/panel-layout/index.ts
@@ -0,0 +1,31 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { ComponentWrapper, ElementWrapper } from '@cloudscape-design/test-utils-core/dom';
+
+import styles from '../../../panel-layout/test-classes/styles.selectors.js';
+
+export default class PanelLayoutWrapper extends ComponentWrapper {
+ static rootSelector: string = styles.root;
+
+ /**
+ * Returns the wrapper for the panel element.
+ */
+ findPanelContent(): ElementWrapper | null {
+ return this.findByClassName(styles.panel);
+ }
+
+ /**
+ * Returns the wrapper for the main content element.
+ */
+ findMainContent(): ElementWrapper | null {
+ return this.findByClassName(styles.content);
+ }
+
+ /**
+ * Returns the wrapper for the resize handle element.
+ * Returns null if the panel layout is not resizable.
+ */
+ findResizeHandle(): ElementWrapper | null {
+ return this.findByClassName(styles['resize-handle']);
+ }
+}