Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-05 07:36:25+00:00
Task type: Bug Fix

Issue:
autofocus HTML attribute not working with curly braces JavaScript expression
**Describe the bug**
The `autofocus` HTML attribute is not working with curly braces JavaScript expression

**Logs**
Please include browser console and server logs around the time this bug occurred.

**To Reproduce**

Check [this repl ](https://svelte.dev/repl/c11942342a554a0e8df4b28d4248e313?version=3.23.1) and this other [repl](https://svelte.dev/repl/dba0f324a0a14bdfb0df7d1f7af4a2e6?version=3.23.1) with a simpler example.

Steps to reproduce:

Create the following svelte component

```
<script>
	let autofocus = true
</script>

input to focus: <input {autofocus}/>
```

**Expected behavior**
The autofocus attribute to work as expected

**Severity**
annoying, there are workarounds to deal with autofoccus programmatically


Issue discussion:
@opensas can confirm this looks like a bug, but you should also be aware that Svelte is very accessibility focused, and autofocus is an accessibility no-no.
Thanks for the reply @antony , I'm aware of svelte accessibility focused (see what I did?) attitude.

What would be the best way to handle autofocus in a a11y friendly way? The way I see it, programatics option to achieve autofocus (element.focus()) will probable have the same problem.
At some point in the past, Svelte used to handle `autofocus` attributes specially and compile them to `el.focus()` calls, but that appears to have broken.
I seem to be wrong about that. A static `autofocus` attribute results in an `el.focus()` inside the mount function for the block, but a dynamic value was never handled it doesn't look like. Presumably we'd want this to compile to `if (dynamic_expression) { el.focus(); }`.
Excuse my ignorance, but I'm inspecting it in the [repl](https://svelte.dev/repl/dba0f324a0a14bdfb0df7d1f7af4a2e6?version=3.23.1) I posted and in all cases I see the following html being generated:

```html
<body>input to focus: <input autofocus=""><br></body>
```

It's the same wether I execute:

```
<script>
	let autofocus = true
</script>

input to focus: <input autofocus/><br />
``` 

or

```
<script>
	let autofocus = true
</script>

input to focus: <input {autofocus}/><br />
```

In the first case it works, but in the second it won't.

I don't quite get how running `el.focus()` is related to the `autofocus=""` appearing in the dom 
Programmatically creating an element and giving it the `autofocus` attribute does not cause the browser to focus the element. I'm guessing the presence of the attribute is only checked once when first loading the page. To provide more helpful behavior, Svelte has special handling to add a manual `el.focus()` call after an element is created, but it's only currently working in cases with a static `autofocus` attribute.
Absolutely clear

I had a look at the genarated js and it's even more clear what's happening:

https://svelte.dev/repl/dba0f324a0a14bdfb0df7d1f7af4a2e6?version=3.23.1

`input to focus: <input {autofocus}/><br />`:

```
	return {
		c() {
			t = text("input to focus: ");
			input = element("input");
			br = element("br");
			input.autofocus = autofocus;
		},
		m(target, anchor) {
			insert(target, t, anchor);
			insert(target, input, anchor);
			insert(target, br, anchor);
		},
		p: noop,
		i: noop,
		o: noop,
		d(detaching) {
			if (detaching) detach(t);
			if (detaching) detach(input);
			if (detaching) detach(br);
		}
	}
```

`input to focus: <input autofocus/><br />`:

```
		m(target, anchor) {
			insert(target, t, anchor);
			insert(target, input, anchor);
			insert(target, br, anchor);
			input.focus();
		},
```

so efectively the input.focus() is missing when using js expressions
I didn't even know this attribute existed. If anyone sees this and needs a quick fix:

```
<script>
  const focus = node => node.focus();
</script>
<input use:focus type="text">
```
Actions for the win


Solution patch:
diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts
--- a/src/compiler/compile/render_dom/Block.ts
+++ b/src/compiler/compile/render_dom/Block.ts
@@ -71,7 +71,7 @@ export default class Block {
 	get_unique_name: (name: string) => Identifier;
 
 	has_update_method = false;
-	autofocus: string;
+	autofocus?: { element_var: string, condition_expression?: any };
 
 	constructor(options: BlockOptions) {
 		this.parent = options.parent;
@@ -239,7 +239,11 @@ export default class Block {
 		}
 
 		if (this.autofocus) {
-			this.chunks.mount.push(b`${this.autofocus}.focus();`);
+			if (this.autofocus.condition_expression) {
+				this.chunks.mount.push(b`if (${this.autofocus.condition_expression}) ${this.autofocus.element_var}.focus();`);
+			} else {
+				this.chunks.mount.push(b`${this.autofocus.element_var}.focus();`);
+			}
 		}
 
 		this.render_listeners();
diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
@@ -169,8 +169,11 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
 		}
 
 		// special case – autofocus. has to be handled in a bit of a weird way
-		if (this.node.is_true && name === 'autofocus') {
-			block.autofocus = element.var;
+		if (name === 'autofocus') {
+			block.autofocus = {
+				element_var: element.var,
+				condition_expression: this.node.is_true ? undefined : value
+			};
 		}
 	}
 
diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/index.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts
@@ -679,6 +679,12 @@ export default class ElementWrapper extends Wrapper {
 				`);
 			}
 		}
+
+		if (['button', 'input', 'keygen', 'select', 'textarea'].includes(this.node.name)) {
+			block.chunks.mount.push(b`
+				if (${this.var}.autofocus) ${this.var}.focus();
+			`);
+		}
 	}
 
 	add_transitions(


Test patch:
diff --git a/test/runtime/samples/autofocus/_config.js b/test/runtime/samples/autofocus/_config.js
--- a/test/runtime/samples/autofocus/_config.js
+++ b/test/runtime/samples/autofocus/_config.js
@@ -2,7 +2,23 @@ export default {
 	html: '',
 
 	test({ assert, component, target, window }) {
-		component.visible = true;
-		assert.equal(target.querySelector('input'), window.document.activeElement);
+		component.active = 'default';
+		assert.equal(target.querySelector('input[title="default"]'), window.document.activeElement);
+
+		component.active = 'dynamic-false';
+		assert.notEqual(target.querySelector('input[title="dynamic-false"]'), window.document.activeElement);
+
+		// when dynamically set autofocus to true, don't autofocus
+		component.autofocusFalse = true;
+		assert.notEqual(target.querySelector('input[title="dynamic-false"]'), window.document.activeElement);
+
+		component.active = 'dynamic-true';
+		assert.equal(target.querySelector('input[title="dynamic-true"]'), window.document.activeElement);
+
+		component.active = 'spread';
+		assert.equal(target.querySelector('input[title="spread"]'), window.document.activeElement);
+
+		component.active = 'spread-override';
+		assert.notEqual(target.querySelector('input[title="spread-override"]'), window.document.activeElement);
 	}
 };
diff --git a/test/runtime/samples/autofocus/main.svelte b/test/runtime/samples/autofocus/main.svelte
--- a/test/runtime/samples/autofocus/main.svelte
+++ b/test/runtime/samples/autofocus/main.svelte
@@ -1,8 +1,18 @@
 <script>
-	export let visible = false;
-	let input;
+	export let active = 'default';
+	export let autofocusFalse = false;
+	export let autofocusTrue = true;
+	let spread = { autofocus: true };
 </script>
 
-{#if visible}
-	<input bind:this={input} autofocus>
+{#if active === 'default'}
+	<input title={active} autofocus />
+{:else if active === 'dynamic-false'}
+	<input title={active} autofocus={autofocusFalse} />
+{:else if active === 'dynamic-true'}
+	<input title={active} autofocus={autofocusTrue} />
+{:else if active === 'spread'}
+	<input title={active} {...spread} />
+{:else if active === 'spread-override'}
+	<input title={active} {...spread} autofocus={false} />
 {/if}
\ No newline at end of file