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,114 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Select
|
|
3
|
+
|
|
4
|
+
`Select` is a custom select: a button that shows the chosen option and opens a listbox, following the [WAI-ARIA select-only combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/). Keyboard, focus and typeahead come from [Zag.js](https://zagjs.com)'s select machine, rendered with [`fromZag`](../adapters.md#zag-machines-fromzag). It is a [widget](../widgets.md) tag.
|
|
5
|
+
|
|
6
|
+
A native `<select>` is smaller and works everywhere; use `Select` when the options need custom styling, or several values with checkmarks.
|
|
7
|
+
|
|
8
|
+
```jsx live
|
|
9
|
+
import { Select } from 'sygnal/ui/select'
|
|
10
|
+
|
|
11
|
+
const SIZES = [
|
|
12
|
+
{ value: 's', label: 'Small' },
|
|
13
|
+
{ value: 'm', label: 'Medium' },
|
|
14
|
+
{ value: 'l', label: 'Large', disabled: true },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function Order({ state }) {
|
|
18
|
+
return (
|
|
19
|
+
<form className="order">
|
|
20
|
+
<Select className="size" label="Size" items={SIZES} value={state.size} placeholder="Pick a size" name="size" />
|
|
21
|
+
<p>{state.size ? `Size: ${state.size}` : 'No size yet'}</p>
|
|
22
|
+
</form>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Order.initialState = { size: null }
|
|
27
|
+
Order.intent = ({ DOM }) => ({
|
|
28
|
+
SIZE: DOM.select('.size').events('value-change').detail(),
|
|
29
|
+
})
|
|
30
|
+
Order.model = {
|
|
31
|
+
SIZE: (state, size) => ({ ...state, size }),
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The value is controlled: the select shows `state.size`, and `value-change` reports the user's choice. Leave `value` out (or use `defaultValue`) to let the select keep its own value.
|
|
36
|
+
|
|
37
|
+
Install Zag's packages first: `npm install @zag-js/vanilla@~1.45.0 @zag-js/select@~1.45.0` (see [Menu](./menu.md#install)).
|
|
38
|
+
|
|
39
|
+
## Props
|
|
40
|
+
|
|
41
|
+
| Prop | | |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `label` | | The visible label, linked to the trigger (its accessible name). Give every select one |
|
|
44
|
+
| `aria-label`, `aria-labelledby`, `aria-describedby` | | Name or describe the trigger when there is no visible `label` (or in addition to it); they go on the trigger, not the host |
|
|
45
|
+
| `items` | (required) | Strings, or `{ value, label, disabled }` objects. Values are strings |
|
|
46
|
+
| `value` | | Controlled: the value (an array with `multiple`); `null` for none |
|
|
47
|
+
| `defaultValue` | | The start value when `value` is left out |
|
|
48
|
+
| `placeholder` | | Shown while nothing is selected |
|
|
49
|
+
| `multiple` | `false` | Several values; the list stays open while picking |
|
|
50
|
+
| `name`, `form` | | The form field: a hidden native `<select>` carries the value in `FormData` |
|
|
51
|
+
| `disabled`, `readOnly`, `required`, `invalid` | | |
|
|
52
|
+
| `deselectable` | `false` | A click on the selected option clears it |
|
|
53
|
+
| `closeOnSelect` | `true` | With `multiple`: `false` |
|
|
54
|
+
| `loopFocus` | `false` | |
|
|
55
|
+
| `positioning` | | Zag's positioning options: `{ placement: 'bottom-start', sameWidth: true }` |
|
|
56
|
+
| `open` | | Controlled open state; follow `open-change` |
|
|
57
|
+
|
|
58
|
+
## Events and commands
|
|
59
|
+
|
|
60
|
+
| Event | Detail |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `value-change` | The value (with `multiple`: the array of values); `null` when cleared |
|
|
63
|
+
| `open-change` | `true` / `false` |
|
|
64
|
+
|
|
65
|
+
Commands for `ELEMENT`: `open`, `close`, `clear`, and `focus` (the trigger): `CLEAR_SIZE: { ELEMENT: { clear: '.size' } }`.
|
|
66
|
+
|
|
67
|
+
## Keyboard
|
|
68
|
+
|
|
69
|
+
| Key | |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Enter, Space, Down, Up | On the trigger: open the list |
|
|
72
|
+
| A letter | On the trigger: select the next option starting with it; in the list: move to it |
|
|
73
|
+
| Down / Up, Home / End | Move in the list; disabled options are skipped |
|
|
74
|
+
| Enter | Select the highlighted option and close; the focus returns to the trigger |
|
|
75
|
+
| Escape, Tab | Close |
|
|
76
|
+
|
|
77
|
+
## Styling
|
|
78
|
+
|
|
79
|
+
Zag's data attributes: `[data-scope="select"]` with `[data-part="root"]`, `"label"`, `"control"`, `"trigger"`, `"value-text"`, `"indicator"`, `"positioner"`, `"content"`, `"item"`, `"item-text"` and `"item-indicator"`. The trigger and content have `data-state`, options `data-highlighted`, `data-state="checked"` and `data-disabled`, and the value text `data-placeholder-shown` while empty. The item indicator (`✓`) is `hidden` on unselected options.
|
|
80
|
+
|
|
81
|
+
```css live
|
|
82
|
+
.size [data-part='trigger'] { min-width: 10rem; display: flex; justify-content: space-between; }
|
|
83
|
+
.size [data-part='content'] { background: Canvas; border: 1px solid #ddd; }
|
|
84
|
+
.size [data-part='item'] { padding: 4px 8px; }
|
|
85
|
+
.size [data-part='item'][data-highlighted] { background: rgb(99 102 241 / 0.25); }
|
|
86
|
+
.size [data-part='item'][data-disabled] { opacity: 0.5; }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Positioning
|
|
90
|
+
|
|
91
|
+
The listbox renders inside the widget's host, next to the trigger, and Zag positions it with `position: absolute` (the `--x` / `--y` variables on the positioner). Inside a container that clips its content (`overflow: hidden` or `auto`: a card, a scrolling panel, a table cell), the open listbox is cut off at the container's edge. Pass `positioning={{ strategy: 'fixed' }}` there: the positioner is then placed relative to the viewport and escapes the clipping (it still follows the trigger when the page scrolls; an ancestor with a `transform`, `filter` or `contain` still clips it). Other options go in the same object: `positioning={{ placement: 'bottom-end', gutter: 4, strategy: 'fixed' }}`.
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
import { it, expect } from 'vitest'
|
|
97
|
+
import { renderComponent } from 'sygnal'
|
|
98
|
+
import Order from './Order.jsx'
|
|
99
|
+
|
|
100
|
+
it('keeps the chosen size', async () => {
|
|
101
|
+
const t = renderComponent(Order)
|
|
102
|
+
await t.ready()
|
|
103
|
+
t.widget('.size').dispatch('value-change', 'm')
|
|
104
|
+
await t.next((state) => state.size === 'm')
|
|
105
|
+
expect(t.widget('.size').props.value).toBe('m')
|
|
106
|
+
t.dispose()
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For keyboard tests, see [Menu](./menu.md#testing).
|
|
111
|
+
|
|
112
|
+
## Size
|
|
113
|
+
|
|
114
|
+
Gzipped, in a small app: Select adds **33 KB** (mostly Zag's select machine, its positioning and `@zag-js/vanilla`). With Menu and Combobox, the three add 47 KB together.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Tabs
|
|
3
|
+
|
|
4
|
+
`tabs()` is a [behavior](../behaviors.md) that turns your own buttons and panels into tabs, following the [WAI-ARIA tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). `tabsAttrs()` computes the attributes to spread on them: roles, ids that link each tab to its panel, `aria-selected`, the roving `tabindex` and `hidden` on the other panels.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { tabs, tabsAttrs } from 'sygnal/ui'
|
|
8
|
+
|
|
9
|
+
const SECTIONS = [
|
|
10
|
+
{ id: 'general', label: 'General' },
|
|
11
|
+
{ id: 'privacy', label: 'Privacy' },
|
|
12
|
+
{ id: 'billing', label: 'Billing' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function Settings({ state, uid }) {
|
|
16
|
+
const a = tabsAttrs(state.tabs, uid)
|
|
17
|
+
return (
|
|
18
|
+
<div className="settings">
|
|
19
|
+
<div className="tab-list" {...a.list} aria-label="Settings">
|
|
20
|
+
{SECTIONS.map((s) => <button className="tab" {...a.tab(s.id)}>{s.label}</button>)}
|
|
21
|
+
</div>
|
|
22
|
+
{SECTIONS.map((s) => (
|
|
23
|
+
<section className="tab-panel" {...a.panel(s.id)}>
|
|
24
|
+
<h2>{s.label}</h2>
|
|
25
|
+
</section>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Settings.uses = { tabs: tabs({ tab: '.tab', selected: 'general' }) }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`state.tabs.selected` is the selected tab's value. Render every panel: the ones not selected get `hidden`, and each tab's `aria-controls` points at its panel.
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
| Option | Default | |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `tab` | (required) | The tabs: a selector or a [control](https://sygnal.js.org/guide/controls/) |
|
|
41
|
+
| `selected` | | The tab selected at the start. Without it, the first tab rendered is selected |
|
|
42
|
+
| `orientation` | `'horizontal'` | `'vertical'`: Up and Down move between the tabs instead of Left and Right |
|
|
43
|
+
| `activation` | `'automatic'` | `'manual'`: the arrow keys only move the focus; Enter or Space selects |
|
|
44
|
+
| `loop` | `true` | The arrow keys wrap from the last tab to the first |
|
|
45
|
+
| `id` | the key in `uses` | The prefix of the ids |
|
|
46
|
+
|
|
47
|
+
## Keyboard
|
|
48
|
+
|
|
49
|
+
| Key | |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Tab | Into the tab list, onto the selected tab; Tab again goes to its panel |
|
|
52
|
+
| Right / Left (Down / Up when vertical) | The next or previous tab, wrapping; it is selected too, unless `activation: 'manual'` |
|
|
53
|
+
| Home / End | The first or last tab |
|
|
54
|
+
| Enter / Space | Selects the focused tab (a button click) |
|
|
55
|
+
|
|
56
|
+
Disabled tabs (`disabled`) are skipped.
|
|
57
|
+
|
|
58
|
+
## State, actions and attributes
|
|
59
|
+
|
|
60
|
+
`state.tabs` is `{ id, selected, orientation }`. Actions: `tabs.SELECT` (a value; a click on a tab sends its own) and `tabs.MOVE` (a value: focus that tab, and select it unless manual). Select a tab from the model with `next('tabs.SELECT', 'billing')`, or from your intent with `'tabs.SELECT': DOM.click('.go-billing').mapTo('billing')`.
|
|
61
|
+
|
|
62
|
+
`tabsAttrs(state.tabs, uid)` returns:
|
|
63
|
+
|
|
64
|
+
| | Attributes |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `list` | `role="tablist"`, `aria-orientation` |
|
|
67
|
+
| `tab(value)` | `id`, `role="tab"`, `aria-selected`, `aria-controls`, `tabindex` (`0` on the selected tab, `-1` on the others), `type="button"`, `data-value`, `data-state` (`active` / `inactive`) |
|
|
68
|
+
| `panel(value)` | `id`, `role="tabpanel"`, `aria-labelledby`, `tabindex="0"`, `hidden` unless selected, `data-state` |
|
|
69
|
+
|
|
70
|
+
Values are compared as strings.
|
|
71
|
+
|
|
72
|
+
`tabsAttrs(state.tabs, uid, values)` takes the tabs' values, in order, as an optional third argument (leave disabled tabs out). Pass it when the tabs can change: a `selected` value that isn't among them (its tab was removed) then shows the first tab selected, so there is always a tab to Tab into and a panel shown. Without it, the first tab rendered is selected only while nothing is.
|
|
73
|
+
|
|
74
|
+
## Closable tabs
|
|
75
|
+
|
|
76
|
+
When the user closes the selected tab, select a neighbour in the same reducer and move the focus to it (the close button that had the focus is gone):
|
|
77
|
+
|
|
78
|
+
```jsx live
|
|
79
|
+
import { ABORT } from 'sygnal'
|
|
80
|
+
import { tabs, tabsAttrs } from 'sygnal/ui'
|
|
81
|
+
|
|
82
|
+
function Editor({ state, uid }) {
|
|
83
|
+
const a = tabsAttrs(state.tabs, uid, state.files.map((f) => f.id))
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<div className="tab-list" {...a.list} aria-label="Open files">
|
|
87
|
+
{state.files.map((f) => (
|
|
88
|
+
<span className="tab-wrap">
|
|
89
|
+
<button className="tab" {...a.tab(f.id)}>{f.name}</button>
|
|
90
|
+
<button className="close" data-value={f.id} aria-label={'Close ' + f.name}>×</button>
|
|
91
|
+
</span>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
{state.files.map((f) => <section className="tab-panel" {...a.panel(f.id)}>{f.text}</section>)}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Editor.uses = { tabs: tabs({ tab: '.tab' }) }
|
|
100
|
+
Editor.initialState = {
|
|
101
|
+
files: [
|
|
102
|
+
{ id: 'readme', name: 'README.md', text: 'About this project' },
|
|
103
|
+
{ id: 'main', name: 'main.js', text: 'run(App)' },
|
|
104
|
+
{ id: 'styles', name: 'styles.css', text: 'body { margin: 0 }' },
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Editor.intent = ({ DOM }) => ({ CLOSE: DOM.click('.close').data('value') })
|
|
109
|
+
|
|
110
|
+
// the file after the closed one, or the one before it when it was the last
|
|
111
|
+
const neighbour = (files, id) => {
|
|
112
|
+
const i = files.findIndex((f) => f.id === id), rest = files.filter((f) => f.id !== id)
|
|
113
|
+
return rest[Math.min(i, rest.length - 1)]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Editor.model = {
|
|
117
|
+
CLOSE: {
|
|
118
|
+
STATE: (state, id) => ({
|
|
119
|
+
...state,
|
|
120
|
+
files: state.files.filter((f) => f.id !== id),
|
|
121
|
+
tabs: { ...state.tabs, selected: state.tabs.selected === id ? (neighbour(state.files, id)?.id ?? null) : state.tabs.selected },
|
|
122
|
+
}),
|
|
123
|
+
ELEMENT: (state, id) => {
|
|
124
|
+
const next = neighbour(state.files, id)
|
|
125
|
+
return next ? { focus: `.tab[data-value="${next.id}"]` } : ABORT
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
(`ELEMENT` gets the state before the action, as every non-`STATE` entry does.)
|
|
132
|
+
|
|
133
|
+
## Styling
|
|
134
|
+
|
|
135
|
+
```css live
|
|
136
|
+
.tab-list { display: flex; gap: 4px; border-bottom: 1px solid #ddd; }
|
|
137
|
+
.tab[data-state='active'] { border-bottom: 2px solid currentColor; font-weight: 600; }
|
|
138
|
+
.tab-panel:focus-visible { outline: 2px solid #2563eb; }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Testing
|
|
142
|
+
|
|
143
|
+
The arrow keys read the order of the tabs on the page, so test them with `dom: 'real'`. Clicks and `simulateAction` work on the mock DOM:
|
|
144
|
+
|
|
145
|
+
```jsx
|
|
146
|
+
import { it, expect } from 'vitest'
|
|
147
|
+
import { renderComponent } from 'sygnal'
|
|
148
|
+
import Settings from './Settings.jsx'
|
|
149
|
+
|
|
150
|
+
it('selects a tab and shows its panel', async () => {
|
|
151
|
+
const t = renderComponent(Settings)
|
|
152
|
+
await t.ready()
|
|
153
|
+
t.simulateEvent('.tab', 'click', { data: { value: 'privacy' } })
|
|
154
|
+
await t.next((s) => s.tabs.selected === 'privacy')
|
|
155
|
+
expect(t.query('.tab[data-value="privacy"]').getAttribute('aria-selected')).toBe('true')
|
|
156
|
+
expect(t.actions.map((a) => a.type)).toEqual(['INITIALIZE', 'tabs.SELECT'])
|
|
157
|
+
t.dispose()
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The [UI parts overview](./overview.md#testing) shows a keyboard test with Testing Library.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Toaster
|
|
3
|
+
|
|
4
|
+
`<Toaster>` shows short notifications ("Saved", "Upload failed") that any component sends as an event. Render it once, near the root of the app:
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
// App.jsx
|
|
8
|
+
import { Toaster } from 'sygnal/ui'
|
|
9
|
+
import { Editor } from './Editor.jsx'
|
|
10
|
+
|
|
11
|
+
export default function App({ state }) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="app">
|
|
14
|
+
<h1>{state.title}</h1>
|
|
15
|
+
<Editor />
|
|
16
|
+
<Toaster />
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
App.initialState = { title: 'Notes' }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { run, makeTimerDriver } from 'sygnal'
|
|
26
|
+
import App from './App.jsx'
|
|
27
|
+
|
|
28
|
+
run(App, { TIMER: makeTimerDriver() })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then send a toast from anywhere, through the `EVENTS` bus:
|
|
32
|
+
|
|
33
|
+
```jsx live-file=./Editor.jsx
|
|
34
|
+
// Editor.jsx
|
|
35
|
+
import { event } from 'sygnal'
|
|
36
|
+
|
|
37
|
+
export function Editor({ state }) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="editor">
|
|
40
|
+
<label>Note <textarea className="text" value={state.text} /></label>
|
|
41
|
+
<button className="save">Save</button>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Editor.initialState = { text: '' }
|
|
47
|
+
Editor.isolatedState = true
|
|
48
|
+
Editor.intent = ({ DOM }) => ({ TEXT: DOM.input('.text').value(), SAVE: DOM.click('.save') })
|
|
49
|
+
Editor.model = {
|
|
50
|
+
TEXT: (state, text) => ({ ...state, text }),
|
|
51
|
+
SAVE: { EVENTS: event('TOAST', { text: 'Note saved', kind: 'success' }) },
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The timer driver dismisses toasts after their timeout. Without it, they stay until dismissed.
|
|
56
|
+
|
|
57
|
+
## The TOAST event
|
|
58
|
+
|
|
59
|
+
| Field | Default | |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `text` | (required) | The message. `event('TOAST', 'Saved')` is short for `{ text: 'Saved' }` |
|
|
62
|
+
| `kind` | `'info'` | `'info'`, `'success'`, `'warning'` or `'error'`. Errors are announced at once (`role="alert"`), the others politely (`role="status"`) |
|
|
63
|
+
| `timeoutMs` | `5000` | ms until it dismisses itself; `0` keeps it until the user dismisses it |
|
|
64
|
+
| `id` | `'t1'`, `'t2'`… | A toast sent with the `id` of a shown one replaces it in place and starts its timeout again. Ids compare as strings (`7` and `'7'` are the same toast); the automatic ones never equal an id of a shown toast |
|
|
65
|
+
|
|
66
|
+
`event('TOAST_DISMISS', id)` removes the toast with that `id`; `event('TOAST_DISMISS')` removes all of them. With ids, a long task can report its progress in one toast:
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
Upload.model = {
|
|
70
|
+
START: { EVENTS: event('TOAST', { id: 'upload', text: 'Uploading…', timeoutMs: 0 }) },
|
|
71
|
+
DONE: { EVENTS: event('TOAST', { id: 'upload', text: 'Upload complete', kind: 'success' }) },
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Props
|
|
76
|
+
|
|
77
|
+
| Prop | Default | |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `label` | `'Notifications'` | The region's accessible name |
|
|
80
|
+
| `dismissLabel` | `'Dismiss'` | The text of each toast's dismiss button; its accessible name is `Dismiss: <text>` |
|
|
81
|
+
| `pauseOnHover` | `true` | The timeouts stop while the pointer or the keyboard focus is in the region, and start over when it leaves |
|
|
82
|
+
| `transition` | `'toast'` | The [Transition](https://sygnal.js.org/advanced/transitions/) class prefix: `toast-enter-from`, `toast-leave-to`… |
|
|
83
|
+
| `duration` | `200` | How long a dismissed toast stays for its leave animation, in ms |
|
|
84
|
+
| `className` | | More classes on the region |
|
|
85
|
+
|
|
86
|
+
## Markup and styling
|
|
87
|
+
|
|
88
|
+
```html
|
|
89
|
+
<div class="toaster-home">
|
|
90
|
+
<section class="toaster" aria-label="Notifications" popover="manual" data-paused>
|
|
91
|
+
<div class="toaster-status" role="status" aria-live="polite">
|
|
92
|
+
<div class="toaster-list">
|
|
93
|
+
<div class="toast" data-kind="success">
|
|
94
|
+
<span class="toast-text">Note saved</span>
|
|
95
|
+
<button class="toast-dismiss" aria-label="Dismiss: Note saved">Dismiss</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="toaster-alert" role="alert"><div class="toaster-list"></div></div>
|
|
100
|
+
</section>
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The region is a popover, so the browser draws it in the top layer, centred and with a border. Place it in a corner and style the toasts:
|
|
105
|
+
|
|
106
|
+
```css live
|
|
107
|
+
.toaster { inset: auto 1rem 1rem auto; margin: 0; padding: 0; border: 0; background: none; overflow: visible; }
|
|
108
|
+
.toast { display: flex; gap: 0.75rem; align-items: center; margin-top: 0.5rem; padding: 0.75rem 1rem;
|
|
109
|
+
border-radius: 6px; background: #1f2937; color: #fff; }
|
|
110
|
+
.toast[data-kind='error'] { background: #b91c1c; }
|
|
111
|
+
.toast-enter-active, .toast-leave-active { transition: opacity 200ms, transform 200ms; }
|
|
112
|
+
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translateY(0.5rem); }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Above modal dialogs
|
|
116
|
+
|
|
117
|
+
An open modal `<dialog>` makes the rest of the page inert: a toast outside it would be visible but couldn't be clicked or reached with Tab, and screen readers would skip it. While a modal dialog is open, the Toaster moves its region into that dialog, where it stays on top and usable, and back when the dialog closes or is removed. Toasts shown before the dialog opened move with it. Nothing is needed on your side; the region keeps its events, Collection items and timers wherever it is.
|
|
118
|
+
|
|
119
|
+
It sees the modal dialogs of the page and of its own shadow root, so a Toaster inside a [custom element](../web-components.md) moves into that element's dialogs too.
|
|
120
|
+
|
|
121
|
+
## Accessibility
|
|
122
|
+
|
|
123
|
+
- The `role="status"` and `role="alert"` regions are always rendered, even when empty, so screen readers announce a toast when it is added.
|
|
124
|
+
- Keep toasts short and don't put the only copy of important information in them: they go away. Use `timeoutMs: 0` for a message the user must act on.
|
|
125
|
+
- The timeouts pause while the user hovers or tabs into the toasts ([WCAG 2.2.1](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable)), and stay paused while either the pointer or the focus is still there.
|
|
126
|
+
- Dismissing a toast with its focused Dismiss button moves the focus to the next toast's Dismiss button, or the previous one's, and after the last toast back to where it was before it entered the toasts.
|
|
127
|
+
|
|
128
|
+
## Testing
|
|
129
|
+
|
|
130
|
+
`renderComponent()` renders the Toaster with the rest of the app; the timeouts run on fake timers without a driver:
|
|
131
|
+
|
|
132
|
+
```jsx
|
|
133
|
+
import { it, expect, vi } from 'vitest'
|
|
134
|
+
import { renderComponent } from 'sygnal'
|
|
135
|
+
import App from './App.jsx'
|
|
136
|
+
|
|
137
|
+
it('shows a toast when a note is saved, and removes it after 5 s', async () => {
|
|
138
|
+
vi.useFakeTimers()
|
|
139
|
+
const t = renderComponent(App)
|
|
140
|
+
await t.ready()
|
|
141
|
+
t.simulateEvent('.save', 'click')
|
|
142
|
+
await t.settle()
|
|
143
|
+
expect(t.query('.toast-text').textContent).toBe('Note saved')
|
|
144
|
+
expect(t.emitted.map((e) => e.type)).toContain('TOAST')
|
|
145
|
+
|
|
146
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
147
|
+
await t.settle()
|
|
148
|
+
expect(t.queryAll('.toast')).toHaveLength(0)
|
|
149
|
+
t.dispose()
|
|
150
|
+
vi.useRealTimers()
|
|
151
|
+
})
|
|
152
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Tooltip
|
|
3
|
+
|
|
4
|
+
`tooltip()` is a [behavior](../behaviors.md) that shows a short description of a control when the pointer rests on it or it gets the keyboard focus. The tip is a `popover="manual"` element, so it is drawn in the top layer, above dialogs and scrolling containers. CSS anchor positioning places it next to its trigger; there is no JavaScript positioning. The show and hide delays are [timers](../timers.md), so they are cancelled when the pointer leaves early and they run on fake timers in tests.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { tooltip } from 'sygnal/ui'
|
|
8
|
+
|
|
9
|
+
function Toolbar({ uid }) {
|
|
10
|
+
const anchor = '--' + uid('save')
|
|
11
|
+
return (
|
|
12
|
+
<div className="toolbar">
|
|
13
|
+
<button className="save" aria-describedby={uid('save-tip')} style={{ anchorName: anchor }}>Save</button>
|
|
14
|
+
<div className="tip" id={uid('save-tip')} role="tooltip" popover="manual" style={{ positionAnchor: anchor }}>
|
|
15
|
+
Save the draft (Ctrl+S)
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Toolbar.uses = { saveTip: tooltip({ trigger: '.save', tip: '.tip' }) }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { run, makeTimerDriver } from 'sygnal'
|
|
26
|
+
import Toolbar from './Toolbar.jsx'
|
|
27
|
+
|
|
28
|
+
run(Toolbar, { TIMER: makeTimerDriver() })
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The delays need the timer driver in `run()`. Tests don't: [fake timers](../timers.md#testing) drive them.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
| Option | Default | |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `trigger` | (required) | The element it describes |
|
|
38
|
+
| `tip` | (required) | The tooltip element: `role="tooltip"`, `popover="manual"` |
|
|
39
|
+
| `showDelay` | `500` | ms the pointer or focus must stay before it shows |
|
|
40
|
+
| `hideDelay` | `100` | ms before it hides after the pointer or focus leaves |
|
|
41
|
+
|
|
42
|
+
## Behavior
|
|
43
|
+
|
|
44
|
+
- Hovering or focusing the trigger shows the tip after `showDelay`; leaving before that never shows it.
|
|
45
|
+
- Leaving hides it after `hideDelay`. Moving the pointer from the trigger onto the tip keeps it open, so the tip can be read and selected ([WCAG 1.4.13](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus)).
|
|
46
|
+
- The pointer and the focus count apart: it hides once both have left, so the mouse moving away doesn't hide a tip the keyboard focus still shows.
|
|
47
|
+
- Touch is left out: a tap focuses the trigger, which shows the tip as focus does.
|
|
48
|
+
- Escape hides it at once, without moving the focus. (The behavior listens for it on the document only while the tip is shown or about to be.)
|
|
49
|
+
|
|
50
|
+
`state.saveTip` is `{ open, pending, hover, focus }`: `pending` is `'show'` or `'hide'` while a delay runs; `hover` and `focus` say where the pointer and the focus are. The actions are `saveTip.ENTER` and `saveTip.LEAVE` (data: `'hover'` or `'focus'`), `saveTip.SHOW`, `saveTip.HIDE` (the timers), `saveTip.ESCAPE` and `saveTip.TOGGLED`. The timers are declared under the names `saveTip.show` and `saveTip.hide`, next to the component's own [`timers`](../timers.md).
|
|
51
|
+
|
|
52
|
+
## Positioning
|
|
53
|
+
|
|
54
|
+
The trigger gets an anchor name and the tip refers to it. The anchor name must be unique on the page, so build it from `uid()` in the view, as above. The rest is CSS:
|
|
55
|
+
|
|
56
|
+
```css live
|
|
57
|
+
.tip {
|
|
58
|
+
position-area: top; /* above the trigger, centred */
|
|
59
|
+
position-try: flip-block; /* below it when there is no room above */
|
|
60
|
+
inset: auto;
|
|
61
|
+
margin: 0 0 6px;
|
|
62
|
+
padding: 4px 8px;
|
|
63
|
+
border: none;
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
background: #222;
|
|
66
|
+
color: #fff;
|
|
67
|
+
font-size: 0.875rem;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`inset: auto` and `margin` replace the popover's default centring. Browsers without CSS anchor positioning need JavaScript positioning: see [Floating UI for older browsers](./overview.md#floating-ui-for-older-browsers).
|
|
72
|
+
|
|
73
|
+
Safari 26 places the tip wrongly when the trigger is inside a `position: fixed` element (a fixed toolbar or header) and the page is scrolled: the tip moves up by the scroll distance. Triggers in the normal flow of the page are placed correctly. For triggers in a fixed bar, the Floating UI recipe works in every browser.
|
|
74
|
+
|
|
75
|
+
## Accessibility
|
|
76
|
+
|
|
77
|
+
- `aria-describedby` on the trigger points at the tip, so screen readers read the tip as the trigger's description. The trigger still needs its own name (its text or `aria-label`).
|
|
78
|
+
- Keep the tip short and plain text; it can't take the focus. For anything interactive, use a [popover](./popover.md).
|
|
79
|
+
- Don't rely on a tooltip for touch users: there is no hover, and a tap focuses the trigger only briefly.
|
|
80
|
+
|
|
81
|
+
## Testing
|
|
82
|
+
|
|
83
|
+
```jsx
|
|
84
|
+
import { it, expect, vi } from 'vitest'
|
|
85
|
+
import { renderComponent } from 'sygnal'
|
|
86
|
+
import Toolbar from './Toolbar.jsx'
|
|
87
|
+
|
|
88
|
+
it('shows the tip after the delay', async () => {
|
|
89
|
+
vi.useFakeTimers()
|
|
90
|
+
const t = renderComponent(Toolbar)
|
|
91
|
+
await t.ready()
|
|
92
|
+
t.simulateEvent('.save', 'pointerenter')
|
|
93
|
+
await t.next((s) => s.saveTip.pending === 'show')
|
|
94
|
+
expect(t.timers()).toEqual([{ name: 'saveTip.show', after: 500, action: 'saveTip.SHOW', component: 'Toolbar' }])
|
|
95
|
+
|
|
96
|
+
await vi.advanceTimersByTimeAsync(500)
|
|
97
|
+
await t.settle()
|
|
98
|
+
expect(t.commands('ELEMENT')).toEqual([{ showPopover: '.tip' }])
|
|
99
|
+
expect(t.actions.map((a) => a.type)).toEqual(['INITIALIZE', 'saveTip.ENTER', 'saveTip.SHOW'])
|
|
100
|
+
t.dispose()
|
|
101
|
+
vi.useRealTimers()
|
|
102
|
+
})
|
|
103
|
+
```
|