Add focus support to BrowserRenderer
See original GitHub issueIs your feature request related to a problem? Please describe.
To set focus on an element from Blazor, currently requires and ElementReference (or an id) and a JSInterop call in OnAfterRenderAsync. It would be nice to be able to do this declaratively by using the “autofocus” attribute.
This is a capability of HTML, but does not work for SPA applications where the elements are inserted into an existing DOM.
The addition of an ability to have autofocus
on newly created elements would make the SPA developer experience much simpler and provide a better result for the end user of the application.
Describe the solution you’d like
The Blazor application renders an element with the autofocus attribute and that triggers the BrowserRenderer to call the focus() method on the newly create element.
<button @onclick=@(MyClickHandler) autofocus>Click Me</button>
This should only cover initial element creation to maintain consistency with normal html autofocus
.
Additional context
The BrowserRenderer used in Blazor can be modified in a manner similar to this (proof of concept testing confirms this at a superficial level) to provide autofocus
on element creation.
private insertElement(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) {
const frameReader = batch.frameReader;
const tagName = frameReader.elementName(frame)!;
const newDomElementRaw = tagName === 'svg' || isSvgElement(parent) ?
document.createElementNS('http://www.w3.org/2000/svg', tagName) :
document.createElement(tagName);
const newElement = toLogicalElement(newDomElementRaw);
insertLogicalChild(newDomElementRaw, parent, childIndex);
+ // Handle autofocus
+ let wantsFocus: boolean = false;
// Apply attributes
const descendantsEndIndexExcl = frameIndex + frameReader.subtreeLength(frame);
for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) {
const descendantFrame = batch.referenceFramesEntry(frames, descendantIndex);
if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
+ // Handle autofocus
+ let attrName = batch.frameReader.attributeName(descendantFrame);
+ wantsFocus = ( attrName === 'autofocus' );
} else {
// As soon as we see a non-attribute child, all the subsequent child frames are
// not attributes, so bail out and insert the remnants recursively
this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
break;
}
}
+ if (wantsFocus) { // Handle autofocus
+ newDomElementRaw.focus();
+ }
// We handle setting 'value' on a <select> in two different ways:
// [1] When inserting a corresponding <option>, in case you're dynamically adding options
// [2] After we finish inserting the <select>, in case the descendant options are being
// added as an opaque markup block rather than individually
// Right here we implement [2]
if (newDomElementRaw instanceof HTMLSelectElement && selectValuePropname in newDomElementRaw) {
const selectValue = newDomElementRaw[selectValuePropname];
newDomElementRaw.value = selectValue;
delete newDomElementRaw[selectValuePropname];
}
}
Link to gist with full source : https://gist.github.com/SQL-MisterMagoo/949f2aff8aa0006ab6843bcedd14dd62/revisions
EDIT: 30/11/2019 Section below should be considered removed from this request as it was flawed
~~### Additional context
At this point in the code, it would be simple to add another case statement to handle autofocus
private tryApplySpecialProperty(batch: RenderBatch, element: Element, attributeName: string, attributeFrame: RenderTreeFrame | null) {
switch (attributeName) {
case 'value':
return this.tryApplyValueProperty(batch, element, attributeFrame);
case 'checked':
return this.tryApplyCheckedProperty(batch, element, attributeFrame);
/* ** Suggested addition ** */
case 'autofocus': {
element.focus();
return true;
}
default: {
if (attributeName.startsWith(internalAttributeNamePrefix)) {
this.applyInternalAttribute(batch, element, attributeName.substring(internalAttributeNamePrefix.length), attributeFrame);
return true;
}
return false;
}
}
}
```~~
Issue Analytics
- State:
- Created 4 years ago
- Reactions:21
- Comments:18 (15 by maintainers)
We’ll consider this feature during the next release planning period and update the status of this issue accordingly.
@ all I have modified the original message (I left in the original as people have “upvoted”)
The reason was that I finally got around to testing and realised my original suggestion was unworkable.
This alternative is meant as a jumping off point - for cleverer people than me to discuss, but minimal testing shows it to work very nicely.
@egil I think a separate issue for your suggestion would be a good thing and I would be happy to support it, but we are in danger of discussing two slightly different things in one issue, I think. If they are two separate issues, one can be closed without affecting the other.