Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/layout-panel-publishes-applied-inset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] `LayoutPanel`: on an edge that takes the Layout's own inset, the panel now tells its children the padding it applies there. Children that read their container's padding, such as a full-bleed `Divider`, `Table`, or `TabList`, or a nested `Layout`, now follow the panel's outer edge whenever the Layout's `padding` differs from the inner inset. Before, they used the inner inset: with `padding={6}` a full-bleed `Divider` stopped 8px short of the edge, and with `padding={0}` it ran 16px past it. The docs now say which edge `hasDivider` draws on, that the Layout's `defaultHasDividers` does not apply, and that `padding={0}` is full bleed, and they list `ref`.

@cixzhang
9 changes: 9 additions & 0 deletions .github/a11y-audit-axe.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,14 @@
"core-layout--no-dividers",
"core-layout--full-bleed-content"
]
},
"LayoutPanel": {
"components": ["Layout"],
"stories": [
"core-layout--panel-states",
"core-layout--with-sidebar",
"core-layout--dual-panels",
"core-layout--content-width-with-start-panel"
]
}
}
254 changes: 254 additions & 0 deletions apps/storybook/stories/Layout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
VStack,
} from '@astryxdesign/core/Layout';
import {Button} from '@astryxdesign/core/Button';
import {Divider} from '@astryxdesign/core/Divider';
import {
colorVars,
spacingVars,
Expand Down Expand Up @@ -2339,3 +2340,256 @@ export const HeaderStates: Story = {
);
},
};

export const PanelStates: Story = {
name: 'Panel States',
render: () => {
const body = <p {...stylex.props(styles.bodyText)}>Content area.</p>;
const panelText = <p {...stylex.props(styles.bodyText)}>Panel</p>;
const rows = Array.from({length: 12}, (_, index) => (
<p key={index} {...stylex.props(styles.bodyText)}>
Item {index + 1}
</p>
));
const cases = [
{
id: 'start-automatic',
label: 'Start, no divider, padding=6',
layout: (
<Layout
padding={6}
start={
<LayoutPanel width={120} data-panel-case="start-automatic">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'start-divider',
label: 'Start, hasDivider, padding=6',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width={120}
hasDivider
data-panel-case="start-divider">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'end-automatic',
label: 'End, no divider, padding=6',
layout: (
<Layout
padding={6}
end={
<LayoutPanel width={120} data-panel-case="end-automatic">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'end-divider',
label: 'End, hasDivider, padding=6',
layout: (
<Layout
padding={6}
end={
<LayoutPanel width={120} hasDivider data-panel-case="end-divider">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'header-footer',
label: 'Start, hasDivider, with header and footer',
layout: (
<Layout
padding={6}
header={
<LayoutHeader hasDivider>
<p {...stylex.props(styles.bodyText)}>Header</p>
</LayoutHeader>
}
start={
<LayoutPanel
width={120}
hasDivider
data-panel-case="header-footer">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
footer={
<LayoutFooter hasDivider>
<p {...stylex.props(styles.bodyText)}>Footer</p>
</LayoutFooter>
}
/>
),
},
{
id: 'bleed-child',
label: 'Start, hasDivider, full-bleed Divider inside',
layout: (
<Layout
padding={6}
start={
<LayoutPanel width={160} hasDivider data-panel-case="bleed-child">
{panelText}
<Divider isFullBleed />
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'padding-2',
label: 'Start, hasDivider, panel padding=2',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width={120}
hasDivider
padding={2}
data-panel-case="padding-2">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'full-bleed',
label: 'Start, hasDivider, panel padding=0',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width={120}
hasDivider
padding={0}
data-panel-case="full-bleed">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'width-string',
label: 'Start, hasDivider, width="10rem"',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width="10rem"
hasDivider
data-panel-case="width-string">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'landmark',
label: 'Start, role="navigation", label="Sections"',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width={120}
hasDivider
role="navigation"
label="Sections"
data-panel-case="landmark">
{panelText}
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'scrolling',
label: 'Start, overflowing content',
layout: (
<Layout
padding={6}
start={
<LayoutPanel width={120} hasDivider data-panel-case="scrolling">
<VStack gap={2}>
{rows}
{/* A focusable control keeps the scrolling content keyboard-reachable. */}
<Button label="More" variant="secondary">
More
</Button>
</VStack>
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
{
id: 'not-scrollable',
label: 'Start, overflowing content, isScrollable={false}',
layout: (
<Layout
padding={6}
start={
<LayoutPanel
width={120}
hasDivider
isScrollable={false}
data-panel-case="not-scrollable">
<VStack gap={2}>{rows}</VStack>
</LayoutPanel>
}
content={<LayoutContent>{body}</LayoutContent>}
/>
),
},
];
return (
<VStack gap={4} xstyle={styles.storySection}>
<HStack gap={4} wrap="wrap">
{cases.map(({id, label, layout}) => (
<VStack key={id} gap={2}>
<p {...stylex.props(styles.subheading)}>{label}</p>
<div {...stylex.props(styles.cwContainer, styles.insetFrame)}>
{layout}
</div>
</VStack>
))}
</HStack>
</VStack>
);
},
};
27 changes: 21 additions & 6 deletions packages/core/src/Layout/LayoutPanel.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ export const docs = {
{
name: 'hasDivider',
type: 'boolean',
description: 'Border on the appropriate edge.',
description:
"Line on the edge that faces the content: the inline end in the start slot, the inline start in the end slot. Without it and without padding, the panel and the content share one gap. The Layout's defaultHasDividers does not apply. Beside a ResizeHandle that draws its own line, leave it false.",
default: 'false',
},
{
name: 'padding',
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
description:
'Internal padding using the spacing scale. Overrides the default padding from the layout container.',
'Internal padding on every edge, using the spacing scale. Replaces the automatic inset and the gap shared with the content. 0 is full bleed.',
},
{
name: 'isScrollable',
Expand Down Expand Up @@ -73,6 +74,11 @@ export const docs = {
description:
"The output of useResizable(), or its props. When provided, the hook drives the panel width and a ResizeHandle should be placed adjacent to the panel. Carries the region's axis ('horizontal' | 'vertical'), which must match the adjacent ResizeHandle's direction. With the hook's own value, the panel slides closed and open from the edge facing its ResizeHandle; a hand-built object does not slide. Either way, focus inside the panel moves to its ResizeHandle as it collapses, and the panel is inert while collapsed.",
},
{
name: 'ref',
type: 'React.Ref<HTMLDivElement>',
description: 'Ref forwarded to the panel element.',
},
],
};

Expand All @@ -90,13 +96,15 @@ export const docsZh = {
{
name: 'hasDivider',
type: 'boolean',
description: '相应边缘的边框。',
description:
'朝向内容一侧边缘的分隔线:在 start 插槽中位于行内结束边,在 end 插槽中位于行内起始边。不设置此项且未设置 padding 时,面板与内容共用一个间距。Layout 的 defaultHasDividers 不适用。旁边的 ResizeHandle 自带分隔线时,请保持为 false。',
default: 'false',
},
{
name: 'padding',
type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10',
description: '使用间距比例的内边距。覆盖布局容器的默认内边距。',
description:
'使用间距比例设置四边的内边距,替换自动内边距以及与内容共用的间距。0 表示全出血。',
},
{
name: 'isScrollable',
Expand Down Expand Up @@ -127,6 +135,11 @@ export const docsZh = {
description:
"useResizable() 的返回值或其 props。提供时面板宽度由 hook 驱动,应在面板旁放置 ResizeHandle。其中携带区域的轴向('horizontal' | 'vertical'),必须与相邻 ResizeHandle 的 direction 一致。使用 hook 自身返回的值时,面板从朝向其 ResizeHandle 的边缘滑动关闭和打开;手动构造的对象不会滑动。两种情况下,折叠时面板内的焦点都会移到其 ResizeHandle 上,折叠期间面板为 inert。",
},
{
name: 'ref',
type: 'React.Ref<HTMLDivElement>',
description: '转发到面板元素的 ref。',
},
],
};

Expand All @@ -137,9 +150,10 @@ export const docsDense = {
description: 'Sidebar for navigation, settings, inspector panels.',
propDescriptions: {
children: 'Panel content.',
hasDivider: 'Border on appropriate edge.',
hasDivider:
'Line on the content-facing edge (start slot: inline end; end slot: inline start); without it or padding, panel and content share one gap. Layout defaultHasDividers does not apply. False beside a ResizeHandle with its own line.',
padding:
'Internal padding on spacing scale. Overrides layout container default.',
'Padding on every edge; replaces the automatic inset and the shared gap. 0 is full bleed.',
isScrollable: 'Enable scrollable overflow.',
label:
'Accessible label for landmark element; also names the region while it scrolls (default "Content").',
Expand All @@ -148,5 +162,6 @@ export const docsDense = {
'Panel width. Numbers = pixels, strings as-is. Ignored when resizable provided; hook controls width. width={region.size} beside a ResizeHandle given the region props: inert while collapsed; focus inside moves to the handle on collapse.',
resizable:
'useResizable() output or its props. Hook drives panel width; place ResizeHandle adjacent. Hook values slide; hand-built ones do not. Focus inside moves to the handle on collapse; inert while collapsed.',
ref: 'Ref forwarded to the panel element.',
},
};
Loading
Loading