For the record, I did get this working by storing the focus state in the plugin state. The code now looks like this:
const key = new PluginKey('selectionSize');
let selectionSizePlugin = new Plugin({
key,
state: {
init() {
return false; // assume the view starts out without focus
},
apply(transaction, prevFocused) {
// update plugin state when transaction contains the meta property
// set by the focus/blur DOM event handlers
let focused = transaction.getMeta(key);
return typeof focused === 'boolean' ? focused : prevFocused;
}
},
view(editorView) { return new SelectionSizeTooltip(editorView) },
props: {
handleDOMEvents: {
blur(view) { view.dispatch(view.state.tr.setMeta(key, false)) },
focus(view) { view.dispatch(view.state.tr.setMeta(key, true)) }
}
}
})
class SelectionSizeTooltip {
// constructor see https://prosemirror.net/examples/tooltip/
update(view, lastState) {
let state = view.state
let focused = key.getState(state);
// Don't do anything if the document, selection, and focus didn't change
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection) &&
focused === key.getState(lastState)) return
// Hide the tooltip if the selection is empty
if (state.selection.empty || !focused) {
this.tooltip.style.display = "none"
return
}
// rest of the code see https://prosemirror.net/examples/tooltip/
}
}
Now I am moving the focus state plugin into its own plugin so that it can easily be reused by other similar plugins. But the general principle works great as far as I can tell. Thanks for the help.