feat(ui): support invisible resizable split separators
This commit is contained in:
@@ -376,6 +376,7 @@ export function CatalogApp() {
|
|||||||
const [workspaceWindowDemoOpen, setWorkspaceWindowDemoOpen] = useState(true);
|
const [workspaceWindowDemoOpen, setWorkspaceWindowDemoOpen] = useState(true);
|
||||||
const [workspaceWindowDemoMaximized, setWorkspaceWindowDemoMaximized] = useState(false);
|
const [workspaceWindowDemoMaximized, setWorkspaceWindowDemoMaximized] = useState(false);
|
||||||
const [workspaceWindowDemoRect, setWorkspaceWindowDemoRect] = useState<WorkspaceWindowRect>({ x: 24, y: 24, width: 340, height: 230 });
|
const [workspaceWindowDemoRect, setWorkspaceWindowDemoRect] = useState<WorkspaceWindowRect>({ x: 24, y: 24, width: 340, height: 230 });
|
||||||
|
const [splitPaneInvisible, setSplitPaneInvisible] = useState(false);
|
||||||
const [splitPaneDemoSize, setSplitPaneDemoSize] = useState(50);
|
const [splitPaneDemoSize, setSplitPaneDemoSize] = useState(50);
|
||||||
const [sidePanelDemoOpen, setSidePanelDemoOpen] = useState(true);
|
const [sidePanelDemoOpen, setSidePanelDemoOpen] = useState(true);
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
@@ -1510,9 +1511,11 @@ export function CatalogApp() {
|
|||||||
<p className="catalog-preview__explanation">Rectangle, maximize, close и stacking остаются состоянием приложения; дизайн-система владеет одинаковой bounded-геометрией и доступным управлением.</p>
|
<p className="catalog-preview__explanation">Rectangle, maximize, close и stacking остаются состоянием приложения; дизайн-система владеет одинаковой bounded-геометрией и доступным управлением.</p>
|
||||||
</Preview>
|
</Preview>
|
||||||
<Preview title="Split pane" note="pointer / keyboard / controlled" className="catalog-preview--wide catalog-split-pane-preview">
|
<Preview title="Split pane" note="pointer / keyboard / controlled" className="catalog-preview--wide catalog-split-pane-preview">
|
||||||
|
<Switch label="Скрыть линию разделителя" checked={splitPaneInvisible} onChange={setSplitPaneInvisible} />
|
||||||
<div className="catalog-split-pane-demo">
|
<div className="catalog-split-pane-demo">
|
||||||
<SplitPane
|
<SplitPane
|
||||||
primarySize={splitPaneDemoSize}
|
primarySize={splitPaneDemoSize}
|
||||||
|
separatorAppearance={splitPaneInvisible ? "invisible" : "line"}
|
||||||
onPrimarySizeChange={setSplitPaneDemoSize}
|
onPrimarySizeChange={setSplitPaneDemoSize}
|
||||||
minPrimarySize={25}
|
minPrimarySize={25}
|
||||||
minSecondarySize={25}
|
minSecondarySize={25}
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ ConfirmationModal оборачивает Window и защищает async-confir
|
|||||||
|
|
||||||
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `resizable={false}` панели сохраняют ту же стабильную DOM-композицию, а separator полностью отсутствует в визуальном слое и accessibility tree; это позволяет приложению сворачивать одну из панелей без remount тяжёлого renderer.
|
Разделитель является структурной границей между двумя рабочими представлениями и не создаёт декоративную рамку вокруг контента. При `resizable={false}` панели сохраняют ту же стабильную DOM-композицию, а separator полностью отсутствует в визуальном слое и accessibility tree; это позволяет приложению сворачивать одну из панелей без remount тяжёлого renderer.
|
||||||
|
|
||||||
|
При `separatorAppearance="invisible"` линия скрыта в покое, при наведении и перетаскивании. Зона захвата, курсор, клавиатурное изменение размера и индикатор клавиатурного фокуса сохраняются.
|
||||||
|
|
||||||
## ShareAccessModal
|
## ShareAccessModal
|
||||||
|
|
||||||
Workflow-sharing modal из нового Engine: заголовок ресурса, compact avatar stack, список участников, редактирование ролей, удаление доступа, email и роль нового участника. Компонент сохраняет Engine-геометрию `600 px` и controls `46 px`, но не импортирует ACL API. Consumer передаёт members, permissions и callbacks.
|
Workflow-sharing modal из нового Engine: заголовок ресурса, compact avatar stack, список участников, редактирование ролей, удаление доступа, email и роль нового участника. Компонент сохраняет Engine-геометрию `600 px` и controls `46 px`, но не импортирует ACL API. Consumer передаёт members, permissions и callbacks.
|
||||||
|
|||||||
@@ -3111,6 +3111,12 @@ textarea.nodedc-field__control {
|
|||||||
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.16);
|
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep the full hit area and keyboard focus cue without a visible divider. */
|
||||||
|
.nodedc-split-pane[data-separator-appearance="invisible"] > .nodedc-split-pane__separator:not(:focus-visible)::before {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nodedc-split-pane[data-dragging="true"] {
|
.nodedc-split-pane[data-dragging="true"] {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface SplitPaneProps extends Omit<HTMLAttributes<HTMLDivElement>, "ch
|
|||||||
step?: number;
|
step?: number;
|
||||||
resizable?: boolean;
|
resizable?: boolean;
|
||||||
separatorLabel: string;
|
separatorLabel: string;
|
||||||
|
separatorAppearance?: "line" | "invisible";
|
||||||
}
|
}
|
||||||
|
|
||||||
const clamp = (value: number, minimum: number, maximum: number) => (
|
const clamp = (value: number, minimum: number, maximum: number) => (
|
||||||
@@ -48,6 +49,7 @@ export function SplitPane({
|
|||||||
step = 2,
|
step = 2,
|
||||||
resizable = true,
|
resizable = true,
|
||||||
separatorLabel,
|
separatorLabel,
|
||||||
|
separatorAppearance = "line",
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
@@ -95,6 +97,7 @@ export function SplitPane({
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={cn("nodedc-split-pane", className)}
|
className={cn("nodedc-split-pane", className)}
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
|
data-separator-appearance={separatorAppearance}
|
||||||
data-dragging={dragging ? "true" : undefined}
|
data-dragging={dragging ? "true" : undefined}
|
||||||
style={splitStyle}
|
style={splitStyle}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -288,7 +288,8 @@
|
|||||||
"summary": "Controlled two-panel layout with a pointer- and keyboard-resizable accessible separator.",
|
"summary": "Controlled two-panel layout with a pointer- and keyboard-resizable accessible separator.",
|
||||||
"anatomy": ["primary panel", "structural separator", "secondary panel"],
|
"anatomy": ["primary panel", "structural separator", "secondary panel"],
|
||||||
"variants": ["vertical", "horizontal"],
|
"variants": ["vertical", "horizontal"],
|
||||||
"behavior": ["controlled percentage", "optional resizable separator", "pointer capture drag", "Arrow/Home/End keyboard sizing", "ARIA separator range", "ratio preservation across host resize"],
|
"behavior": ["controlled percentage", "optional resizable separator",
|
||||||
|
"line or invisible separator appearance with retained keyboard focus", "pointer capture drag", "Arrow/Home/End keyboard sizing", "ARIA separator range", "ratio preservation across host resize"],
|
||||||
"rules": [
|
"rules": [
|
||||||
"The application owns the controlled primary percentage and panel content; the component owns separator interaction and accessibility.",
|
"The application owns the controlled primary percentage and panel content; the component owns separator interaction and accessibility.",
|
||||||
"Minimum primary and secondary percentages clamp both pointer and keyboard changes.",
|
"Minimum primary and secondary percentages clamp both pointer and keyboard changes.",
|
||||||
|
|||||||
@@ -61,3 +61,15 @@ test("SplitPane is registered, documented, cataloged and keyboard-addressable",
|
|||||||
assert.match(docs, /## SplitPane/);
|
assert.match(docs, /## SplitPane/);
|
||||||
assert.match(catalog, /<SplitPane/);
|
assert.match(catalog, /<SplitPane/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("invisible divider retains an accessible resize target", () => {
|
||||||
|
const markup = renderToStaticMarkup(createElement(SplitPane, {
|
||||||
|
primary: "Camera", secondary: "Map", primarySize: 40,
|
||||||
|
onPrimarySizeChange: () => {}, separatorLabel: "Resize", separatorAppearance: "invisible",
|
||||||
|
}));
|
||||||
|
assert.match(markup, /data-separator-appearance="invisible"/);
|
||||||
|
assert.match(markup, /role="separator"/);
|
||||||
|
assert.match(markup, /tabindex="0"/);
|
||||||
|
assert.match(markup, /aria-valuenow="40"/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user