Skip to content

Commit b3912fe

Browse files
authored
Merge pull request #149 from grapoza/fix-initial-focus-stealing
Fix initial focus stealing
2 parents f175f4c + 3903bb8 commit b3912fe

File tree

11 files changed

+109
-47
lines changed

11 files changed

+109
-47
lines changed

docsrc/generateDocs.ps1

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ if (-not [System.String]::IsNullOrEmpty($siteRoot)) {
99

1010
$metafile = (Join-Path -Path $PSScriptRoot -ChildPath "metadata.demo.yaml")
1111
# Replace TreeView package references
12-
(Get-Content $metafile) -replace 'http://localhost:8082', ( -Join ('https://unpkg.com/@grapoza/vue-tree@', $env:package_version)) | Set-Content $metafile
12+
# Replace full JS with minified
1313
# replace CSS paths
14-
(Get-Content $metafile) -replace '/style/demo/', ( -Join ($siteRoot, '/style/demo/')) | Set-Content $metafile
14+
(Get-Content $metafile) | ForEach-Object {
15+
$_ -replace 'http://localhost:8082', ( -Join ('https://unpkg.com/@grapoza/vue-tree@', $env:package_version)) `
16+
-replace 'dist/vue.js', 'dist/vue.min.js' `
17+
-replace '/vue-tree.umd.js', '/vue-tree.umd.min.js' `
18+
-replace '/style/demo/', ( -Join ($siteRoot, '/style/demo/'))
19+
} |
20+
Set-Content $metafile
1521
}
1622

1723
Get-ChildItem $PSScriptRoot\* -Recurse -Include *.md, *.css, *.js, *.png | Where-Object { -not $_.PsIsContainer -and $_.DirectoryName -notmatch 'output' } |

docsrc/metadata.demo.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
header-includes:
33
- |
44
```{=html}
5-
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
6-
<script src="http://localhost:8082/vue-tree.umd.min.js"></script>
5+
<script src="https://unpkg.com/vue/dist/vue.js"></script>
6+
<script src="http://localhost:8082/vue-tree.umd.js"></script>
77
<link rel="stylesheet" href="http://localhost:8082/vue-tree.css">
88
<link rel="stylesheet" href="/style/demo/demo.css">
99
<link rel="stylesheet" href="/style/demo/grayscale.css">

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "Yet another Vue treeview component.",
44
"author": "Gregg Rapoza <grapoza@gmail.com>",
55
"license": "MIT",
6-
"version": "2.0.0",
6+
"version": "2.0.1",
77
"main": "dist/vue-tree.umd.min.js",
88
"browser": "index.js",
99
"repository": {

src/components/TreeView.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
:initial-model="nodeModel"
1212
:selection-mode="selectionMode"
1313
:tree-id="uniqueId"
14+
:is-mounted="isMounted"
1415
:initial-radio-group-values="radioGroupValues"
1516
@treeViewNodeClick="(t, e)=>$emit('treeViewNodeClick', t, e)"
1617
@treeViewNodeDblclick="(t, e)=>$emit('treeViewNodeDblclick', t, e)"
@@ -82,7 +83,8 @@
8283
return {
8384
uniqueId: null,
8485
model: this.initialModel,
85-
radioGroupValues: {}
86+
radioGroupValues: {},
87+
isMounted: false
8688
};
8789
},
8890
computed: {
@@ -95,6 +97,13 @@
9597
mounted() {
9698
this.$_treeView_enforceSingleSelectionMode();
9799
this.$set(this, 'uniqueId', this.$el.id ? this.$el.id : null);
100+
101+
// Set this in a $nextTick so the focusable watcher
102+
// in TreeViewNodeAria fires before isMounted is set.
103+
// Otherwise, it steals focus when the tree is mounted.
104+
this.$nextTick(() => {
105+
this.isMounted = true;
106+
});
98107
},
99108
methods: {
100109
getCheckedCheckboxes() {

src/components/TreeViewNode.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
:tree-id="treeId"
152152
:initial-radio-group-values="radioGroupValues"
153153
:aria-key-map="ariaKeyMap"
154+
:is-mounted="isMounted"
154155
@treeViewNodeClick="(t, e)=>$emit('treeViewNodeClick', t, e)"
155156
@treeViewNodeDblclick="(t, e)=>$emit('treeViewNodeDblclick', t, e)"
156157
@treeViewNodeCheckboxChange="(t, e)=>$emit('treeViewNodeCheckboxChange', t, e)"
@@ -196,6 +197,10 @@
196197
type: Object,
197198
required: true
198199
},
200+
isMounted: {
201+
type: Boolean,
202+
required: true
203+
},
199204
modelDefaults: {
200205
type: Object,
201206
required: true
@@ -470,7 +475,7 @@
470475
this.$emit('treeViewNodeExpandedChange', this.model, event);
471476
},
472477
$_treeViewNode_toggleSelected(event) {
473-
// Note that selection change is already handled by the "model.focusable" watcher
478+
// Note that selection change is already handled by the "model.treeNodeSpec.focusable" watcher
474479
// method in TreeViewNodeAria if selectionMode is selectionFollowsFocus.
475480
if (this.model.treeNodeSpec.selectable && ['single', 'multiple'].includes(this.selectionMode)) {
476481
this.model.treeNodeSpec.state.selected = !this.model.treeNodeSpec.state.selected;

src/mixins/TreeViewAria.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default {
8585
this.$_treeView_enforceSingleSelectionMode();
8686
}
8787
else if (this.selectionMode === 'selectionFollowsFocus') {
88-
// Make sure the actual focused item is selected if the mode changes, and deselect all others
88+
// Make sure the actual focusable item is selected if the mode changes, and deselect all others
8989
this.$_treeView_depthFirstTraverse((node) => {
9090
let idPropName = node.treeNodeSpec.idProperty;
9191
let focusableIdPropName = this.focusableNodeModel.treeNodeSpec.idProperty;
@@ -101,11 +101,13 @@ export default {
101101
}
102102
},
103103
$_treeViewAria_handleFocusableChange(newNodeModel) {
104-
if (this.focusableNodeModel) {
105-
this.focusableNodeModel.treeNodeSpec.focusable = false;
106-
}
104+
if (this.focusableNodeModel !== newNodeModel) {
105+
if (this.focusableNodeModel) {
106+
this.focusableNodeModel.treeNodeSpec.focusable = false;
107+
}
107108

108-
this.$set(this, 'focusableNodeModel', newNodeModel);
109+
this.$set(this, 'focusableNodeModel', newNodeModel);
110+
}
109111
},
110112
$_treeViewAria_focusFirstNode() {
111113
this.model[0].treeNodeSpec.focusable = true;

src/mixins/TreeViewNodeAria.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ export default {
1515
},
1616
watch: {
1717
'model.treeNodeSpec.focusable': function(newValue) {
18-
// If focusable is set to true, also focus the treeitem element.
1918
if (newValue === true) {
20-
this.$el.focus();
19+
// If focusable is set to true and the tree is mounted in the DOM,
20+
// also focus the node's element.
21+
if (this.isMounted) {
22+
this.$el.focus();
23+
}
2124
this.$emit('treeViewNodeAriaFocusable', this.model);
2225
}
2326

tests/unit/TreeViewNode.customizations.spec.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const getDefaultPropsData = function () {
2323
modelDefaults: {},
2424
depth: 0,
2525
treeId: 'tree-id',
26-
initialRadioGroupValues: {}
26+
initialRadioGroupValues: {},
27+
isMounted: false
2728
}
2829
};
2930

@@ -80,7 +81,8 @@ describe('TreeViewNode.vue (customizations)', () => {
8081
depth: 0,
8182
treeId: 'tree',
8283
initialRadioGroupValues: {},
83-
selectionMode: 'single'
84+
selectionMode: 'single',
85+
isMounted: false
8486
});
8587
});
8688

@@ -196,7 +198,8 @@ describe('TreeViewNode.vue (customizations)', () => {
196198
modelDefaults: { customizations: { classes: customClasses } },
197199
depth: 0,
198200
treeId: 'tree',
199-
initialRadioGroupValues: {}
201+
initialRadioGroupValues: {},
202+
isMounted: false
200203
},
201204
{
202205
text: '<span :id="props.model.id" class="text-slot-content"><span class="slot-custom-classes">{{ JSON.stringify(props.customClasses) }}</span></span>',
@@ -231,7 +234,8 @@ describe('TreeViewNode.vue (customizations)', () => {
231234
modelDefaults: { customizations: { classes: customClasses } },
232235
depth: 0,
233236
treeId: 'tree',
234-
initialRadioGroupValues: {}
237+
initialRadioGroupValues: {},
238+
isMounted: false
235239
},
236240
{
237241
checkbox: `<span :id="props.model.id" class="text-slot-content">
@@ -278,7 +282,8 @@ describe('TreeViewNode.vue (customizations)', () => {
278282
modelDefaults: { customizations: { classes: customClasses } },
279283
depth: 0,
280284
treeId: 'tree',
281-
initialRadioGroupValues: {}
285+
initialRadioGroupValues: {},
286+
isMounted: false
282287
},
283288
{
284289
radio: `<span :id="props.model.id" class="text-slot-content">

tests/unit/TreeViewNode.interactions.spec.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const getDefaultPropsData = function () {
2424
depth: 0,
2525
treeId: 'tree-id',
2626
initialRadioGroupValues: {},
27+
isMounted: false,
2728
selectionMode: 'multiple'
2829
}
2930
};
@@ -116,7 +117,8 @@ describe('TreeViewNode.vue (interactions)', () => {
116117
modelDefaults: {},
117118
depth: 0,
118119
treeId: 'tree',
119-
initialRadioGroupValues: {}
120+
initialRadioGroupValues: {},
121+
isMounted: false
120122
});
121123

122124
expander = wrapper.find('#' + wrapper.vm.expanderId);
@@ -184,7 +186,8 @@ describe('TreeViewNode.vue (interactions)', () => {
184186
modelDefaults: {},
185187
depth: 0,
186188
treeId: 'tree',
187-
initialRadioGroupValues: {}
189+
initialRadioGroupValues: {},
190+
isMounted: false
188191
});
189192

190193
radioButton = wrapper.find('#' + wrapper.vm.inputId);
@@ -224,7 +227,8 @@ describe('TreeViewNode.vue (interactions)', () => {
224227
modelDefaults: {},
225228
depth: 0,
226229
treeId: 'tree',
227-
initialRadioGroupValues: {}
230+
initialRadioGroupValues: {},
231+
isMounted: false
228232
});
229233

230234
deleteButton = wrapper.find('#' + wrapper.vm.$children[0].nodeId + '-delete');
@@ -258,7 +262,8 @@ describe('TreeViewNode.vue (interactions)', () => {
258262
modelDefaults: {},
259263
depth: 0,
260264
treeId: 'tree',
261-
initialRadioGroupValues: {}
265+
initialRadioGroupValues: {},
266+
isMounted: false
262267
});
263268

264269
addChildButton = wrapper.find('#' + wrapper.vm.nodeId + '-add-child');
@@ -294,7 +299,8 @@ describe('TreeViewNode.vue (interactions)', () => {
294299
modelDefaults: {},
295300
depth: 0,
296301
treeId: 'tree',
297-
initialRadioGroupValues: {}
302+
initialRadioGroupValues: {},
303+
isMounted: false
298304
});
299305

300306
addChildButton = wrapper.find('#' + wrapper.vm.nodeId + '-add-child');

0 commit comments

Comments
 (0)