Skip to content

Commit 3903bb8

Browse files
committed
Fix focus stealing on mount
1 parent e15585a commit 3903bb8

File tree

8 files changed

+98
-42
lines changed

8 files changed

+98
-42
lines changed

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)