diff --git a/_includes/header.html b/_includes/header.html
index b8969025..85142076 100644
--- a/_includes/header.html
+++ b/_includes/header.html
@@ -1,5 +1,5 @@
-
+
@@ -13,7 +13,7 @@
diff --git a/css/custom.css b/css/custom.css
index fefb1846..64ef07bd 100644
--- a/css/custom.css
+++ b/css/custom.css
@@ -14,7 +14,7 @@ body {
}
a {
- color: #6797e2;
+ color: #4772b5;
text-decoration: none;
}
@@ -1880,7 +1880,8 @@ dl dd {
}
table {
- display: block;
+ /* https://bugzilla.mozilla.org/show_bug.cgi?id=1005271 */
+ /* display: block; */
width: 100%;
overflow: auto;
}
@@ -1890,7 +1891,7 @@ table th {
}
table th, table td {
- padding: 6px 13px;
+ padding: 6px 11px;
border: 1px solid #ddd;
}
@@ -1957,9 +1958,15 @@ pre code::before, pre code::after {
content: normal;
}
+#feature-support-scrollbox {
+ overflow: auto;
+ margin: 0 0 16px 0;
+}
+
#feature-support {
white-space: nowrap;
cursor: default;
+ margin: 0;
}
#feature-support>caption {
@@ -1984,7 +1991,119 @@ pre code::before, pre code::after {
height: 32px;
}
-#feature-support td[title] {
- text-decoration: underline dashed;
- text-underline-position: under;
+#feature-support td {
+ position: relative; /* for tooltip */
+}
+
+#feature-support td:hover, #feature-support td:focus, #feature-support td:focus-within {
+ background: rgba(0, 0, 0, .04);
+}
+
+.feature-cell {
+ position: relative;
+ height: 24px; /* height of the icon inside */
+ line-height: 24px;
+}
+
+.feature-cell > sup {
+ font-size: .7em;
+ position: absolute;
+ top: .2em;
+}
+
+.feature-cell > svg {
+ width: 24px;
+ height: 24px;
+}
+
+.feature-cell.icon-yes {
+ color: #1B5E20;
+}
+
+.feature-cell.icon-yes > svg .svg-stroke {
+ fill: #1B5E20;
+}
+
+.feature-cell.icon-no {
+ color: #a96e8e;
+}
+
+.feature-cell.icon-no > svg .svg-stroke {
+ fill: #a96e8e;
+}
+
+.feature-cell.icon-flag {
+ color: #575581;
+}
+
+.feature-cell.icon-flag > svg .svg-stroke {
+ fill: #575581;
}
+
+.feature-cell.icon-na {
+ color: #78909C;
+}
+
+.feature-cell.icon-unknown > svg .svg-stroke {
+ fill: #78909C;
+}
+
+#feature-support-scrollbox + ol {
+ list-style: lower-alpha;
+ font-size: 0.7em;
+ margin: 0 0 1em 0;
+}
+
+#feature-support-scrollbox + ol > li {
+ transition: background-color .08s ease-in-out;
+}
+
+#feature-support-scrollbox + ol .ref-highlight {
+ background: #ECEFF1;
+}
+
+.feature-tooltip {
+ text-align: left;
+ text-align: start;
+ white-space: normal;
+ background: #fefefe;
+ font-size: 0.8em;
+ border-radius: 2px;
+ outline: none;
+
+ top: 0;
+ left: 0;
+ z-index: 1;
+ max-width: 16em;
+ width: max-content;
+ height: max-content;
+ padding: 12px;
+}
+
+/* Only apply transition after the initial position was set */
+.feature-tooltip[data-placement] {
+ transition: transform .2s ease-in-out;
+}
+
+.feature-tooltip, .feature-tooltip-arrow {
+ position: absolute;
+ contain: layout style;
+ --shadow-size: 3px;
+ box-shadow: 0 0 var(--shadow-size) rgba(0, 0, 0, .30);
+}
+
+.feature-tooltip-arrow {
+ --arrow-size: 8px;
+ background: inherit;
+ width: var(--arrow-size);
+ height: var(--arrow-size);
+
+ --c0: calc(var(--shadow-size) * -1);
+ --c1: calc(100% + var(--shadow-size));
+ clip-path: polygon(var(--c0) var(--c1), var(--c0) var(--c0), var(--c1) var(--c0));
+}
+
+[data-placement="top"] > .feature-tooltip-arrow { bottom: 0; transform: translateY(50%) rotate(-135deg); }
+[data-placement="bottom"] > .feature-tooltip-arrow { top: 0; transform: translateY(-50%) rotate(45deg); }
+[data-placement="left"] > .feature-tooltip-arrow { right: 0; transform: translateX(50%) rotate(135deg); }
+[data-placement="right"] > .feature-tooltip-arrow { left: 0; transform: translateX(-50%) rotate(-45deg); }
diff --git a/features.json b/features.json
index f8768ffb..b3f2a991 100644
--- a/features.json
+++ b/features.json
@@ -90,68 +90,65 @@
"Chrome": {
"url": "https://www.google.com/chrome/",
"logo": "/images/chrome.svg",
- "version": "96",
"features": {
- "bigInt": true,
- "bulkMemory": true,
- "exceptions": true,
- "multiValue": true,
- "mutableGlobals": true,
- "referenceTypes": true,
- "saturatedFloatToInt": true,
- "signExtensions": true,
- "simd": true,
- "tailCall": "#enable-experimental-webassembly-features",
- "threads": true
+ "bigInt": "85",
+ "bulkMemory": "75",
+ "exceptions": "95",
+ "multiValue": "85",
+ "mutableGlobals": "74",
+ "referenceTypes": "96",
+ "saturatedFloatToInt": "75",
+ "signExtensions": "74",
+ "simd": "91",
+ "tailCall": ["flag", "Requires flag `#enable-experimental-webassembly-features`"],
+ "threads": "74"
}
},
"Firefox": {
"url": "https://www.mozilla.org/firefox/",
"logo": "/images/firefox.svg",
- "version": "90",
"features": {
- "bigInt": true,
- "bulkMemory": true,
- "exceptions": true,
- "extendedConstantExprs": "Enabled in Nightly, unavailable in Beta/Release",
- "memory64": "Enabled in Nightly, unavailable in Beta/Release",
- "multiValue": true,
- "mutableGlobals": true,
- "referenceTypes": true,
- "relaxedSimd": "Enabled in Nightly, unavailable in Beta/Release",
- "saturatedFloatToInt": true,
- "signExtensions": true,
- "simd": true,
- "threads": true,
- "typeReflection": "Enabled in Nightly, unavailable in Beta/Release"
+ "bigInt": "78",
+ "bulkMemory": "79",
+ "exceptions": "100",
+ "extendedConstantExprs": ["flag", "Enabled in Nightly, unavailable in Beta/Release"],
+ "memory64": ["flag", "Enabled in Nightly, unavailable in Beta/Release"],
+ "multiValue": "78",
+ "mutableGlobals": "61",
+ "referenceTypes": "79",
+ "relaxedSimd": ["flag", "Enabled in Nightly, unavailable in Beta/Release"],
+ "saturatedFloatToInt": "64",
+ "signExtensions": "62",
+ "simd": "89",
+ "threads": "79",
+ "typeReflection": ["flag", "Enabled in Nightly, unavailable in Beta/Release"]
}
},
"Safari": {
"url": "https://www.apple.com/safari/",
- "logo": "/images/safari_48x48.png",
- "version": "15.2",
+ "logo": "/images/safari.svg",
"features": {
- "bigInt": true,
- "bulkMemory": true,
- "exceptions": true,
+ "bigInt": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"],
+ "bulkMemory": "15",
+ "exceptions": "15.2",
"multiValue": true,
"mutableGlobals": true,
- "referenceTypes": true,
- "saturatedFloatToInt": true,
- "signExtensions": true,
- "threads": true
+ "referenceTypes": "15",
+ "saturatedFloatToInt": "15",
+ "signExtensions": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"],
+ "threads": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"]
}
},
"Wasmtime": {
"url": "https://wasmtime.dev/",
- "logo": "/images/bca.png",
+ "logo": "/images/bca.svg",
"version": "0.33",
"features": {
"bigInt": null,
"bulkMemory": true,
- "memory64": "--enable-memory64",
- "multiMemory": "--enable-multi-memory",
- "moduleLinking": "--enable-module-linking",
+ "memory64": ["flag", "Requires flag `--enable-memory64`"],
+ "multiMemory": ["flag", "Requires flag `--enable-multi-memory`"],
+ "moduleLinking": ["flag", "Requires flag `--enable-module-linking`"],
"multiValue": true,
"mutableGlobals": true,
"referenceTypes": true,
@@ -162,8 +159,7 @@
},
"Wasmer": {
"url": "https://wasmer.io/",
- "logo": "/images/wasmer.png",
- "version": "2.0",
+ "logo": "/images/wasmer.svg",
"features": {
"bigInt": null,
"bulkMemory": true,
@@ -173,31 +169,29 @@
"saturatedFloatToInt": true,
"signExtensions": true,
"simd": true,
- "threads": "--enable-threads"
+ "threads": ["flag", "Requires flag `--enable-threads`"]
}
},
"Node.js": {
"url": "https://nodejs.org/",
"logo": "/images/nodejs.svg",
- "version": "16.4",
"features": {
"bigInt": true,
"bulkMemory": true,
- "exceptions": "--experimental-wasm-eh",
+ "exceptions": ["flag", "Requires flag `--experimental-wasm-eh`"],
"multiValue": true,
"mutableGlobals": true,
- "referenceTypes": "--experimental-wasm-reftypes",
+ "referenceTypes": ["flag", "Requires flag `--experimental-wasm-reftypes`"],
"saturatedFloatToInt": true,
"signExtensions": true,
"simd": true,
- "tailCall": "--experimental-wasm-return-call",
+ "tailCall": ["flag", "Requires flag `--experimental-wasm-return-call`"],
"threads": true
}
},
"Deno": {
"url": "https://deno.land/",
"logo": "/images/deno.svg",
- "version": "1.21.3",
"features": {
"bigInt": true,
"bulkMemory": true,
diff --git a/images/bca.png b/images/bca.png
deleted file mode 100644
index 9baadb36..00000000
Binary files a/images/bca.png and /dev/null differ
diff --git a/images/bca.svg b/images/bca.svg
new file mode 100644
index 00000000..d91371ef
--- /dev/null
+++ b/images/bca.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/chrome.svg b/images/chrome.svg
index 8d2d942e..381be82c 100644
--- a/images/chrome.svg
+++ b/images/chrome.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/images/edge.svg b/images/edge.svg
index e3707609..ad41c550 100644
--- a/images/edge.svg
+++ b/images/edge.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/images/firefox.svg b/images/firefox.svg
index 951b6d2d..c530ab22 100644
--- a/images/firefox.svg
+++ b/images/firefox.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/images/nodejs.svg b/images/nodejs.svg
index 974a2b3e..db19b09b 100644
--- a/images/nodejs.svg
+++ b/images/nodejs.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/images/safari.svg b/images/safari.svg
new file mode 100644
index 00000000..9c40a6f6
--- /dev/null
+++ b/images/safari.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/images/safari_144x144.png b/images/safari_144x144.png
deleted file mode 100644
index a91a1e1b..00000000
Binary files a/images/safari_144x144.png and /dev/null differ
diff --git a/images/safari_48x48.png b/images/safari_48x48.png
deleted file mode 100644
index bcac3d70..00000000
Binary files a/images/safari_48x48.png and /dev/null differ
diff --git a/images/safari_96x96.png b/images/safari_96x96.png
deleted file mode 100644
index 3034690f..00000000
Binary files a/images/safari_96x96.png and /dev/null differ
diff --git a/images/wasmer.png b/images/wasmer.png
deleted file mode 100644
index 3b136d1b..00000000
Binary files a/images/wasmer.png and /dev/null differ
diff --git a/images/wasmer.svg b/images/wasmer.svg
new file mode 100644
index 00000000..c38c773f
--- /dev/null
+++ b/images/wasmer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/roadmap.js b/roadmap.js
new file mode 100644
index 00000000..951a8651
--- /dev/null
+++ b/roadmap.js
@@ -0,0 +1,389 @@
+(async () => {
+ 'use strict';
+
+ function partitionArray(arr, condition) {
+ const matched = [];
+ const unmatched = [];
+
+ for (const item of arr) {
+ if (condition(item)) {
+ matched.push(item);
+ } else {
+ unmatched.push(item);
+ }
+ }
+
+ return { matched, unmatched };
+ }
+
+ function h(name, props = {}, children = []) {
+ const node = Object.assign(document.createElement(name), props);
+ node.append(...children);
+ return node;
+ }
+
+ // Convert number to lowercase hexavigesimal like "a, b, c, .., x, y, z, aa, ab, ..", starting from zero.
+ // This is the same format as CSS `list-style: lower-alpha`, which is used for our footnote lists.
+ function toAlphabet(num) {
+ const digit = num % 26,
+ char = String.fromCharCode(97 + digit),
+ rem = num - digit;
+ return rem ? toAlphabet(Math.floor(rem / 26) - 1) + char : char;
+ }
+
+ // Map names to HTML ids. For example, idMap['table-col']('Chrome') will return 'table-col-chrome'.
+ // This is to satisfy the need for unique ids in `headers` attributes.
+ // Hardcoded array makes it easier to find typos, since it would throw an error if the namespace is mistyped.
+ const idMap = ['table-group', 'table-col', 'table-row'].reduce((map, namespace) => {
+ map[namespace] = (str) => namespace + '-' + str.toLowerCase().replace(/[^\w\d-_]+/g, '-');
+ return map;
+ }, {});
+
+ // Get a copy of the requested SVG icon. Those are defined in the markdown as templates.
+ function icon(key) {
+ return document.getElementById(`support-symbol-${key}`).content.firstElementChild.cloneNode(true);
+ }
+
+ const scrollbox = document.getElementById('feature-support-scrollbox');
+ const table = document.getElementById('feature-support');
+
+ const detectWasmFeature = _loadFeatureDetectModule();
+ const addTooltip = _loadTooltipModule();
+
+ const { features, browsers } = await fetch('/features.json', {
+ credentials: 'include', // https://stackoverflow.com/a/63814972
+ mode: 'no-cors'
+ }).then(res => res.json());
+
+ const tBody = document.createElement('tbody');
+ table.append(
+ h('thead', {}, [
+ h('tr', {}, [
+ h('th', { id: 'table-blank' }),
+ h('th', { scope: 'col', id: idMap['table-col']('Your browser') }, ['Your browser']),
+ ...Object.entries(browsers).map(([name, { url, logo }]) =>
+ h('th', { scope: 'col', id: idMap['table-col'](name) }, [
+ h('a', { href: url, target: '_blank' }, [
+ // https://www.w3.org/WAI/WCAG22/Techniques/html/H2
+ h('img', { src: logo, width: 32, height: 32, alt: '' }),
+ h('br'),
+ name
+ ])
+ ])
+ )
+ ])
+ ]),
+ tBody
+ );
+
+ let featureGroups = partitionArray(
+ Object.entries(features).map(([name, feature]) => Object.assign(feature, { name })),
+ feature => feature.phase >= 4
+ );
+
+ featureGroups = [
+ { name: 'Standardized features', features: featureGroups.matched },
+ { name: 'In-progress proposals', features: featureGroups.unmatched },
+ ];
+
+ // Collect all notes and assign an index to each unique item
+ // { "First unique note": 0, "Second unique note": 1, ...}
+ const notes = Object.values(browsers).flatMap(b =>
+ Object.values(b.features)
+ .filter(s => Array.isArray(s))
+ .map(s => s[1])
+ );
+ const note2index = new Map();
+ let noteIndex = 0;
+ for (const note of notes) {
+ if (!note2index.has(note)) {
+ note2index.set(note, noteIndex++);
+ }
+ }
+
+ // Generate the footnote list. They are later referenced in the actual table.
+ const noteList = document.createElement('ol');
+ // Place footnote list outside of the scolling area
+ scrollbox.parentNode.insertBefore(noteList, scrollbox.nextSibling);
+ for (const [note, index] of note2index) {
+ const item = h('li', { id: `feature-note-${index}` });
+ noteList.appendChild(item).appendChild(renderNote(note));
+ }
+
+ // Create an element that links to the specified footnote.
+ // Also returns the HTML id of the footnote it refers to.
+ function createNoteRef(index) {
+ const id = `feature-note-${index}`;
+ return [id, h('a', { href: `#${id}` }, [`[${toAlphabet(index)}]`])];
+ }
+
+ const columnCount = 2 + Object.keys(browsers).length;
+
+ for (const { name: groupName, features } of featureGroups) {
+ tBody.append(
+ h('tr', {}, [
+ h('th', {
+ scope: 'colgroup',
+ colSpan: columnCount,
+ id: idMap['table-group'](groupName),
+ headers: 'table-blank',
+ // Chrome doesn't handle `headers` attribute correctly.
+ // Just hide the group headers for now...
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=1081201
+ //
+ // Actually Firefox doesn't support `ariaHidden` attribute.
+ // This is a happy coincidence, since `headers` works fine on Firefox anyway.
+ ariaHidden: true
+ }, [groupName])
+ ])
+ );
+ for (const { name: featName, description, url } of features) {
+ const detectResult = h('td', {
+ headers: [idMap['table-col']('Your browser'), idMap['table-row'](featName)].join(' ')
+ }, [buildCellInner('loading')]);
+
+ detectWasmFeature(featName).then(supported => {
+ detectResult.textContent = '';
+ detectResult.appendChild(buildCellInner(supported ? 'yes' : 'no'));
+ addTooltip(detectResult, supported ? '✓ Supported' : '✗ Not supported', [tBody, scrollbox]);
+ }, _err => {
+ detectResult.textContent = '';
+ detectResult.appendChild(buildCellInner('unknown'));
+ addTooltip(detectResult, 'Detection unavailable for this feature', [tBody, scrollbox]);
+ });
+
+ tBody.append(
+ h('tr', {}, [
+ h('th', {
+ scope: 'row',
+ id: idMap['table-row'](featName),
+ headers: idMap['table-group'](groupName)
+ }, [h('a', { href: url, target: '_blank' }, [description])]),
+ detectResult,
+ ...Object.entries(browsers).map(([browserName, { features }]) => {
+ // Meaning of each entry:
+ // * null => not applicable for this browser
+ // * true/false => supported/unsupported
+ // * "version" => supported since "version"
+ // * "flag" => flag required (must be lowercase)
+ // * [true, "footnotes"] => supported, with "footnotes"
+ // * ["version", "footnotes"] => supported since "version", with "footnotes"
+ // ...and any combination thereof
+ let support = features[featName];
+ let box, note;
+
+ // First extract the footnote part if it's an array
+ if (Array.isArray(support)) {
+ note = support[1];
+ support = support[0];
+ }
+
+ if (typeof support === 'string') {
+ if (support === 'flag') {
+ // 'flag' is treated specially as the "requires flag" icon
+ box = buildCellInner('flag');
+ } else {
+ // Otherwise it's a version number, like "95"
+ box = buildCellInner('yes', support);
+ note ||= `✓ Supported since version ${support}`;
+ }
+ } else if (!support) {
+ if (support === null) {
+ box = buildCellInner('na', 'N/A');
+ note ||= '✗ Not applicable for this browser/engine';
+ } else {
+ box = buildCellInner('no');
+ note ||= '✗ Not supported';
+ }
+ } else {
+ if (support !== true) throw new TypeError();
+ box = buildCellInner('yes');
+ // Magic value, keep in sync with `renderNote`
+ note ||= '✓ Supported, introduced in unknown version';
+ }
+
+ const cell = h('td', {
+ headers: [idMap['table-col'](browserName), idMap['table-row'](featName)].join(' ')
+ }, [box]);
+
+ // Give the cell itself an `aria-lebel` to avoid screen readers calling it "empty cell".
+ const icon = box.firstElementChild;
+ if (icon?.hasAttribute('aria-label')) {
+ cell.setAttribute('aria-label', icon.getAttribute('aria-label'));
+ icon.removeAttribute('aria-label');
+ }
+
+ if (note && note2index.has(note)) {
+ cell.tabIndex = 0; // focusable
+ const index = note2index.get(note);
+ const [noteId, refLink] = createNoteRef(index);
+ box.appendChild(h('sup', {}, [refLink]));
+
+ const noteItem = document.getElementById(noteId);
+ if (noteItem) {
+ cell.addEventListener('mouseenter', () => noteItem.classList.add('ref-highlight'));
+ cell.addEventListener('mouseleave', () => noteItem.classList.remove('ref-highlight'));
+ }
+ }
+
+ // Clip to both and the scrollbox.
+ // the former is to avoid blocking out the headers;
+ // the latter is to keep the tooltip inside the scrollable area
+ addTooltip(cell, note, [tBody, scrollbox]);
+ return cell;
+ })
+ ])
+ );
+ tBody.lastElementChild.setAttribute('aria-describedby', idMap['table-row'](featName));
+ }
+ }
+
+ function buildCellInner(type, text) {
+ const content = text || icon(type);
+ return h('div', { className: `feature-cell icon-${type}`}, [content]);
+ }
+
+ function renderNote(note) {
+ const fragment = document.createDocumentFragment();
+ const isMissingData = note.includes('introduced in unknown version');
+
+ // Transform markdown-like backticks into html
+ while (note) {
+ const [head, body, tail] = splitParts(note, '`');
+ head && fragment.append(head);
+ body && fragment.appendChild(h('code', {}, [body]));
+ note = tail;
+ }
+
+ const firstNode = fragment.firstChild;
+ if (firstNode.nodeType === Node.TEXT_NODE) {
+ // No point for screen readers to pronounce those symbols out loud.
+ for (const symbol of ['✓', '✗']) {
+ if (firstNode.nodeValue?.startsWith(symbol)) {
+ // Before: <#text>✓ Supported#text>
+ // After: ✓<#text> Supported#text>
+ firstNode.splitText(1);
+ const symbolNode = h('span', {}, firstNode.nodeValue);
+ symbolNode.setAttribute('aria-hidden', '');
+ fragment.replaceChild(symbolNode, firstNode);
+ break;
+ }
+ }
+ }
+
+ if (isMissingData) {
+ fragment.appendChild(h('a', {
+ href: 'https://github.com/WebAssembly/website/blob/master/features.json',
+ target: '_blank'
+ }, [' (contribute data)']))
+ }
+
+ return fragment;
+ }
+
+ // Break a string into three parts using the given delimiter.
+ function splitParts(str, delim) {
+ const start = str.indexOf(delim);
+ const end = str.indexOf(delim, start + 1);
+ if (start >= 0 && end > start) {
+ const head = str.substring(0, start);
+ const body = str.substring(start + 1, end);
+ const tail = str.substring(end + 1);
+ return [head, body, tail];
+ }
+ return [str, '', '']
+ }
+
+ // Lazy-loading
+ function _loadTooltipModule() {
+ // Be sure to change the preloads in markdown when updating url.
+ // The ESM bundle of this package doesn't work with unpkg.com.
+ const module = import('https://cdn.jsdelivr.net/npm/@floating-ui/dom@1/+esm');
+
+ const subscribers = new Set();
+ const updateAll = () => { for (const fn of subscribers) fn(); };
+
+ document.addEventListener('scroll', updateAll, { passive: true });
+ scrollbox.addEventListener('scroll', updateAll, { passive: true });
+ window.addEventListener('resize', updateAll, { passive: true });
+
+ let counter = 0;
+ return (reference, note, boundary) =>
+ module.then(({ computePosition, offset, flip, shift, arrow }) => {
+ const tooltipId = `tooltip-${counter++}`;
+ const tooltip = h('div', { id: tooltipId, className: 'feature-tooltip', role: 'tooltip' });
+ tooltip.appendChild(renderNote(note));
+
+ const arrowElement = h('div', { className: 'feature-tooltip-arrow' });
+ tooltip.appendChild(arrowElement);
+
+ const update = () => computePosition(reference, tooltip, {
+ placement: 'top',
+ middleware: [
+ offset(6),
+ flip({ boundary }),
+ shift({ padding: 6, boundary }),
+ arrow({ element: arrowElement, padding: 3, boundary })
+ ],
+ }).then(({ x, y, placement, middlewareData }) => {
+ const { x: arrowX, y: arrowY } = middlewareData.arrow;
+ Object.assign(arrowElement.style, {
+ left: arrowX !== null ? `${arrowX}px` : '',
+ top: arrowY !== null ? `${arrowY}px` : '',
+ });
+
+ tooltip.style.transform = `translate(${x}px, ${y}px)`;
+ // Force the browser to apply CSS changes first
+ if (tooltip.dataset.placement !== placement) tooltip.offsetHeight;
+ // This will then enable the transition effect
+ tooltip.dataset.placement = placement;
+ });
+
+ const setVisible = (visible) => {
+ if (visible) {
+ tooltip.style.removeProperty('display');
+ update();
+ subscribers.add(update);
+ } else {
+ subscribers.delete(update);
+ tooltip.style.display = 'none';
+ delete tooltip.dataset.placement; // disable the transition effect
+ }
+ };
+
+ setVisible(false);
+
+ const monitor = (name, state, listener = () => setVisible(state)) =>
+ reference.addEventListener(name, listener);
+ monitor('focusin', true);
+ monitor('focusout', false);
+
+ // Add a bit of delay to mouse events
+ let timeout = null;
+ monitor('mouseenter', true, () => {
+ clearTimeout(timeout);
+ if (subscribers.size) {
+ timeout = setTimeout(() => setVisible(true), 80);
+ } else {
+ // Immediately show if there aren't other tooltips visible
+ setVisible(true);
+ }
+ });
+ monitor('mouseleave', false, () => {
+ clearTimeout(timeout);
+ timeout = setTimeout(() => setVisible(false), 80);
+ });
+
+ reference.appendChild(tooltip);
+ reference.setAttribute('aria-describedby', tooltipId);
+ return tooltip;
+ });
+ }
+
+ function _loadFeatureDetectModule() {
+ // Be sure to change the preloads in markdown when updating url.
+ const module = import('https://cdn.jsdelivr.net/npm/wasm-feature-detect@1/dist/esm/index.js');
+ return (featureName) => module
+ .then(wasmFeatureDetect => wasmFeatureDetect[featureName]());
+ }
+})();
diff --git a/roadmap.md b/roadmap.md
index 0780fabf..ee201218 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -7,106 +7,32 @@ In November 2017, WebAssembly CG members representing four browsers, Chrome, Edg
After the initial release, WebAssembly has been gaining new features through the [standardization process](https://github.com/WebAssembly/meetings/blob/master/process/phases.md). For the complete list of current proposals and their respective stages, check out the [`WebAssembly/proposals` repo](https://github.com/WebAssembly/proposals).
-
- The table below aims to track implemented features in popular engines:
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+The table below aims to track implemented features in popular engines:
+
+
+
+
+
+
To detect supported features at runtime from JavaScript, check out the [`wasm-feature-detect` library](https://github.com/GoogleChromeLabs/wasm-feature-detect), which powers the "Your browser" column above.