Skip to content

Commit 2063397

Browse files
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`.
1 parent 3457fe1 commit 2063397

File tree

4 files changed

+132
-87
lines changed

4 files changed

+132
-87
lines changed

src/presentation/assets/styles/_mixins.scss

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
@mixin hover-or-touch($selector-suffix: '', $selector-prefix: '&') {
22
@media (hover: hover) {
3-
4-
/* We only do this if hover is truly supported; otherwise the emulator in mobile
5-
keeps hovered style in-place even after touching, making it sticky. */
3+
/*
4+
Only apply hover styles if the device truly supports hover; otherwise the
5+
emulator in mobile keeps hovered style in-place even after touching, making it sticky.
6+
*/
67
#{$selector-prefix}:hover #{$selector-suffix} {
78
@content;
89
}
910
}
1011

1112
@media (hover: none) {
12-
13-
/* We only do this if hover is not supported,otherwise the desktop behavior is not
14-
as desired; it does not get activated on hover but only during click/touch. */
13+
/*
14+
Apply active styles on touch or click, ensuring interactive feedback on devices without hover capability.
15+
*/
1516
#{$selector-prefix}:active #{$selector-suffix} {
1617
@content;
1718
}
1819
}
1920
}
2021

22+
/*
23+
This mixin removes the default blue tap highlight seen in mobile WebKit browsers (e.g., Chrome, Safari, Edge).
24+
The mixin by itself may reduce accessibility by hiding this interactive cue. Therefore, it is recommended
25+
to use this mixin in conjunction with the `hover-or-touch` mixin to provide necessary visual feedback
26+
for interactive elements during hover or touch interactions.
27+
*/
2128
@mixin clickable($cursor: 'pointer') {
2229
cursor: #{$cursor};
2330
user-select: none;
24-
/*
25-
It removes (blue) background during touch as seen in mobile webkit browsers (Chrome, Safari, Edge).
26-
The default behavior is that any element (or containing element) that has cursor:pointer
27-
explicitly set and is clicked will flash blue momentarily.
28-
Removing it could have accessibility issue since that hides an interactive cue. But as we still provide
29-
response to user actions through :active by `hover-or-touch` mixin.
30-
*/
31-
-webkit-tap-highlight-color: transparent;
31+
-webkit-tap-highlight-color: transparent; // Removes blue tap highlight
3232
}
3333

3434
@mixin fade-transition($name) {

src/presentation/components/Scripts/View/Tree/TreeView/Node/HierarchicalTreeNode.vue

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
<template>
22
<div class="wrapper">
3-
<div
3+
<InteractableNode
44
class="expansible-node"
55
:style="{
66
'padding-left': `${currentNode.hierarchy.depthInTree * 24}px`,
77
}"
8+
:node-id="nodeId"
9+
:tree-root="treeRoot"
810
>
911
<div
1012
class="expand-collapse-arrow"
1113
:class="{
12-
expanded: expanded,
14+
expanded: isExpanded,
1315
'has-children': hasChildren,
1416
}"
1517
@click.stop="toggleExpand"
@@ -24,10 +26,10 @@
2426
</template>
2527
</LeafTreeNode>
2628
</div>
27-
</div>
29+
</InteractableNode>
2830
<transition name="children-transition">
2931
<ul
30-
v-if="hasChildren && expanded"
32+
v-if="hasChildren && isExpanded"
3133
class="children"
3234
>
3335
<HierarchicalTreeNode
@@ -54,12 +56,14 @@ import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStra
5456
import { useNodeState } from './UseNodeState';
5557
import { TreeNode } from './TreeNode';
5658
import LeafTreeNode from './LeafTreeNode.vue';
59+
import InteractableNode from './InteractableNode.vue';
5760
import type { PropType } from 'vue';
5861
5962
export default defineComponent({
6063
name: 'HierarchicalTreeNode', // Needed due to recursion
6164
components: {
6265
LeafTreeNode,
66+
InteractableNode,
6367
},
6468
props: {
6569
nodeId: {
@@ -82,7 +86,7 @@ export default defineComponent({
8286
);
8387
8488
const { state } = useNodeState(currentNode);
85-
const expanded = computed<boolean>(() => state.value.isExpanded);
89+
const isExpanded = computed<boolean>(() => state.value.isExpanded);
8690
8791
const renderedNodeIds = computed<readonly string[]>(
8892
() => currentNode.value
@@ -96,18 +100,13 @@ export default defineComponent({
96100
currentNode.value.state.toggleExpand();
97101
}
98102
99-
function toggleCheck() {
100-
currentNode.value.state.toggleCheck();
101-
}
102-
103103
const hasChildren = computed<boolean>(
104104
() => currentNode.value.hierarchy.isBranchNode,
105105
);
106106
107107
return {
108108
renderedNodeIds,
109-
expanded,
110-
toggleCheck,
109+
isExpanded,
111110
toggleExpand,
112111
currentNode,
113112
hasChildren,
@@ -123,7 +122,6 @@ export default defineComponent({
123122
.wrapper {
124123
display: flex;
125124
flex-direction: column;
126-
cursor: pointer;
127125
128126
.children {
129127
@include reset-ul;
@@ -140,16 +138,15 @@ export default defineComponent({
140138
141139
flex-direction: row;
142140
align-items: center;
143-
@include hover-or-touch {
144-
background: $color-node-highlight-bg;
145-
}
141+
146142
.expand-collapse-arrow {
147143
flex-shrink: 0;
148144
height: 30px;
149-
cursor: pointer;
150145
margin-left: 30px;
151146
width: 0;
152147
148+
@include clickable;
149+
153150
&:after {
154151
position: absolute;
155152
display: block;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<template>
2+
<div
3+
class="clickable-node focusable-node"
4+
tabindex="-1"
5+
:class="{
6+
'keyboard-focus': hasKeyboardFocus,
7+
}"
8+
@click.stop="toggleCheckState"
9+
@focus="onNodeFocus"
10+
>
11+
<slot />
12+
</div>
13+
</template>
14+
15+
<script lang="ts">
16+
import { defineComponent, computed, toRef } from 'vue';
17+
import { TreeRoot } from '../TreeRoot/TreeRoot';
18+
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
19+
import { useNodeState } from './UseNodeState';
20+
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
21+
import { TreeNode } from './TreeNode';
22+
import type { PropType } from 'vue';
23+
24+
export default defineComponent({
25+
props: {
26+
nodeId: {
27+
type: String,
28+
required: true,
29+
},
30+
treeRoot: {
31+
type: Object as PropType<TreeRoot>,
32+
required: true,
33+
},
34+
},
35+
setup(props) {
36+
const { isKeyboardBeingUsed } = useKeyboardInteractionState();
37+
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
38+
const currentNode = computed<TreeNode>(() => nodes.value.getNodeById(props.nodeId));
39+
const { state } = useNodeState(currentNode);
40+
41+
const hasKeyboardFocus = computed<boolean>(() => {
42+
if (!isKeyboardBeingUsed.value) {
43+
return false;
44+
}
45+
return state.value.isFocused;
46+
});
47+
48+
const onNodeFocus = () => {
49+
props.treeRoot.focus.setSingleFocus(currentNode.value);
50+
};
51+
52+
function toggleCheckState() {
53+
currentNode.value.state.toggleCheck();
54+
}
55+
56+
return {
57+
onNodeFocus,
58+
toggleCheckState,
59+
currentNode,
60+
hasKeyboardFocus,
61+
};
62+
},
63+
});
64+
</script>
65+
66+
<style scoped lang="scss">
67+
@use "@/presentation/assets/styles/main" as *;
68+
@use "./../tree-colors" as *;
69+
70+
.clickable-node {
71+
@include clickable;
72+
@include hover-or-touch {
73+
background: $color-node-highlight-bg;
74+
}
75+
}
76+
77+
.focusable-node {
78+
outline: none; // We handle keyboard focus through own styling
79+
&.keyboard-focus {
80+
background: $color-node-highlight-bg;
81+
}
82+
}
83+
</style>

src/presentation/components/Scripts/View/Tree/TreeView/Node/LeafTreeNode.vue

Lines changed: 22 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,41 @@
11
<template>
2-
<li
3-
class="node focusable"
4-
tabindex="-1"
5-
:class="{
6-
'keyboard-focus': hasKeyboardFocus,
7-
}"
8-
@click.stop="toggleCheckState"
9-
@focus="onNodeFocus"
10-
>
11-
<div class="node__layout">
12-
<div class="node__checkbox">
13-
<NodeCheckbox
14-
:node-id="nodeId"
15-
:tree-root="treeRoot"
16-
/>
2+
<li>
3+
<InteractableNode
4+
:node-id="nodeId"
5+
:tree-root="treeRoot"
6+
class="node"
7+
>
8+
<div class="node__layout">
9+
<div class="node__checkbox">
10+
<NodeCheckbox
11+
:node-id="nodeId"
12+
:tree-root="treeRoot"
13+
/>
14+
</div>
15+
<div class="node__content content">
16+
<slot
17+
name="node-content"
18+
:node-metadata="currentNode.metadata"
19+
/>
20+
</div>
1721
</div>
18-
<div class="node__content content">
19-
<slot
20-
name="node-content"
21-
:node-metadata="currentNode.metadata"
22-
/>
23-
</div>
24-
</div>
22+
</InteractableNode>
2523
</li>
2624
</template>
2725

2826
<script lang="ts">
2927
import { defineComponent, computed, toRef } from 'vue';
3028
import { TreeRoot } from '../TreeRoot/TreeRoot';
3129
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
32-
import { useNodeState } from './UseNodeState';
33-
import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
3430
import { TreeNode } from './TreeNode';
3531
import NodeCheckbox from './NodeCheckbox.vue';
32+
import InteractableNode from './InteractableNode.vue';
3633
import type { PropType } from 'vue';
3734
3835
export default defineComponent({
3936
components: {
4037
NodeCheckbox,
38+
InteractableNode,
4139
},
4240
props: {
4341
nodeId: {
@@ -50,31 +48,11 @@ export default defineComponent({
5048
},
5149
},
5250
setup(props) {
53-
const { isKeyboardBeingUsed } = useKeyboardInteractionState();
5451
const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
5552
const currentNode = computed<TreeNode>(() => nodes.value.getNodeById(props.nodeId));
56-
const { state } = useNodeState(currentNode);
57-
58-
const hasKeyboardFocus = computed<boolean>(() => {
59-
if (!isKeyboardBeingUsed.value) {
60-
return false;
61-
}
62-
return state.value.isFocused;
63-
});
64-
65-
const onNodeFocus = () => {
66-
props.treeRoot.focus.setSingleFocus(currentNode.value);
67-
};
68-
69-
function toggleCheckState() {
70-
currentNode.value.state.toggleCheck();
71-
}
7253
7354
return {
74-
onNodeFocus,
75-
toggleCheckState,
7655
currentNode,
77-
hasKeyboardFocus,
7856
};
7957
},
8058
});
@@ -97,27 +75,14 @@ export default defineComponent({
9775
overflow: auto; // Prevents horizontal expansion of inner content (e.g., when a code block is shown)
9876
}
9977
}
100-
101-
.focusable {
102-
outline: none; // We handle keyboard focus through own styling
103-
}
10478
.node {
10579
margin-bottom: 3px;
10680
margin-top: 3px;
10781
padding-bottom: 3px;
10882
padding-top: 3px;
10983
padding-right: 6px;
110-
cursor: pointer;
11184
box-sizing: border-box;
11285
113-
&.keyboard-focus {
114-
background: $color-node-highlight-bg;
115-
}
116-
117-
@include hover-or-touch {
118-
background: $color-node-highlight-bg;
119-
}
120-
12186
.content {
12287
display: flex; // We could provide `block`, but `flex` is more versatile.
12388
color: $color-node-fg;

0 commit comments

Comments
 (0)