Commit 5ecfceb4 authored by Benjamin Kott's avatar Benjamin Kott Committed by Georg Ringer
Browse files

[TASK] Polish SvgTree component styling

This patch is cleaning up a lot of loose ends in the svg tree component.
It aims to make the component look more clean and polished.

- Unify focus styling with selected, hover and versioning
- Ensure the whole node looks focused instead of only some text
- Replace expand icon to match core icon styling
- Hide expand icon if page tree is stopped
- Move trigger to enter stopped pagetree to the front
- Remove blurry lines for background borders
- Make the edit input fields use as much space as is available
- Load common icons always on setup to avoid node refresh on initial loads
- Reposition the locked icon (another editor is editing)
- Correct keyboard navigation behaviour and focus setting
- Keep focus after leaving the edit mode without changes
- Adjust positioning to rely on calculation instead of hardcoded values
- Align drag delete target
- Make drag positions more visible to the user when moving a page
- Improve styling of tooltips when dragging elements
- Add border to select tree component to make scroll boundaries visible

Affected Areas:
- PageTree
- FileTree
- SelectTree

Releases: main
Resolves: #97344
Change-Id: I6034e0cbc1079b93b24c5c1a99e0299946874b2f
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74203

Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
parent a9cf5c75
$svgColors: ( :root {
border: #d7d7d7, --svgtree-drag-info-bg: #fff;
lines: #ddd,
nodeSelectedBg: #fff, --svgtree-drag-info-color: #{color-contrast(#fff)};
nodeHighlightText: #0078e6,
nodeOverBg: #f2f2f2, --svgtree-drag-info-icon-size: 16px;
dragOverBg: #d7e4f1,
dragOverBorder: transparent, --svgtree-drag-info-border-radius: 2px;
dragAlertBg: #f6d3cf,
dragAlertBorder: #d66c68, --svgtree-drag-info-padding-y: 0.5rem;
dragAboveBg: transparent,
dragAboveBorder: transparent, --svgtree-drag-info-padding-x: 0.75rem;
dragBetweenBg: transparent,
dragBetweenBorder: transparent, --svgtree-drag-dropindicator-color: #{tint-color($primary, 20%)};
dragBelowBg: transparent,
dragBelowBorder: transparent, --svgtree-structure-line-color: #ddd;
dragTooltipBg: #d7e4f1,
dragTooltipAlertBg: #f6d3cf, --svgtree-node-color: #000;
dragTooltipAlertBorder: #d66c68
); --svgtree-node-bg: transparent;
--svgtree-node-border-color: transparent;
--svgtree-node-version-bg: #{rgba(#f7c898, 0.5)};
--svgtree-node-version-border-color: #{shade-color(#f7c898, 20%)};
--svgtree-node-focus-bg: #{rgba(tint-color($primary, 90%), 0.5)};
--svgtree-node-focus-border-color: #{tint-color($primary, 20%)};
--svgtree-node-hover-bg: #{rgba(#fafafa, 0.5)};
--svgtree-node-hover-border-color: #d7d7d7;
--svgtree-node-selected-bg: #{rgba(#fff, 0.5)};
--svgtree-node-selected-border-color: #d7d7d7;
--svgtree-highlight-color: #{$primary};
--svgtree-info-bg: #{tint-color($info, 60%)};
--svgtree-info-color: #{color-contrast(tint-color($info, 60%))};
}
.svg-tree { .svg-tree {
position: relative; position: relative;
...@@ -70,9 +95,23 @@ $svgColors: ( ...@@ -70,9 +95,23 @@ $svgColors: (
} }
} }
// toggle to collapse and expand node entries
.node-toggle {
&-icon {
transform-origin: 50% 50%;
}
&--expanded {
.node-toggle-icon {
transform: rotate(90deg);
}
}
}
.svg-tree-element { .svg-tree-element {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid rgba(0, 0, 0, 0.25);
& > .svg-tree-wrapper { & > .svg-tree-wrapper {
flex: 1 0 0; flex: 1 0 0;
...@@ -83,6 +122,8 @@ $svgColors: ( ...@@ -83,6 +122,8 @@ $svgColors: (
background-color: #fafafa; background-color: #fafafa;
position: sticky; position: sticky;
top: 0; top: 0;
padding: 0.5em;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
} }
} }
...@@ -98,98 +139,67 @@ $svgColors: ( ...@@ -98,98 +139,67 @@ $svgColors: (
path.link { path.link {
fill: none; fill: none;
shape-rendering: crispEdges; shape-rendering: crispEdges;
stroke: map_get($svgColors, lines); stroke: var(--svgtree-structure-line-color);
stroke-width: 1; stroke-width: 1;
pointer-events: none; pointer-events: none;
} }
.node { .node {
&-bg { &-bg {
fill: transparent; fill: var(--svgtree-node-bg);
stroke: var(--svgtree-node-border-color);
stroke-width: 1px;
&__border { &__border {
display: none; display: none;
pointer-events: none; pointer-events: none;
fill: #9eb2c5; fill: var(--svgtree-drag-dropindicator-color);
stroke: var(--svgtree-drag-dropindicator-color);
stroke-width: 1;
shape-rendering: crispedges;
} }
&.ver-element, &.ver-element,
&.ver-versions, &.ver-versions,
&.ver-page { &.ver-page {
fill: #f7c898 !important; --svgtree-node-bg: var(--svgtree-node-version-bg);
--svgtree-node-border-color: var(--svgtree-node-version-border-color);
} }
} }
&-over:not(.node-selected) { &:focus {
fill: map_get($svgColors, nodeOverBg); outline: none;
stroke-width: 1px; }
stroke: map_get($svgColors, border);
&-over {
--svgtree-node-bg: var(--svgtree-node-hover-bg);
--svgtree-node-border-color: var(--svgtree-node-hover-border-color);
} }
&-selected { &-selected {
fill: map_get($svgColors, nodeSelectedBg); --svgtree-node-bg: var(--svgtree-node-selected-bg);
stroke-width: 1px;
stroke: map_get($svgColors, border); --svgtree-node-border-color: var(--svgtree-node-selected-border-color);
} }
}
.nodes { &-focused {
&-wrapper { --svgtree-node-bg: var(--svgtree-node-focus-bg) !important;
$b: '.nodes-wrapper';
cursor: pointer; --svgtree-node-border-color: var(--svgtree-node-focus-border-color) !important;
}
}
&--dragging { .nodes-wrapper {
cursor: grabbing; cursor: pointer;
.node-over {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragOverBg) !important;
stroke-width: 1px;
stroke: map_get($svgColors, dragOverBorder);
}
.node-alert {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragAlertBg) !important;
stroke: map_get($svgColors, dragAlertBorder);
}
&#{$b}--nodrop {
.node-over {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragAlertBg) !important;
}
}
&#{$b}--ok-above {
.node-over {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragAboveBg) !important;
stroke: map_get($svgColors, dragAboveBorder);
}
}
&#{$b}--ok-between {
.node-over {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragBetweenBg) !important;
stroke: map_get($svgColors, dragBetweenBorder);
}
}
&#{$b}--ok-below {
.node-over {
//it must be important because there is inline style in code that we must overwrite
fill: map_get($svgColors, dragBelowBg) !important;
stroke: map_get($svgColors, dragBelowBorder);
}
}
}
&--nodrop { &--dragging {
cursor: no-drop; cursor: grabbing;
} }
&--nodrop {
cursor: no-drop;
} }
} }
} }
...@@ -200,27 +210,38 @@ $svgColors: ( ...@@ -200,27 +210,38 @@ $svgColors: (
display: none; display: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: none; border: 0;
background-color: map_get($svgColors, dragTooltipBg); color: var(--svgtree-drag-info-color);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); background-color: var(--svgtree-drag-info-bg);
border-radius: var(--svgtree-drag-info-border-radius);
box-shadow: 0 2px 2px 2px rgba(0, 0, 0, 0.25);
z-index: 9999; z-index: 9999;
&--nodrop { &__ctrl-icon {
background-color: map_get($svgColors, dragTooltipAlertBg); position: absolute;
border: 1px solid map_get($svgColors, dragTooltipAlertBorder); top: var(--svgtree-drag-info-padding-y);
left: var(--svgtree-drag-info-padding-x);
display: block;
width: var(--svgtree-drag-info-icon-size);
height: var(--svgtree-drag-info-icon-size);
background-color: transparent;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
}
&__text {
display: flex;
padding: var(--svgtree-drag-info-padding-y) var(--svgtree-drag-info-padding-x);
gap: 0.25rem;
}
&--nodrop {
& .node-dd__ctrl-icon { & .node-dd__ctrl-icon {
background-image: url(../Images/pagetree-drag-place-denied.png); background-image: url(../Images/pagetree-drag-place-denied.png);
} }
} }
&__text {
display: table;
vertical-align: middle;
opacity: 0.85;
padding: 5px 5px 5px 20px;
}
&--ok-below { &--ok-below {
&.node-dd--copy .node-dd__ctrl-icon { &.node-dd--copy .node-dd__ctrl-icon {
background-image: url(../Images/pagetree-drag-copy-below.png); background-image: url(../Images/pagetree-drag-copy-below.png);
...@@ -260,31 +281,6 @@ $svgColors: ( ...@@ -260,31 +281,6 @@ $svgColors: (
background-image: url(../Images/pagetree-drag-move-above.png); background-image: url(../Images/pagetree-drag-move-above.png);
} }
} }
&__icon {
display: table-cell;
vertical-align: top;
padding-left: 3px;
padding-right: 3px;
}
&__name {
display: table-cell;
vertical-align: top;
}
&__ctrl-icon {
position: absolute;
top: 3px;
left: 3px;
display: block;
width: 16px;
height: 16px;
background-color: transparent;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
}
} }
.nodes-drop-zone { .nodes-drop-zone {
...@@ -293,18 +289,21 @@ $svgColors: ( ...@@ -293,18 +289,21 @@ $svgColors: (
} }
rect { rect {
fill: map_get($svgColors, dragAlertBorder); fill: $danger;
cursor: grabbing; cursor: grabbing;
} }
text { text {
fill: color-contrast($danger);
pointer-events: none; pointer-events: none;
} }
} }
.node-name { .node-name {
fill: var(--svgtree-node-color);
.node-highlight-text { .node-highlight-text {
fill: map_get($svgColors, nodeHighlightText); fill: var(--svgtree-highlight-color);
font-weight: 700; font-weight: 700;
} }
} }
...@@ -313,6 +312,9 @@ $svgColors: ( ...@@ -313,6 +312,9 @@ $svgColors: (
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
padding: 0.25rem;
border: 1px solid tint-color($primary, 20%);
outline: none;
} }
.scaffold-content .svg-toolbar { .scaffold-content .svg-toolbar {
...@@ -356,16 +358,18 @@ $svgColors: ( ...@@ -356,16 +358,18 @@ $svgColors: (
} }
} }
.node-stop { //
fill: map_get($svgColors, dragAlertBorder); // Info bar displayed above the tree if a page is mounted
} //
.node-mount-point { .node-mount-point {
display: flex; display: flex;
border: 0; border: 0;
background-color: $info; background-color: var(--svgtree-info-bg);
color: #fff; border-bottom: 1px solid rgba(0, 0, 0, 0.25);
padding: 1em; color: var(--svgtree-info-color);
padding: 0.75em 1.167em;
gap: 0.5em;
align-items: center;
&__icon { &__icon {
flex: 0 auto; flex: 0 auto;
......
...@@ -34,8 +34,12 @@ export class SelectTree extends SvgTree ...@@ -34,8 +34,12 @@ export class SelectTree extends SvgTree
readOnlyMode: false, readOnlyMode: false,
showIcons: true, showIcons: true,
marginTop: 15, marginTop: 15,
nodeHeight: 20, nodeHeight: 26,
indentWidth: 16, icon: {
size: 16,
containerSize: 20,
},
indentWidth: 20,
width: 300, width: 300,
duration: 400, duration: 400,
dataUrl: '', dataUrl: '',
...@@ -183,7 +187,11 @@ export class SelectTree extends SvgTree ...@@ -183,7 +187,11 @@ export class SelectTree extends SvgTree
}) })
.append('g') .append('g')
.attr('class', 'tree-check') .attr('class', 'tree-check')
.on('click', (evt: MouseEvent, node: TreeNode) => this.selectNode(node)); .on('click', (evt: MouseEvent, node: TreeNode) => {
this.selectNode(node);
this.focusNode(node);
this.updateVisibleNodes();
});
g.append('use') g.append('use')
.attr('x', 28) .attr('x', 28)
......
...@@ -54,6 +54,7 @@ export class EditablePageTree extends PageTree { ...@@ -54,6 +54,7 @@ export class EditablePageTree extends PageTree {
public selectFirstNode(): void { public selectFirstNode(): void {
this.selectNode(this.nodes[0], true); this.selectNode(this.nodes[0], true);
this.focusNode(this.nodes[0]);
} }
public sendChangeCommand(data: any): void { public sendChangeCommand(data: any): void {
...@@ -99,10 +100,10 @@ export class EditablePageTree extends PageTree { ...@@ -99,10 +100,10 @@ export class EditablePageTree extends PageTree {
/** /**
* Make the DOM element of the node given as parameter focusable and focus it * Make the DOM element of the node given as parameter focusable and focus it
*/ */
public switchFocusNode(node: TreeNode) { public focusNode(node: TreeNode) {
// Focus node only if it's not currently in edit mode // Focus node only if it's not currently in edit mode
if (!this.nodeIsEdit) { if (!this.nodeIsEdit) {
this.switchFocus(this.getNodeElement(node)); super.focusNode(node);
} }
} }
...@@ -142,12 +143,14 @@ export class EditablePageTree extends PageTree { ...@@ -142,12 +143,14 @@ export class EditablePageTree extends PageTree {
.on('click', (event, node: TreeNode) => { .on('click', (event, node: TreeNode) => {
if (node.identifier === '0') { if (node.identifier === '0') {
this.selectNode(node, true); this.selectNode(node, true);
this.focusNode(node);
return; return;
} }
if (++clicks === 1) { if (++clicks === 1) {
setTimeout(() => { setTimeout(() => {
if (clicks === 1) { if (clicks === 1) {
this.selectNode(node, true); this.selectNode(node, true);
this.focusNode(node);
} else { } else {
this.editNodeLabel(node); this.editNodeLabel(node);
} }
...@@ -189,18 +192,20 @@ export class EditablePageTree extends PageTree { ...@@ -189,18 +192,20 @@ export class EditablePageTree extends PageTree {
if (!node.allowEdit) { if (!node.allowEdit) {
return; return;
} }
this.disableFocusedNodes();
node.focused = true
this.updateVisibleNodes();
this.removeEditedText(); this.removeEditedText();
this.nodeIsEdit = true; this.nodeIsEdit = true;
d3selection.select(this.svg.node().parentNode as HTMLElement) d3selection.select(this.svg.node().parentNode as HTMLElement)
.append('input') .append('input')
.attr('class', 'node-edit') .attr('class', 'node-edit')
.style('top', () => { .style('top', (node.y + this.settings.marginTop) + 'px')
const top = node.y + this.settings.marginTop;
return top + 'px';
})
.style('left', (node.x + this.textPosition + 5) + 'px') .style('left', (node.x + this.textPosition + 5) + 'px')
.style('width', this.settings.width - (node.x + this.textPosition + 20) + 'px') .style('width', 'calc(100% - ' + (node.x + this.textPosition + 5) + 'px)')
.style('height', this.settings.nodeHeight + 'px') .style('height', this.settings.nodeHeight + 'px')
.attr('type', 'text') .attr('type', 'text')
.attr('value', node.name) .attr('value', node.name)
...@@ -222,6 +227,7 @@ export class EditablePageTree extends PageTree { ...@@ -222,6 +227,7 @@ export class EditablePageTree extends PageTree {
this.nodeIsEdit = false; this.nodeIsEdit = false;
this.removeEditedText(); this.removeEditedText();
} }
this.focusNode(node);
}) })
.on('blur', (evt: FocusEvent) => { .on('blur', (evt: FocusEvent) => {
if (!this.nodeIsEdit) { if (!this.nodeIsEdit) {
...@@ -235,6 +241,7 @@ export class EditablePageTree extends PageTree { ...@@ -235,6 +241,7 @@ export class EditablePageTree extends PageTree {
this.sendEditNodeLabelCommand(node); this.sendEditNodeLabelCommand(node);
} }
this.removeEditedText(); this.removeEditedText();
this.focusNode(node);
}) })
.node() .node()