Repository: sveltejs/svelte (JavaScript)
Date: 2021-08-01 11:39:33+00:00
Task type: Bug Fix

Issue:
Actions are not acceptable for <svelte:body />
Seems, we not able to assign actions to `<svelte:body />` element: [REPL](https://svelte.dev/repl/28c1ccc76d8040b18cce57d938823365?version=3.6.3)


Issue discussion:
The docs are pretty clear that `<svelte:body>` is only for adding events to `document.body`.

What would putting an action on `<svelte:body>` even do (that isn't easily accomplished otherwise)?
@btk5h In Svelte 3 we don't have custom events anymore (`events: {}` sections in Svelte 2). So, actions are the only way to add a custom event, for example, a group of DOM events (eg. click/touch/etc) or some special stuff like `use:enter={callback}` which is triggering only on the enter key press.

I think, if `<svelte:body>` is for adding events, it should apply actions too.

Solution patch:
diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md
--- a/site/content/docs/02-template-syntax.md
+++ b/site/content/docs/02-template-syntax.md
@@ -1586,7 +1586,9 @@ All except `scrollX` and `scrollY` are readonly.
 
 ---
 
-As with `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave` which don't fire on `window`; and it has to appear at the top level of your component.
+Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](docs#use_action) on the `<body>` element.
+
+`<svelte:body>` also has to appear at the top level of your component.
 
 ```sv
 <svelte:body
diff --git a/src/compiler/compile/nodes/Body.ts b/src/compiler/compile/nodes/Body.ts
--- a/src/compiler/compile/nodes/Body.ts
+++ b/src/compiler/compile/nodes/Body.ts
@@ -1,21 +1,23 @@
 import Node from './shared/Node';
 import EventHandler from './EventHandler';
+import Action from './Action';
 import Component from '../Component';
 import TemplateScope from './shared/TemplateScope';
-import { TemplateNode } from '../../interfaces';
+import { Element } from '../../interfaces';
 
 export default class Body extends Node {
 	type: 'Body';
-	handlers: EventHandler[];
+	handlers: EventHandler[] = [];
+	actions: Action[] = [];
 
-	constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
+	constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) {
 		super(component, parent, scope, info);
 
-		this.handlers = [];
-
-		info.attributes.forEach((node: Node) => {
+		info.attributes.forEach((node) => {
 			if (node.type === 'EventHandler') {
 				this.handlers.push(new EventHandler(component, this, scope, node));
+			} else if (node.type === 'Action') {
+				this.actions.push(new Action(component, this, scope, node));
 			} else {
 				// TODO there shouldn't be anything else here...
 			}
diff --git a/src/compiler/compile/render_dom/wrappers/Body.ts b/src/compiler/compile/render_dom/wrappers/Body.ts
--- a/src/compiler/compile/render_dom/wrappers/Body.ts
+++ b/src/compiler/compile/render_dom/wrappers/Body.ts
@@ -7,6 +7,7 @@ import EventHandler from './Element/EventHandler';
 import add_event_handlers from './shared/add_event_handlers';
 import { TemplateNode } from '../../../interfaces';
 import Renderer from '../Renderer';
+import add_actions from './shared/add_actions';
 
 export default class BodyWrapper extends Wrapper {
 	node: Body;
@@ -19,5 +20,6 @@ export default class BodyWrapper extends Wrapper {
 
 	render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) {
 		add_event_handlers(block, x`@_document.body`, this.handlers);
+		add_actions(block, x`@_document.body`, this.node.actions);
 	}
 }
diff --git a/src/compiler/compile/render_dom/wrappers/shared/add_actions.ts b/src/compiler/compile/render_dom/wrappers/shared/add_actions.ts
--- a/src/compiler/compile/render_dom/wrappers/shared/add_actions.ts
+++ b/src/compiler/compile/render_dom/wrappers/shared/add_actions.ts
@@ -1,17 +1,18 @@
 import { b, x } from 'code-red';
 import Block from '../../Block';
 import Action from '../../../nodes/Action';
+import { Expression } from 'estree';
 import is_contextual from '../../../nodes/shared/is_contextual';
 
 export default function add_actions(
 	block: Block,
-	target: string,
+	target: string | Expression,
 	actions: Action[]
 ) {
 	actions.forEach(action => add_action(block, target, action));
 }
 
-export function add_action(block: Block, target: string, action: Action) {
+export function add_action(block: Block, target: string | Expression, action: Action) {
 	const { expression, template_scope } = action;
 	let snippet;
 	let dependencies;
diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts
--- a/src/compiler/interfaces.ts
+++ b/src/compiler/interfaces.ts
@@ -46,6 +46,23 @@ interface BaseDirective extends BaseNode {
 	modifiers: string[];
 }
 
+export interface Element extends BaseNode {
+	type: 'InlineComponent' | 'SlotTemplate' | 'Title' | 'Slot' | 'Element' | 'Head' | 'Options' | 'Window' | 'Body';
+	attributes: Array<BaseDirective | Attribute | SpreadAttribute>;
+	name: string;
+}
+
+export interface Attribute extends BaseNode {
+	type: 'Attribute';
+	name: string;
+	value: any[];
+}
+
+export interface SpreadAttribute extends BaseNode {
+	type: 'Spread';
+	expression: Node;
+}
+
 export interface Transition extends BaseDirective {
 	type: 'Transition';
 	intro: boolean;
@@ -57,6 +74,9 @@ export type Directive = BaseDirective | Transition;
 export type TemplateNode = Text
 | MustacheTag
 | BaseNode
+| Element
+| Attribute
+| SpreadAttribute
 | Directive
 | Transition
 | Comment;


Test patch:
diff --git a/test/runtime/samples/action-body/_config.js b/test/runtime/samples/action-body/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/action-body/_config.js
@@ -0,0 +1,18 @@
+export default {
+	html: '<div></div>',
+
+	async test({ assert, target, window }) {
+		const enter = new window.MouseEvent('mouseenter');
+		const leave = new window.MouseEvent('mouseleave');
+
+		await window.document.body.dispatchEvent(enter);
+		assert.htmlEqual(target.innerHTML, `
+			<div>
+				<div class="tooltip">Perform an Action</div>
+			</div>
+		`);
+
+		await window.document.body.dispatchEvent(leave);
+		assert.htmlEqual(target.innerHTML, '<div></div>');
+	}
+};
diff --git a/test/runtime/samples/action-body/main.svelte b/test/runtime/samples/action-body/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/action-body/main.svelte
@@ -0,0 +1,32 @@
+<script>
+	let container;
+	function tooltip(node, text) {
+		let tooltip = null;
+
+		function onMouseEnter() {
+			tooltip = document.createElement('div');
+			tooltip.classList.add('tooltip');
+			tooltip.textContent = text;
+			container.appendChild(tooltip);
+		}
+
+		function onMouseLeave() {
+			if (!tooltip) return;
+			tooltip.remove();
+			tooltip = null;
+		}
+
+		node.addEventListener('mouseenter', onMouseEnter);
+		node.addEventListener('mouseleave', onMouseLeave);
+		
+		return {
+			destroy() {
+				node.removeEventListener('mouseenter', onMouseEnter);
+				node.removeEventListener('mouseleave', onMouseLeave);
+			}
+		}
+	}
+</script>
+
+<svelte:body use:tooltip="{'Perform an Action'}" />
+<div bind:this={container} />
\ No newline at end of file