sygnal 5.3.7 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +646 -0
- package/README.md +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +20 -4
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +109 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +13 -6
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +94 -210
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +3158 -190
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +262 -134
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -119
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
|
|
2
|
+
# Accessibility
|
|
3
|
+
|
|
4
|
+
`sygnal-check` has an accessibility lane, SYG701 to SYG708 and SYG722. It finds the markup mistakes that lock keyboard and screen-reader users out without any visible sign: a clickable `<div>`, a field without a label, an icon button with no name. Like the other checks, it reads your source, so it works the same in the editor, in CI and in the dev server.
|
|
5
|
+
|
|
6
|
+
## Severity
|
|
7
|
+
|
|
8
|
+
- **Warning** by default: `npx --no-install sygnal-check` prints the findings and exits with code 1, like any other warning.
|
|
9
|
+
- **Still a warning under `--strict`.** Strict mode checks the canonical forms; it doesn't turn markup that already shipped into errors when you upgrade.
|
|
10
|
+
- **Error** when you ask for it: `sygnal-check --a11y=error`, `check(paths, { a11y: 'error' })`, or `check: { a11y: 'error' }` in the [Vite plugin](https://sygnal.js.org/integration/bundler-config/#plugin-options), where errors also open Vite's error overlay. Use it in CI once an app is clean.
|
|
11
|
+
- **On in the dev server**: the Vite plugin runs `sygnal-check` when it is installed, so the findings appear in the terminal and the browser console as you edit.
|
|
12
|
+
|
|
13
|
+
The checks are static only: markup that is built at runtime (spread props, a dynamic `type` or `role`) is skipped rather than guessed at.
|
|
14
|
+
|
|
15
|
+
To keep one finding on purpose, put `// sygnal-ignore SYG70x` on the line above it, or at the end of the line (see [Suppressing a finding](#suppressing-a-finding)).
|
|
16
|
+
|
|
17
|
+
## The rules
|
|
18
|
+
|
|
19
|
+
Each example shows the flagged form as a comment, then the fix.
|
|
20
|
+
|
|
21
|
+
### SYG701: click listener on a non-interactive element
|
|
22
|
+
|
|
23
|
+
The intent listens for clicks on a `<div>`, `<span>`, `<li>`, `<p>`, `<img>` or similar without a `role` and `tabIndex`. Mouse users can click it; keyboard users can't reach it, and screen readers don't announce it as something to activate. Render a button instead:
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
// Flagged:
|
|
27
|
+
// function Card({ state }) { return <div className="card">{state.title}</div> }
|
|
28
|
+
|
|
29
|
+
function Card({ state }) {
|
|
30
|
+
return <button type="button" className="card">{state.title}</button>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Card.intent = ({ DOM }) => ({ OPEN: DOM.click('.card') })
|
|
34
|
+
Card.model = { OPEN: (state) => ({ ...state, open: true }) }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If the element must stay a `<div>`, give it `role="button"` and `tabIndex={0}`, and handle Enter and Space too:
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
import { xs } from 'sygnal'
|
|
41
|
+
|
|
42
|
+
function Card({ state }) {
|
|
43
|
+
return <div className="card" role="button" tabIndex={0}>{state.title}</div>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Card.intent = ({ DOM }) => ({
|
|
47
|
+
OPEN: xs.merge(
|
|
48
|
+
DOM.click('.card'),
|
|
49
|
+
DOM.keydown('.card').filter(e => e.key === 'Enter' || e.key === ' '),
|
|
50
|
+
),
|
|
51
|
+
})
|
|
52
|
+
Card.model = { OPEN: (state) => ({ ...state, open: true }) }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The rule applies to a [control](https://sygnal.js.org/guide/controls/)'s element too (`controls({ Card: 'div' })` listened to for clicks: declare it as `'button'`). An element that contains a button, link or form field isn't flagged, since a click on those bubbles up to the listener.
|
|
56
|
+
|
|
57
|
+
### SYG702: form field without a label
|
|
58
|
+
|
|
59
|
+
An `<input>`, `<select>` or `<textarea>` has no accessible name, so a screen reader announces only "edit text". Wrap it in a `<label>`, link a `<label for>` to its `id` (with [`uid()`](#ids-with-uid), below), or, for a field with no visible label, add `aria-label`:
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
// Flagged: <input className="search" type="search" value={state.query} />
|
|
63
|
+
|
|
64
|
+
function Search({ state }) {
|
|
65
|
+
return <input className="search" type="search" aria-label="Search tasks" value={state.query} />
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Search.initialState = { query: '' }
|
|
69
|
+
Search.intent = ({ DOM }) => ({ QUERY: DOM.input('.search').value() })
|
|
70
|
+
Search.model = { QUERY: (state, query) => ({ ...state, query }) }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A `placeholder` counts as a last-resort name, but it disappears as soon as the user types; prefer a visible label.
|
|
74
|
+
|
|
75
|
+
### SYG703: image without alt text
|
|
76
|
+
|
|
77
|
+
Every `<img>` needs `alt`. Describe what the image shows, or use `alt=""` for a decorative image, which screen readers then skip:
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
// Flagged: <img src={state.avatarUrl} />
|
|
81
|
+
|
|
82
|
+
function Profile({ state }) {
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
<img src={state.avatarUrl} alt={state.name} />
|
|
86
|
+
<img src="/divider.svg" alt="" />
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### SYG704: link without href used as a button
|
|
93
|
+
|
|
94
|
+
An `<a>` without `href` is not focusable and is announced as plain text. Use a button for an action, and keep `<a href>` for navigation:
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
// Flagged: <a className="more">Show more</a> with DOM.click('.more')
|
|
98
|
+
|
|
99
|
+
function Comments({ state }) {
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
<p>{state.shown} of {state.total} comments</p>
|
|
103
|
+
<button type="button" className="more">Show more</button>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Comments.intent = ({ DOM }) => ({ MORE: DOM.click('.more') })
|
|
109
|
+
Comments.model = { MORE: (state) => ({ ...state, shown: Math.min(state.total, state.shown + 10) }) }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### SYG705: button without an accessible name
|
|
113
|
+
|
|
114
|
+
A button whose only content is an icon is announced as just "button". Add `aria-label`, and hide the icon from screen readers:
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
// Flagged: <button type="button" className="close"><span>×</span></button>, no name for "×"...
|
|
118
|
+
// ...and an icon with no text: <button type="button" className="close"><i className="icon-x" /></button>
|
|
119
|
+
|
|
120
|
+
function Banner({ state }) {
|
|
121
|
+
return (
|
|
122
|
+
<div role="status">
|
|
123
|
+
{state.message}
|
|
124
|
+
<button type="button" className="close" aria-label="Dismiss">
|
|
125
|
+
<i className="icon-x" aria-hidden="true" />
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Banner.intent = ({ DOM }) => ({ DISMISS: DOM.click('.close') })
|
|
132
|
+
Banner.model = { DISMISS: (state) => ({ ...state, message: '' }) }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Only literal content is checked: a button whose children include an expression (`{state.label}`) or a component isn't flagged.
|
|
136
|
+
|
|
137
|
+
### SYG706: positive tabIndex
|
|
138
|
+
|
|
139
|
+
`tabIndex={1}` and higher move the element ahead of everything else in the Tab order. Use `tabIndex={0}` (focusable in document order) or `tabIndex={-1}` (focusable only from code), and change the markup order to change the Tab order:
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
// Flagged: <div className="preview" tabIndex={1}>…</div>
|
|
143
|
+
|
|
144
|
+
function Preview({ state }) {
|
|
145
|
+
return <div className="preview" tabIndex={0} aria-label="Preview">{state.html}</div>
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### SYG707: unknown ARIA attribute or invalid role
|
|
150
|
+
|
|
151
|
+
A misspelled `aria-*` attribute, or a `role` that isn't a WAI-ARIA role (including abstract roles such as `widget`), is ignored by assistive technology. The message suggests the closest valid name:
|
|
152
|
+
|
|
153
|
+
```jsx
|
|
154
|
+
// Flagged: aria-labeledby (a typo), role="widget" (abstract)
|
|
155
|
+
|
|
156
|
+
function Tabs({ state }) {
|
|
157
|
+
return (
|
|
158
|
+
<div>
|
|
159
|
+
<h2 id="tabs-title">Settings</h2>
|
|
160
|
+
<div role="tablist" aria-labelledby="tabs-title">
|
|
161
|
+
{state.tabs.map(tab => <button type="button" role="tab" aria-selected={String(tab === state.current)}>{tab}</button>)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### SYG708: label or ARIA reference to an id that isn't rendered
|
|
169
|
+
|
|
170
|
+
A `<label for>`, `aria-describedby` or `aria-labelledby` names an id that no element has, so the label is attached to nothing. This usually comes from an id renamed on one side only. Inside a component, use `uid()` on both sides:
|
|
171
|
+
|
|
172
|
+
```jsx
|
|
173
|
+
// Flagged: <input id="email" … /> with <label for="e-mail">
|
|
174
|
+
|
|
175
|
+
function EmailField({ state, uid }) {
|
|
176
|
+
return (
|
|
177
|
+
<p>
|
|
178
|
+
<label for={uid('email')}>Email</label>
|
|
179
|
+
<input id={uid('email')} type="email" className="email" value={state.email} aria-describedby={uid('email-error')} />
|
|
180
|
+
{state.error && <span id={uid('email-error')}>{state.error}</span>}
|
|
181
|
+
</p>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
EmailField.initialState = { email: '', error: '' }
|
|
186
|
+
EmailField.intent = ({ DOM }) => ({ EMAIL: DOM.input('.email').value() })
|
|
187
|
+
EmailField.model = { EMAIL: (state, email) => ({ ...state, email, error: email.includes('@') ? '' : 'Enter an email address' }) }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A `uid('x')` reference needs an element in the same component with `id={uid('x')}`; a literal id may be anywhere in the checked files.
|
|
191
|
+
|
|
192
|
+
### SYG722: Menu, Select or Combobox without an accessible name
|
|
193
|
+
|
|
194
|
+
A [Menu](./ui/menu.md), [Select](./ui/select.md) or [Combobox](./ui/combobox.md) without `label`, `aria-label` or `aria-labelledby`. The part names its control from them (Menu's trigger text is its `label`), so without one the menu button or combobox is announced without a name. Give it a visible `label`; when the label is elsewhere, point at it with `aria-labelledby`; for an icon-only menu trigger, use `aria-label`:
|
|
195
|
+
|
|
196
|
+
```jsx
|
|
197
|
+
import { Select } from 'sygnal/ui/select'
|
|
198
|
+
import { Menu } from 'sygnal/ui/menu'
|
|
199
|
+
|
|
200
|
+
// Flagged: <Select className="size" items={SIZES} />
|
|
201
|
+
|
|
202
|
+
function Sizes({ state }) {
|
|
203
|
+
return (
|
|
204
|
+
<div>
|
|
205
|
+
<Select className="size" label="Size" items={['S', 'M', 'L']} value={state.size} />
|
|
206
|
+
<Menu className="more" label="⋯" aria-label="More actions" items={['Duplicate', 'Delete']} />
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
Sizes.initialState = { size: null }
|
|
212
|
+
Sizes.intent = ({ DOM }) => ({ SIZE: DOM.select('.size').events('value-change').detail() })
|
|
213
|
+
Sizes.model = { SIZE: (state, size) => ({ ...state, size }) }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## ARIA values in JSX
|
|
217
|
+
|
|
218
|
+
An `aria-*` attribute set to `true` renders `"true"`. Set to `false`, it renders `"false"` on the ARIA states that have a false value (`aria-expanded`, `aria-hidden`, `aria-pressed`, `aria-checked`, `aria-selected`, `aria-disabled`, `aria-invalid`, `aria-current`, `aria-haspopup`, `aria-busy`, `aria-modal`, `aria-required`, `aria-readonly`, `aria-multiline`, `aria-multiselectable`, `aria-atomic`, `aria-grabbed`); on any other `aria-*` (a label, an id reference, a number) `false` removes the attribute, so `aria-describedby={hasError && uid('error')}` doesn't announce "false". `null` and `undefined` remove it. Passed to a component, `role`, `for`, `tabindex` and `aria-*` are ordinary props: the component gets the value as written.
|
|
219
|
+
|
|
220
|
+
## Ids with uid()
|
|
221
|
+
|
|
222
|
+
Labels and ARIA references need ids, and ids must be unique on the page, which a literal id isn't once the component is rendered twice. Every view gets a `uid` prop: `uid()` is an id for this component instance, and `uid('email')` an id derived from it. They are stable across renders and the same on the server and the client, so they are safe with [SSR](./ssr.md#stable-ids-uid). See [Forms](./inputs.md#labels-and-ids-uid) for a full example. The checks understand `uid()`: SYG702 and SYG708 match `for={uid('email')}` with `id={uid('email')}`.
|
|
223
|
+
|
|
224
|
+
## Suppressing a finding
|
|
225
|
+
|
|
226
|
+
A comment on the line above (or at the end of the line) suppresses one finding:
|
|
227
|
+
|
|
228
|
+
```jsx
|
|
229
|
+
import { xs } from 'sygnal'
|
|
230
|
+
|
|
231
|
+
function Dialog({ state }) {
|
|
232
|
+
return (
|
|
233
|
+
<div>
|
|
234
|
+
<div className="backdrop" />
|
|
235
|
+
<div role="dialog" aria-modal="true" aria-label="Settings">{state.body}</div>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Dialog.intent = ({ DOM }) => ({
|
|
241
|
+
CLOSE: xs.merge(
|
|
242
|
+
// a mouse shortcut: keyboard users close the dialog with Escape
|
|
243
|
+
// sygnal-ignore SYG701
|
|
244
|
+
DOM.click('.backdrop'),
|
|
245
|
+
DOM.keydown('document').key().filter(key => key === 'Escape'),
|
|
246
|
+
),
|
|
247
|
+
})
|
|
248
|
+
Dialog.model = { CLOSE: { PARENT: () => 'closed' } }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A backdrop that closes a dialog on click is a common case: the dialog itself is reachable, and keyboard users close it with Escape. Without codes, `// sygnal-ignore` suppresses every code on that line. To turn a rule off for the dev server, add it to the Vite plugin's ignore list: `sygnal({ check: { ignore: ['SYG705'] } })`.
|
|
252
|
+
|
|
253
|
+
## Related
|
|
254
|
+
|
|
255
|
+
- [Error Reference](https://sygnal.js.org/reference/errors/#syg701): every SYG7xx code, its cause and its fix
|
|
256
|
+
- [Diagnostics](https://sygnal.js.org/guide/diagnostics/#the-static-checker): running `sygnal-check`
|
|
257
|
+
- [Forms](./forms.md): validation and server errors; [Inputs, Labels and Focus](./inputs.md): labels, controlled inputs and focus
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
|
|
2
|
+
# Adapters
|
|
3
|
+
|
|
4
|
+
An adapter turns code written for another model into a Sygnal [widget](./widgets.md) tag: you render it with a class, read its events in the intent with `.detail()`, and send it [element commands](./element-commands.md), as with any widget.
|
|
5
|
+
|
|
6
|
+
- **`fromZag`** (`sygnal/zag`) runs a [Zag.js](https://zagjs.com) state machine and renders its parts with Sygnal JSX. It is what [Menu](./ui/menu.md), [Select](./ui/select.md) and [Combobox](./ui/combobox.md) are built on.
|
|
7
|
+
- **`fromReact`** (`sygnal/react`) renders a React component, or a Preact one through `preact/compat`.
|
|
8
|
+
|
|
9
|
+
**Adapters are an escape hatch.** Reach for one for the component you can't replace, not as a way to build the app. First look for a native answer (`<dialog>`, the Popover API, [`sygnal/ui`](./ui/overview.md)), a [web component](./web-components.md), or a framework-agnostic library through [`defineWidget`](./widgets.md). A React component in a Sygnal app:
|
|
10
|
+
|
|
11
|
+
- **brings its runtime**: React and react-dom add about 68 KB gzipped (Preact through `preact/compat` about 8 KB);
|
|
12
|
+
- **doesn't see your context or providers**: React context, a theme provider, a router or a query client from a React tree don't cross the boundary, and Sygnal's `.context` doesn't reach the component. Pass what it needs as props;
|
|
13
|
+
- **can render outside its host**: a React portal (a modal, a tooltip) goes where React puts it, so Sygnal's `DOM.select` doesn't see events from it; use the component's callbacks instead;
|
|
14
|
+
- **doesn't run in the mock DOM**: test it with `renderComponent(App, { dom: 'real' })` (jsdom or a browser).
|
|
15
|
+
|
|
16
|
+
The libraries are optional peer dependencies of `sygnal`: an app that doesn't import an adapter never installs or ships them. When one is missing, `sygnal/vite` names it ([SYG666](https://sygnal.js.org/reference/errors/#syg666)).
|
|
17
|
+
|
|
18
|
+
## React components: fromReact
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install react react-dom
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The React component here is a small one of our own, written with React's `createElement` (the JSX on these pages is Sygnal's); a component from npm is wrapped the same way:
|
|
25
|
+
|
|
26
|
+
```js live-file=./Rating.js
|
|
27
|
+
// Rating.js: a React component
|
|
28
|
+
import { createElement as h } from 'react'
|
|
29
|
+
|
|
30
|
+
export function Rating({ value, max, onChange }) {
|
|
31
|
+
return h('span', { role: 'group', 'aria-label': 'Rating' },
|
|
32
|
+
Array.from({ length: max }, (_, i) => h('button', {
|
|
33
|
+
key: i, type: 'button', 'aria-label': `${i + 1} of ${max}`, 'aria-pressed': i < value, onClick: () => onChange(i + 1),
|
|
34
|
+
}, i < value ? '★' : '☆')))
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```jsx live-file=./Stars.js
|
|
39
|
+
// Stars.js
|
|
40
|
+
import { fromReact } from 'sygnal/react'
|
|
41
|
+
import { Rating } from './Rating.js'
|
|
42
|
+
|
|
43
|
+
export const Stars = fromReact(Rating, { events: { rate: 'onChange' } })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```jsx live
|
|
47
|
+
// Review.jsx
|
|
48
|
+
import { Stars } from './Stars.js'
|
|
49
|
+
|
|
50
|
+
export function Review({ state }) {
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
<Stars className="rating" value={state.rating} max={5} />
|
|
54
|
+
<p>{state.rating} of 5</p>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Review.initialState = { rating: 0 }
|
|
60
|
+
Review.intent = ({ DOM }) => ({
|
|
61
|
+
RATE: DOM.select('.rating').events('rate').detail(),
|
|
62
|
+
})
|
|
63
|
+
Review.model = {
|
|
64
|
+
RATE: (state, rating) => ({ ...state, rating }),
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The tag renders a host `<div class="rating">` and a React root inside it. Every render passes the newest props to the component (`value`, `max`); the props are routed between the host and the component:
|
|
69
|
+
|
|
70
|
+
| Props | Go to |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `className`, `class`, `id`, `style`, `attrs`, `tabIndex`, `hidden` | the host only (one tab stop) |
|
|
73
|
+
| `aria-*`, `role`, `title` | the component only: a React control names itself, so `<IconButton aria-label="Delete" />` labels the component's `<button>`, not the generic host `<div>` |
|
|
74
|
+
| `data-*` | both |
|
|
75
|
+
| everything else | the component (`name`, `placeholder`, `lang` and `dir` also go on the host, as for any widget) |
|
|
76
|
+
|
|
77
|
+
List a host prop in `ownProps` to send it to the component instead, or a component-only one in `hostProps` to put it on the host too. A React component that doesn't pass `aria-*` and `role` on to its element (it takes only the props it names) gets no accessible name this way: fix the component to spread them, or list them in `hostProps` (`hostProps: ['role', 'aria-label']`) so the host carries the role and the name. When the component calls `onChange(4)`, the adapter dispatches a `rate` event on the host with `4` as its detail.
|
|
78
|
+
|
|
79
|
+
When the host leaves the page (also inside a shadow root, as with `sygnal/element`'s `shadow: true`), the React root unmounts. A host can also be destroyed while its element stays in the page, for example during a `<Transition>` leave that never gets its `transitionend`. The React root then stays mounted until the element is removed, and for at most 10 seconds, so no root or observer is leaked. A component's cleanup (`useEffect` returns) can therefore run up to 10 seconds after the host was destroyed.
|
|
80
|
+
|
|
81
|
+
| Option | |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `events` | `{ eventName: 'onCallback' }`: each callback prop becomes a DOM event with that name. An array (`['onChange']`) uses the callback's own name as the event name. The detail is the callback's argument, or an array of them when it gets several. A callback you pass as a prop still runs, first |
|
|
84
|
+
| `props` | `(props) => componentProps`, when the component's props differ from the tag's |
|
|
85
|
+
| `commands` | Element commands, called with `{ root, el, props }` |
|
|
86
|
+
| `tag`, `name`, `fallback`, `hostProps`, `ownProps` | As in [`defineWidget`](./widgets.md#the-definition) |
|
|
87
|
+
|
|
88
|
+
Prefer event names of your own (`'rate'`) to the browser's (`'change'`): the component's own `<input>`s fire native `change` events that bubble out of the host too.
|
|
89
|
+
|
|
90
|
+
### Preact
|
|
91
|
+
|
|
92
|
+
The adapter uses only `createElement` (from `react`), `createRoot` (from `react-dom/client`) and `flushSync` (from `react-dom`), which `preact/compat` provides. Alias the React packages in the bundler, and the same adapter and the same component code run on Preact:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
// vite.config.js
|
|
96
|
+
import { defineConfig } from 'vite'
|
|
97
|
+
import sygnal from 'sygnal/vite'
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
plugins: [sygnal()],
|
|
101
|
+
resolve: {
|
|
102
|
+
alias: {
|
|
103
|
+
react: 'preact/compat',
|
|
104
|
+
'react-dom/client': 'preact/compat/client',
|
|
105
|
+
'react-dom': 'preact/compat',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install preact
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Zag machines: fromZag
|
|
116
|
+
|
|
117
|
+
[Zag.js](https://zagjs.com) implements UI patterns (menus, comboboxes, date pickers, sliders) as framework-agnostic state machines, with the keyboard and ARIA work done. `fromZag(zag, render, options)` runs one on a widget host:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install @zag-js/vanilla@~1.45.0 @zag-js/menu@~1.45.0
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```jsx live-file=./Actions.jsx
|
|
124
|
+
// Actions.jsx
|
|
125
|
+
import { fromZag } from 'sygnal/zag'
|
|
126
|
+
import * as menu from '@zag-js/menu'
|
|
127
|
+
|
|
128
|
+
export const Actions = fromZag(menu, (api, props) => (
|
|
129
|
+
<div>
|
|
130
|
+
<button {...api.getTriggerProps()}>{props.label}</button>
|
|
131
|
+
<div {...api.getPositionerProps()}>
|
|
132
|
+
<ul {...api.getContentProps()}>
|
|
133
|
+
{props.items.map((item) => <li {...api.getItemProps({ value: item })}>{item}</li>)}
|
|
134
|
+
</ul>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
), {
|
|
138
|
+
name: 'Actions',
|
|
139
|
+
events: { pick: ['onSelect', (details) => details.value] },
|
|
140
|
+
commands: { open: (api) => api.setOpen(true) },
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```jsx live live-height=170
|
|
145
|
+
// Toolbar.jsx
|
|
146
|
+
import { Actions } from './Actions.jsx'
|
|
147
|
+
|
|
148
|
+
export function Toolbar({ state }) {
|
|
149
|
+
return <div><Actions className="actions" label="Edit" items={['cut', 'copy', 'paste']} /><p>{state.last}</p></div>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Toolbar.initialState = { last: '' }
|
|
153
|
+
Toolbar.intent = ({ DOM }) => ({
|
|
154
|
+
PICK: DOM.select('.actions').events('pick').detail(),
|
|
155
|
+
})
|
|
156
|
+
Toolbar.model = {
|
|
157
|
+
PICK: (state, last) => ({ ...state, last }),
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Zag's parts come unstyled. Style them by the `data-scope` and `data-part` attributes the prop getters put on them:
|
|
162
|
+
|
|
163
|
+
```css live
|
|
164
|
+
[data-scope="menu"][data-part="content"] { margin: 0; padding: 0.25rem 0; list-style: none; background: Canvas; border: 1px solid GrayText; z-index: 1; }
|
|
165
|
+
[data-scope="menu"][data-part="item"] { padding: 0.25rem 0.75rem; cursor: pointer; }
|
|
166
|
+
[data-scope="menu"][data-part="item"][data-highlighted] { background: Highlight; color: HighlightText; }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- **`zag`** is the machine package as a namespace (`import * as menu from '@zag-js/menu'`): its `machine` and `connect`.
|
|
170
|
+
- **`render(api, props, instance)`** returns one element. `api` is the machine's connected API: spread its prop getters on your elements (`{...api.getTriggerProps()}`); they carry the ARIA attributes, the ids and the event handlers. It runs again whenever the machine's state changes and when the tag gets new props, and the result is patched into the host. Render **plain elements only**: the adapter patches the result itself, outside the component tree, so a Sygnal component, a widget tag or special JSX (`<Transition>`, `<Portal>`, `<Collection>`…) inside it doesn't run. Pass data in through the tag's props, and keep components and widgets around the tag in the view ([SYG669](https://sygnal.js.org/reference/errors/#syg669) in dev).
|
|
171
|
+
- **Props** go to the machine as they are (`open`, `value`, `positioning`...), always the newest ones: a controlled prop such as `open={state.menuOpen}` drives the machine. `options.props(props, instance)` maps them when they differ (Select turns `value: 'm'` into Zag's `['m']`).
|
|
172
|
+
- **`events`**: `{ eventName: 'onCallback' }` dispatches the callback's details object; `{ eventName: ['onCallback', (details) => detail] }` dispatches what the function returns.
|
|
173
|
+
- **`commands`**: `{ name: (api, options, instance) => … }` for `ELEMENT`.
|
|
174
|
+
|
|
175
|
+
Pin Zag's version (`~1.45.0`, all packages the same): the adapter uses `@zag-js/vanilla`'s machine runtime, whose API changes in Zag 2.0.
|
|
176
|
+
|
|
177
|
+
## Testing
|
|
178
|
+
|
|
179
|
+
In the default mock DOM, an adapter's tag renders its host and nothing runs inside it. `t.widget(selector)` gives the props the view passed and dispatches the adapter's events, so the component's logic can be tested without React or Zag:
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
import { it, expect } from 'vitest'
|
|
183
|
+
import { renderComponent } from 'sygnal'
|
|
184
|
+
import { Review } from './Review.jsx'
|
|
185
|
+
|
|
186
|
+
it('stores the rating', async () => {
|
|
187
|
+
const t = renderComponent(Review)
|
|
188
|
+
await t.ready()
|
|
189
|
+
t.widget('.rating').dispatch('rate', 4)
|
|
190
|
+
await t.next((state) => state.rating === 4)
|
|
191
|
+
expect(t.widget('.rating').props.value).toBe(4)
|
|
192
|
+
t.dispose()
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
To run the React component or the machine itself, use `renderComponent(Review, { dom: 'real' })` (in a jsdom test environment), click with `t.query(...)`, and wait for the state with `t.next`. Zag's positioning and list scrolling use `ResizeObserver`, `CSS.escape` and `Element.prototype.scrollTo`, which jsdom lacks: while a `dom: 'real'` test runs, `renderComponent` adds the missing ones (a `ResizeObserver` that observes nothing, a `scrollTo` that does nothing) and removes them when the last test instance is disposed. An implementation the environment already has is kept. Outside `renderComponent` (a test that calls `run()` itself), stub them in a setup file.
|
|
197
|
+
|
|
198
|
+
## Server rendering
|
|
199
|
+
|
|
200
|
+
`renderToString` renders an adapter's host with its `fallback` inside, like any widget; React and Zag run only in the browser. The client's first render replaces the fallback.
|
|
201
|
+
|
|
202
|
+
## Size
|
|
203
|
+
|
|
204
|
+
Gzipped, added to a small app:
|
|
205
|
+
|
|
206
|
+
| | Adds |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `fromReact` with React 19 + react-dom | 68 KB (the adapter itself: 0.6 KB) |
|
|
209
|
+
| `fromReact` with `preact/compat` | 8 KB |
|
|
210
|
+
| `fromZag` with Zag's dialog machine | 20 KB (the adapter itself: 2 KB) |
|
|
211
|
+
| [Menu](./ui/menu.md), [Select](./ui/select.md), [Combobox](./ui/combobox.md) | 33–34 KB each, 47 KB for the three |
|
|
212
|
+
|
|
213
|
+
## Diagnostics
|
|
214
|
+
|
|
215
|
+
| Code | When |
|
|
216
|
+
|---|---|
|
|
217
|
+
| [SYG666](https://sygnal.js.org/reference/errors/#syg666) | An adapter entry is imported, but a package it needs isn't installed (`sygnal/vite`) |
|
|
218
|
+
| [SYG667](https://sygnal.js.org/reference/errors/#syg667) | `fromZag` or `fromReact` got something that isn't a machine package or a component |
|
|
219
|
+
| [SYG669](https://sygnal.js.org/reference/errors/#syg669) | A `fromZag` render returned a Sygnal component, a widget tag or special JSX, which can't run there (dev) |
|
|
220
|
+
|
|
221
|
+
The widget codes ([SYG140–144](./widgets.md#diagnostics), [SYG660–662](https://sygnal.js.org/reference/errors/#syg660)) apply to adapters as to any widget.
|