diff --git a/elements/index.d.ts b/elements/index.d.ts index ac32ae94c3..e7ed8901af 100644 --- a/elements/index.d.ts +++ b/elements/index.d.ts @@ -1078,11 +1078,17 @@ export interface SvelteMediaTimeRange { end: number; } +export interface SvelteDocumentAttributes extends HTMLAttributes { + readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null; + readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null; +} + export interface SvelteWindowAttributes extends HTMLAttributes { readonly 'bind:innerWidth'?: Window['innerWidth'] | undefined | null; readonly 'bind:innerHeight'?: Window['innerHeight'] | undefined | null; readonly 'bind:outerWidth'?: Window['outerWidth'] | undefined | null; readonly 'bind:outerHeight'?: Window['outerHeight'] | undefined | null; + readonly 'bind:devicePixelRatio'?: Window['devicePixelRatio'] | undefined | null; 'bind:scrollX'?: Window['scrollX'] | undefined | null; 'bind:scrollY'?: Window['scrollY'] | undefined | null; readonly 'bind:online'?: Window['navigator']['onLine'] | undefined | null; @@ -1591,7 +1597,7 @@ export interface SvelteHTMLElements { // Svelte specific 'svelte:window': SvelteWindowAttributes; - 'svelte:document': HTMLAttributes; + 'svelte:document': SvelteDocumentAttributes; 'svelte:body': HTMLAttributes; 'svelte:fragment': { slot?: string }; 'svelte:options': { [name: string]: any }; diff --git a/site/content/docs/02-template-syntax/05-element-directives.md b/site/content/docs/02-template-syntax/05-element-directives.md index 440cc6db92..f5b79f9210 100644 --- a/site/content/docs/02-template-syntax/05-element-directives.md +++ b/site/content/docs/02-template-syntax/05-element-directives.md @@ -291,6 +291,8 @@ Inputs that work together can use `bind:group`. ``` +> `bind:group` only works if the inputs are in the same Svelte component. + ## bind:this ```svelte diff --git a/site/content/docs/02-template-syntax/07-special-elements.md b/site/content/docs/02-template-syntax/07-special-elements.md index 5bf215a778..e2f49c4498 100644 --- a/site/content/docs/02-template-syntax/07-special-elements.md +++ b/site/content/docs/02-template-syntax/07-special-elements.md @@ -237,6 +237,7 @@ You can also bind to the following properties: - `scrollX` - `scrollY` - `online` — an alias for `window.navigator.onLine` +- `devicePixelRatio` All except `scrollX` and `scrollY` are readonly. @@ -252,6 +253,10 @@ All except `scrollX` and `scrollY` are readonly. ``` +```svelte + +``` + Similarly to ``, this element allows you to add listeners to events on `document`, such as `visibilitychange`, which don't fire on `window`. It also lets you use [actions](/docs/element-directives#use-action) on `document`. As with ``, this element may only appear the top level of your component and must never be inside a block or element. @@ -263,6 +268,13 @@ As with ``, this element may only appear the top level of your co /> ``` +You can also bind to the following properties: + +- `fullscreenElement` +- `visibilityState` + +All are readonly. + ## `` ```svelte diff --git a/src/compiler/compile/nodes/Binding.ts b/src/compiler/compile/nodes/Binding.ts index 303506222f..f655554c81 100644 --- a/src/compiler/compile/nodes/Binding.ts +++ b/src/compiler/compile/nodes/Binding.ts @@ -9,6 +9,7 @@ import { TemplateNode } from '../../interfaces'; import Element from './Element'; import InlineComponent from './InlineComponent'; import Window from './Window'; +import Document from './Document'; import { clone } from '../../utils/clone'; import compiler_errors from '../compiler_errors'; import compiler_warnings from '../compiler_warnings'; @@ -36,7 +37,7 @@ export default class Binding extends Node { is_contextual: boolean; is_readonly: boolean; - constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) { + constructor(component: Component, parent: Element | InlineComponent | Window | Document, scope: TemplateScope, info: TemplateNode) { super(component, parent, scope, info); if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { diff --git a/src/compiler/compile/nodes/Document.ts b/src/compiler/compile/nodes/Document.ts index 653ccb627b..60264aa40e 100644 --- a/src/compiler/compile/nodes/Document.ts +++ b/src/compiler/compile/nodes/Document.ts @@ -1,14 +1,24 @@ import Node from './shared/Node'; +import Binding from './Binding'; import EventHandler from './EventHandler'; +import fuzzymatch from '../../utils/fuzzymatch'; import Action from './Action'; import Component from '../Component'; +import list from '../../utils/list'; import TemplateScope from './shared/TemplateScope'; import { Element } from '../../interfaces'; import compiler_warnings from '../compiler_warnings'; +import compiler_errors from '../compiler_errors'; + +const valid_bindings = [ + 'fullscreenElement', + 'visibilityState' +]; export default class Document extends Node { type: 'Document'; handlers: EventHandler[] = []; + bindings: Binding[] = []; actions: Action[] = []; constructor(component: Component, parent: Node, scope: TemplateScope, info: Element) { @@ -17,6 +27,17 @@ export default class Document extends Node { info.attributes.forEach((node) => { if (node.type === 'EventHandler') { this.handlers.push(new EventHandler(component, this, scope, node)); + } else if (node.type === 'Binding') { + if (!~valid_bindings.indexOf(node.name)) { + const match = fuzzymatch(node.name, valid_bindings); + if (match) { + return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` (did you mean '${match}'?)`)); + } else { + return component.error(node, compiler_errors.invalid_binding_on(node.name, '', ` — valid bindings are ${list(valid_bindings)}`)); + } + } + + this.bindings.push(new Binding(component, this, scope, node)); } else if (node.type === 'Action') { this.actions.push(new Action(component, this, scope, node)); } else { diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 2410904d63..44d84f7566 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -121,6 +121,7 @@ const a11y_implicit_semantics = new Map([ ['details', 'group'], ['dt', 'term'], ['fieldset', 'group'], + ['figure', 'figure'], ['form', 'form'], ['h1', 'heading'], ['h2', 'heading'], @@ -132,6 +133,7 @@ const a11y_implicit_semantics = new Map([ ['img', 'img'], ['li', 'listitem'], ['link', 'link'], + ['main', 'main'], ['menu', 'list'], ['meter', 'progressbar'], ['nav', 'navigation'], @@ -142,6 +144,7 @@ const a11y_implicit_semantics = new Map([ ['progress', 'progressbar'], ['section', 'region'], ['summary', 'button'], + ['table', 'table'], ['tbody', 'rowgroup'], ['textarea', 'textbox'], ['tfoot', 'rowgroup'], @@ -631,9 +634,7 @@ export default class Element extends Node { } // no-redundant-roles - const has_redundant_role = current_role === get_implicit_role(this.name, attribute_map); - - if (this.name === current_role || has_redundant_role) { + if (current_role === get_implicit_role(this.name, attribute_map)) { component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(current_role)); } diff --git a/src/compiler/compile/nodes/Window.ts b/src/compiler/compile/nodes/Window.ts index c5bec0acd3..2f8a015d8a 100644 --- a/src/compiler/compile/nodes/Window.ts +++ b/src/compiler/compile/nodes/Window.ts @@ -17,6 +17,7 @@ const valid_bindings = [ 'outerHeight', 'scrollX', 'scrollY', + 'devicePixelRatio', 'online' ]; diff --git a/src/compiler/compile/render_dom/wrappers/Document.ts b/src/compiler/compile/render_dom/wrappers/Document.ts index 4f7c86c54f..0a9565e64c 100644 --- a/src/compiler/compile/render_dom/wrappers/Document.ts +++ b/src/compiler/compile/render_dom/wrappers/Document.ts @@ -1,6 +1,6 @@ import Block from '../Block'; import Wrapper from './shared/Wrapper'; -import { x } from 'code-red'; +import { b, x } from 'code-red'; import Document from '../../nodes/Document'; import { Identifier } from 'estree'; import EventHandler from './Element/EventHandler'; @@ -9,6 +9,16 @@ import { TemplateNode } from '../../../interfaces'; import Renderer from '../Renderer'; import add_actions from './shared/add_actions'; +const associated_events = { + fullscreenElement: ['fullscreenchange'], + visibilityState: ['visibilitychange'] +}; + +const readonly = new Set([ + 'fullscreenElement', + 'visibilityState' +]); + export default class DocumentWrapper extends Wrapper { node: Document; handlers: EventHandler[]; @@ -19,7 +29,66 @@ export default class DocumentWrapper extends Wrapper { } render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { + const { renderer } = this; + const { component } = renderer; + + const events: Record> = {}; + const bindings: Record = {}; + add_event_handlers(block, x`@_document`, this.handlers); add_actions(block, x`@_document`, this.node.actions); + + this.node.bindings.forEach(binding => { + // TODO: what if it's a MemberExpression? + const binding_name = (binding.expression.node as Identifier).name; + + // in dev mode, throw if read-only values are written to + if (readonly.has(binding.name)) { + renderer.readonly.add(binding_name); + } + + bindings[binding.name] = binding_name; + + const binding_events = associated_events[binding.name]; + const property = binding.name; + + binding_events.forEach(associated_event => { + if (!events[associated_event]) events[associated_event] = []; + events[associated_event].push({ + name: binding_name, + value: property + }); + }); + }); + + Object.keys(events).forEach(event => { + const id = block.get_unique_name(`ondocument${event}`); + const props = events[event]; + + renderer.add_to_context(id.name); + const fn = renderer.reference(id.name); + + props.forEach(prop => { + renderer.meta_bindings.push( + b`this._state.${prop.name} = @_document.${prop.value};` + ); + }); + + block.event_listeners.push(x` + @listen(@_document, "${event}", ${fn}) + `); + + component.partly_hoisted.push(b` + function ${id}() { + ${props.map(prop => renderer.invalidate(prop.name, x`${prop.name} = @_document.${prop.value}`))} + } + `); + + block.chunks.init.push(b` + @add_render_callback(${fn}); + `); + + component.has_reactive_assignments = true; + }); } } diff --git a/src/compiler/compile/render_dom/wrappers/Window.ts b/src/compiler/compile/render_dom/wrappers/Window.ts index c98af18268..9e58bbcaad 100644 --- a/src/compiler/compile/render_dom/wrappers/Window.ts +++ b/src/compiler/compile/render_dom/wrappers/Window.ts @@ -14,6 +14,7 @@ const associated_events = { innerHeight: 'resize', outerWidth: 'resize', outerHeight: 'resize', + devicePixelRatio: 'resize', scrollX: 'scroll', scrollY: 'scroll' @@ -29,6 +30,7 @@ const readonly = new Set([ 'innerHeight', 'outerWidth', 'outerHeight', + 'devicePixelRatio', 'online' ]); diff --git a/test/runtime/samples/document-binding-fullscreen/_config.js b/test/runtime/samples/document-binding-fullscreen/_config.js new file mode 100644 index 0000000000..154ec0445a --- /dev/null +++ b/test/runtime/samples/document-binding-fullscreen/_config.js @@ -0,0 +1,31 @@ +export default { + before_test() { + Object.defineProperties(window.document, { + fullscreenElement: { + value: null, + configurable: true + } + }); + }, + + // copied from window-binding + // there's some kind of weird bug with this test... it compiles with the wrong require.extensions hook for some bizarre reason + skip_if_ssr: true, + + async test({ assert, target, window, component }) { + const event = new window.Event('fullscreenchange'); + + const div = target.querySelector('div'); + + Object.defineProperties(window.document, { + fullscreenElement: { + value: div, + configurable: true + } + }); + + window.document.dispatchEvent(event); + + assert.equal(component.fullscreen, div); + } +}; diff --git a/test/runtime/samples/document-binding-fullscreen/main.svelte b/test/runtime/samples/document-binding-fullscreen/main.svelte new file mode 100644 index 0000000000..5b00199821 --- /dev/null +++ b/test/runtime/samples/document-binding-fullscreen/main.svelte @@ -0,0 +1,7 @@ + + + + +
\ No newline at end of file diff --git a/test/runtime/samples/window-binding-resize/_config.js b/test/runtime/samples/window-binding-resize/_config.js index c99e92a07b..d7f0282147 100644 --- a/test/runtime/samples/window-binding-resize/_config.js +++ b/test/runtime/samples/window-binding-resize/_config.js @@ -1,5 +1,5 @@ export default { - html: '
1024x768
', + html: '
1024x768
1
', before_test() { Object.defineProperties(window, { @@ -10,6 +10,10 @@ export default { innerHeight: { value: 768, configurable: true + }, + devicePixelRatio: { + value: 1, + configurable: true } }); }, @@ -27,13 +31,17 @@ export default { innerHeight: { value: 456, configurable: true + }, + devicePixelRatio: { + value: 2, + configurable: true } }); await window.dispatchEvent(event); assert.htmlEqual(target.innerHTML, ` -
567x456
+
567x456
2
`); } }; diff --git a/test/runtime/samples/window-binding-resize/main.svelte b/test/runtime/samples/window-binding-resize/main.svelte index 405f4e6e23..8ece184416 100644 --- a/test/runtime/samples/window-binding-resize/main.svelte +++ b/test/runtime/samples/window-binding-resize/main.svelte @@ -1,8 +1,10 @@ - + -
{width}x{height}
\ No newline at end of file +
{width}x{height}
+
{devicePixelRatio}
diff --git a/test/validator/samples/a11y-no-redundant-roles/input.svelte b/test/validator/samples/a11y-no-redundant-roles/input.svelte index 05525effb6..537d5c0fd3 100644 --- a/test/validator/samples/a11y-no-redundant-roles/input.svelte +++ b/test/validator/samples/a11y-no-redundant-roles/input.svelte @@ -41,4 +41,8 @@
-
\ No newline at end of file +
+ + + + diff --git a/test/validator/samples/window-binding-invalid/errors.json b/test/validator/samples/window-binding-invalid/errors.json index 1277984258..04ecbaafcd 100644 --- a/test/validator/samples/window-binding-invalid/errors.json +++ b/test/validator/samples/window-binding-invalid/errors.json @@ -1,6 +1,6 @@ [{ "code": "invalid-binding", - "message": "'potato' is not a valid binding on — valid bindings are innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY or online", + "message": "'potato' is not a valid binding on — valid bindings are innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY, devicePixelRatio or online", "start": { "line": 1, "column": 15