Fix touch-enabled Chromium highlight on tree nodes

This commit resolves issues with the touch highlight behavior on tree
nodes in touch-enabled Chromium browsers (such as Google Chrome).

The fix addresses two issues:

1. Dual color transition issue during tapping actions on tree nodes.
2. Not highlighting full visible width of the node on keyboard focus.

Other changes include:

- Create `InteractableNode.vue` to centralize click styling and logic.
- Remove redundant click/hover/touch styling from `LeafTreeNode.vue` and
  `HierarchicalTreeNode.vue`.
This commit is contained in:
undergroundwires
2023-12-15 08:00:46 +01:00
parent 3457fe18cf
commit 20633972e9
4 changed files with 132 additions and 87 deletions

View File

@@ -1,15 +1,17 @@
<template>
<div class="wrapper">
<div
<InteractableNode
class="expansible-node"
:style="{
'padding-left': `${currentNode.hierarchy.depthInTree * 24}px`,
}"
:node-id="nodeId"
:tree-root="treeRoot"
>
<div
class="expand-collapse-arrow"
:class="{
expanded: expanded,
expanded: isExpanded,
'has-children': hasChildren,
}"
@click.stop="toggleExpand"
@@ -24,10 +26,10 @@
</template>
</LeafTreeNode>
</div>
</div>
</InteractableNode>
<transition name="children-transition">
<ul
v-if="hasChildren && expanded"
v-if="hasChildren && isExpanded"
class="children"
>
<HierarchicalTreeNode
@@ -54,12 +56,14 @@ import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStra
import { useNodeState } from './UseNodeState';
import { TreeNode } from './TreeNode';
import LeafTreeNode from './LeafTreeNode.vue';
import InteractableNode from './InteractableNode.vue';
import type { PropType } from 'vue';
export default defineComponent({
name: 'HierarchicalTreeNode', // Needed due to recursion
components: {
LeafTreeNode,
InteractableNode,
},
props: {
nodeId: {
@@ -82,7 +86,7 @@ export default defineComponent({
);
const { state } = useNodeState(currentNode);
const expanded = computed<boolean>(() => state.value.isExpanded);
const isExpanded = computed<boolean>(() => state.value.isExpanded);
const renderedNodeIds = computed<readonly string[]>(
() => currentNode.value
@@ -96,18 +100,13 @@ export default defineComponent({
currentNode.value.state.toggleExpand();
}
function toggleCheck() {
currentNode.value.state.toggleCheck();
}
const hasChildren = computed<boolean>(
() => currentNode.value.hierarchy.isBranchNode,
);
return {
renderedNodeIds,
expanded,
toggleCheck,
isExpanded,
toggleExpand,
currentNode,
hasChildren,
@@ -123,7 +122,6 @@ export default defineComponent({
.wrapper {
display: flex;
flex-direction: column;
cursor: pointer;
.children {
@include reset-ul;
@@ -140,16 +138,15 @@ export default defineComponent({
flex-direction: row;
align-items: center;
@include hover-or-touch {
background: $color-node-highlight-bg;
}
.expand-collapse-arrow {
flex-shrink: 0;
height: 30px;
cursor: pointer;
margin-left: 30px;
width: 0;
@include clickable;
&:after {
position: absolute;
display: block;

View File

@@ -0,0 +1,83 @@
<template>
<div
class="clickable-node focusable-node"
tabindex="-1"
:class="{
'keyboard-focus': hasKeyboardFocus,
}"
@click.stop="toggleCheckState"
@focus="onNodeFocus"
>
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent, computed, toRef } from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState';
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
import { TreeNode } from './TreeNode';
import type { PropType } from 'vue';
export default defineComponent({
props: {
nodeId: {
type: String,
required: true,
},
treeRoot: {
type: Object as PropType<TreeRoot>,
required: true,
},
},
setup(props) {
const { isKeyboardBeingUsed } = useKeyboardInteractionState();
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const currentNode = computed<TreeNode>(() => nodes.value.getNodeById(props.nodeId));
const { state } = useNodeState(currentNode);
const hasKeyboardFocus = computed<boolean>(() => {
if (!isKeyboardBeingUsed.value) {
return false;
}
return state.value.isFocused;
});
const onNodeFocus = () => {
props.treeRoot.focus.setSingleFocus(currentNode.value);
};
function toggleCheckState() {
currentNode.value.state.toggleCheck();
}
return {
onNodeFocus,
toggleCheckState,
currentNode,
hasKeyboardFocus,
};
},
});
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
@use "./../tree-colors" as *;
.clickable-node {
@include clickable;
@include hover-or-touch {
background: $color-node-highlight-bg;
}
}
.focusable-node {
outline: none; // We handle keyboard focus through own styling
&.keyboard-focus {
background: $color-node-highlight-bg;
}
}
</style>

View File

@@ -1,27 +1,25 @@
<template>
<li
class="node focusable"
tabindex="-1"
:class="{
'keyboard-focus': hasKeyboardFocus,
}"
@click.stop="toggleCheckState"
@focus="onNodeFocus"
>
<div class="node__layout">
<div class="node__checkbox">
<NodeCheckbox
:node-id="nodeId"
:tree-root="treeRoot"
/>
<li>
<InteractableNode
:node-id="nodeId"
:tree-root="treeRoot"
class="node"
>
<div class="node__layout">
<div class="node__checkbox">
<NodeCheckbox
:node-id="nodeId"
:tree-root="treeRoot"
/>
</div>
<div class="node__content content">
<slot
name="node-content"
:node-metadata="currentNode.metadata"
/>
</div>
</div>
<div class="node__content content">
<slot
name="node-content"
:node-metadata="currentNode.metadata"
/>
</div>
</div>
</InteractableNode>
</li>
</template>
@@ -29,15 +27,15 @@
import { defineComponent, computed, toRef } from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState';
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
import { TreeNode } from './TreeNode';
import NodeCheckbox from './NodeCheckbox.vue';
import InteractableNode from './InteractableNode.vue';
import type { PropType } from 'vue';
export default defineComponent({
components: {
NodeCheckbox,
InteractableNode,
},
props: {
nodeId: {
@@ -50,31 +48,11 @@ export default defineComponent({
},
},
setup(props) {
const { isKeyboardBeingUsed } = useKeyboardInteractionState();
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const currentNode = computed<TreeNode>(() => nodes.value.getNodeById(props.nodeId));
const { state } = useNodeState(currentNode);
const hasKeyboardFocus = computed<boolean>(() => {
if (!isKeyboardBeingUsed.value) {
return false;
}
return state.value.isFocused;
});
const onNodeFocus = () => {
props.treeRoot.focus.setSingleFocus(currentNode.value);
};
function toggleCheckState() {
currentNode.value.state.toggleCheck();
}
return {
onNodeFocus,
toggleCheckState,
currentNode,
hasKeyboardFocus,
};
},
});
@@ -97,27 +75,14 @@ export default defineComponent({
overflow: auto; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
}
}
.focusable {
outline: none; // We handle keyboard focus through own styling
}
.node {
margin-bottom: 3px;
margin-top: 3px;
padding-bottom: 3px;
padding-top: 3px;
padding-right: 6px;
cursor: pointer;
box-sizing: border-box;
&.keyboard-focus {
background: $color-node-highlight-bg;
}
@include hover-or-touch {
background: $color-node-highlight-bg;
}
.content {
display: flex; // We could provide `block`, but `flex` is more versatile.
color: $color-node-fg;