Skip to content

Add focus support to BrowserRenderer #17472

@SQL-MisterMagoo

Description

@SQL-MisterMagoo

Is 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

https://github.com/aspnet/AspNetCore/blob/32a2cc594363672ccfe7644a649f77a8bfc9c4a8/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts#L311

  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;
      }
    }
  }
```~~

Metadata

Metadata

Assignees

No one assigned

    Labels

    Components Big RockThis issue tracks a big effort which can span multiple issuesarea-blazorIncludes: Blazor, Razor ComponentsenhancementThis issue represents an ask for new feature or an enhancement to an existing one

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions