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,124 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Dialog
|
|
3
|
+
|
|
4
|
+
`dialog()` is a [behavior](../behaviors.md) for a native `<dialog>`. It opens the dialog with `showModal()`, so the browser does the hard parts: it traps the focus inside the dialog, makes the rest of the page inert, closes it on Escape and returns the focus when it closes. The behavior keeps `open` and `returnValue` in state, whichever way the dialog closed.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { dialog } from 'sygnal/ui'
|
|
8
|
+
|
|
9
|
+
function Profile({ state, uid }) {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<button className="edit-profile">Edit profile</button>
|
|
13
|
+
<dialog className="profile" aria-labelledby={uid('title')}>
|
|
14
|
+
<h2 id={uid('title')}>Edit profile</h2>
|
|
15
|
+
<label>Name <input className="name" value={state.name} /></label>
|
|
16
|
+
<button className="save">Save</button>
|
|
17
|
+
<button className="cancel">Cancel</button>
|
|
18
|
+
</dialog>
|
|
19
|
+
<p>{state.profile.returnValue === 'saved' ? 'Profile saved' : ''}</p>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Profile.initialState = { name: '' }
|
|
25
|
+
Profile.uses = { profile: dialog({ dialog: '.profile', trigger: '.edit-profile', close: '.cancel' }) }
|
|
26
|
+
Profile.intent = ({ DOM }) => ({
|
|
27
|
+
NAME: DOM.input('.name').value(),
|
|
28
|
+
SAVE: DOM.click('.save'),
|
|
29
|
+
})
|
|
30
|
+
Profile.model = {
|
|
31
|
+
NAME: (state, name) => ({ ...state, name }),
|
|
32
|
+
SAVE: { ELEMENT: { close: '.profile', returnValue: 'saved' } },
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A click on `.edit-profile` opens the dialog and a click on `.cancel` closes it. Save closes it from the model with a return value, through the built-in [`ELEMENT` sink](../element-commands.md).
|
|
37
|
+
|
|
38
|
+
## Options
|
|
39
|
+
|
|
40
|
+
| Option | Default | |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `dialog` | (required) | The `<dialog>` element: a selector or a [control](https://sygnal.js.org/guide/controls/). It must match only this behavior's dialog: a selector that also matches a dialog nested in it (`dialog`, a shared class) gets that dialog's events too |
|
|
43
|
+
| `trigger` | | Its clicks open the dialog |
|
|
44
|
+
| `close` | | Its clicks close the dialog, with the return value `''` |
|
|
45
|
+
| `modal` | `true` | `false` opens it with `show()`: not modal, the page stays usable |
|
|
46
|
+
| `cancelable` | `true` | `false` keeps Escape from closing it: the `cancel` event is prevented, and opening it sets `closedby="none"` (Chromium closes a dialog on a second Escape otherwise; the attribute goes again when it closes). `CANCEL` still runs on each Escape. In a browser without `closedby`, a second Escape can still close it; the `CLOSED` action tells you |
|
|
47
|
+
| `returnFocus` | `true` | When the dialog closes and the focus was lost, focus the element that opened it (the clicked trigger, when several match `trigger`), else the trigger. A selector names another element; `false` leaves the focus to the browser |
|
|
48
|
+
|
|
49
|
+
The browser returns the focus to the element that had it before the dialog opened. Safari doesn't focus a button when it is clicked, so after a mouse click the focus would go back to the page itself; `returnFocus` focuses the trigger instead. It does nothing when the browser already moved the focus somewhere. It works for a dialog the host renders only while it is open, too.
|
|
50
|
+
|
|
51
|
+
## State and actions
|
|
52
|
+
|
|
53
|
+
`state.profile` is `{ open, returnValue }`.
|
|
54
|
+
|
|
55
|
+
| Action | Data | |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `profile.OPEN` | | Opens the dialog (no change when it is open) |
|
|
58
|
+
| `profile.CLOSE` | the return value | Closes it with that return value |
|
|
59
|
+
| `profile.CLOSED` | the return value | The dialog closed, whichever way: sets `open: false` and `returnValue` |
|
|
60
|
+
| `profile.TOGGLED` | `true` / `false` | The dialog's `toggle` event: a dialog opened without the model (for example with `commandfor`) sets `open: true` |
|
|
61
|
+
| `profile.CANCEL` | the event | Escape was pressed (the dialog closes next, unless `cancelable: false`). The `cancel` event; with `cancelable: false`, the Escape `keydown` in the dialog (`closedby="none"` stops the `cancel` event) |
|
|
62
|
+
| `profile.SYNC` | `false` | The dialog left the page while open (a page change, a conditional render), or OPEN's `showModal()` threw (a disconnected dialog, a popover open on it): sets `open: false`, as no close event comes (your `close` listeners don't run for a dialog that never opened) |
|
|
63
|
+
|
|
64
|
+
`OPEN` and `CLOSE` check the dialog itself before they act: `showModal()` only on a closed dialog, `close()` only on an open one. A dialog removed while open opens again when it is back.
|
|
65
|
+
|
|
66
|
+
React to a close in your own model with an entry for the namespaced action. It runs after the behavior's, on the full state:
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
Profile.model = {
|
|
70
|
+
'profile.CLOSED': (state, returnValue) => (returnValue === 'saved' ? { ...state, savedName: state.name } : state),
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Open the dialog from another place with an intent action of the same name, `'profile.OPEN': DOM.click('.avatar')`, or from a model entry that sends `{ showModal: '.profile' }` to `ELEMENT`.
|
|
75
|
+
|
|
76
|
+
## Accessibility
|
|
77
|
+
|
|
78
|
+
- Name the dialog: `aria-labelledby` pointing at its heading, as above, or `aria-label`.
|
|
79
|
+
- Put a close button inside. Escape closes a modal dialog, but not everyone has a keyboard.
|
|
80
|
+
- The dialog element has `role="dialog"`; a modal one is also `aria-modal` to assistive technology.
|
|
81
|
+
- A form inside the dialog with `method="dialog"` closes it on submit, with the submit button's `value` as the return value; `CLOSED` sees it.
|
|
82
|
+
|
|
83
|
+
## Styling
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
.profile { border: none; border-radius: 8px; padding: 1.5rem; }
|
|
87
|
+
.profile::backdrop { background: rgb(0 0 0 / 0.4); }
|
|
88
|
+
.profile[open] { animation: fade-in 150ms ease-out; }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Testing
|
|
92
|
+
|
|
93
|
+
The mock DOM records the `showModal` and `close` commands without running them, so simulate the dialog's `close` event as the browser sends it:
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
import { it, expect } from 'vitest'
|
|
97
|
+
import { renderComponent } from 'sygnal'
|
|
98
|
+
import Profile from './Profile.jsx'
|
|
99
|
+
|
|
100
|
+
it('opens the dialog and keeps the return value', async () => {
|
|
101
|
+
const t = renderComponent(Profile)
|
|
102
|
+
await t.ready()
|
|
103
|
+
t.simulateEvent('.edit-profile', 'click')
|
|
104
|
+
await t.next((s) => s.profile.open)
|
|
105
|
+
expect(t.commands('ELEMENT')).toEqual([{ showModal: '.profile' }])
|
|
106
|
+
|
|
107
|
+
t.simulateEvent('.save', 'click')
|
|
108
|
+
await t.settle()
|
|
109
|
+
expect(t.commands('ELEMENT').at(-1)).toEqual({ close: '.profile', returnValue: 'saved' })
|
|
110
|
+
|
|
111
|
+
t.simulateEvent('.profile', 'close', { target: { returnValue: 'saved' } })
|
|
112
|
+
await t.next((s) => !s.profile.open)
|
|
113
|
+
expect(t.state.profile.returnValue).toBe('saved')
|
|
114
|
+
expect(t.actions.map((a) => [a.type, a.cause])).toEqual([
|
|
115
|
+
['INITIALIZE', 'built-in'],
|
|
116
|
+
['profile.OPEN', 'behavior'],
|
|
117
|
+
['SAVE', 'intent'],
|
|
118
|
+
['profile.CLOSED', 'behavior'],
|
|
119
|
+
])
|
|
120
|
+
t.dispose()
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With `dom: 'real'`, the commands run on the jsdom elements; focus trapping and the backdrop need a real browser.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Disclosure
|
|
3
|
+
|
|
4
|
+
`disclosure()` is a [behavior](../behaviors.md) for a button that shows and hides one section of content, following the [WAI-ARIA disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/). `disclosureAttrs()` gives the button `aria-expanded` and `aria-controls`, and the section its `id` and `hidden`.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { disclosure, disclosureAttrs } from 'sygnal/ui'
|
|
8
|
+
|
|
9
|
+
function Order({ state, uid }) {
|
|
10
|
+
const a = disclosureAttrs(state.details, uid)
|
|
11
|
+
return (
|
|
12
|
+
<article className="order">
|
|
13
|
+
<h2>Order {state.number}</h2>
|
|
14
|
+
<button className="details-toggle" {...a.trigger}>{state.details.open ? 'Hide details' : 'Show details'}</button>
|
|
15
|
+
<div className="details" {...a.panel}>
|
|
16
|
+
<p>Shipped to {state.address}</p>
|
|
17
|
+
</div>
|
|
18
|
+
</article>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Order.initialState = { number: 1042, address: '1 Main St' }
|
|
23
|
+
Order.uses = { details: disclosure({ trigger: '.details-toggle' }) }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For a plain show-and-hide section with no state in the model, the native `<details>` and `<summary>` elements need no code at all. Use the behavior when the model needs to know or set whether the section is open.
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
30
|
+
| Option | Default | |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `trigger` | (required) | The button: a selector or a [control](https://sygnal.js.org/guide/controls/) |
|
|
33
|
+
| `open` | `false` | Open at the start |
|
|
34
|
+
| `id` | the key in `uses` | The prefix of the ids |
|
|
35
|
+
|
|
36
|
+
## State, actions and attributes
|
|
37
|
+
|
|
38
|
+
`state.details` is `{ id, open }`. Actions: `details.TOGGLE` (a click on the button), `details.OPEN` and `details.CLOSE` (no change when it already is).
|
|
39
|
+
|
|
40
|
+
`disclosureAttrs(state.details, uid)` returns `trigger` (`aria-expanded`, `aria-controls`, `type="button"`, `data-state`) and `panel` (`id`, `hidden` unless open, `data-state`: `open` / `closed`).
|
|
41
|
+
|
|
42
|
+
## Testing
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
import { it, expect } from 'vitest'
|
|
46
|
+
import { renderComponent } from 'sygnal'
|
|
47
|
+
import Order from './Order.jsx'
|
|
48
|
+
|
|
49
|
+
it('shows and hides the details', async () => {
|
|
50
|
+
const t = renderComponent(Order)
|
|
51
|
+
await t.ready()
|
|
52
|
+
expect(t.query('.details').hidden).toBe(true)
|
|
53
|
+
t.simulateEvent('.details-toggle', 'click')
|
|
54
|
+
await t.next((s) => s.details.open)
|
|
55
|
+
expect(t.query('.details-toggle').getAttribute('aria-expanded')).toBe('true')
|
|
56
|
+
expect(t.query('.details').hidden).toBe(false)
|
|
57
|
+
expect(t.actions.map((a) => a.type)).toEqual(['INITIALIZE', 'details.TOGGLE'])
|
|
58
|
+
t.dispose()
|
|
59
|
+
})
|
|
60
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Menu
|
|
3
|
+
|
|
4
|
+
`Menu` is a menu button: a trigger that opens a list of actions, following the [WAI-ARIA menu button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). The keyboard handling, focus management and typeahead come from [Zag.js](https://zagjs.com)'s menu machine; Sygnal renders it with [`fromZag`](../adapters.md#zag-machines-fromzag). It is a [widget](../widgets.md) tag: render it with a class, and read its events in the intent like any element's.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { Menu } from 'sygnal/ui/menu'
|
|
8
|
+
|
|
9
|
+
const ACTIONS = [
|
|
10
|
+
{ value: 'rename', label: 'Rename' },
|
|
11
|
+
{ value: 'duplicate', label: 'Duplicate' },
|
|
12
|
+
{ separator: true },
|
|
13
|
+
{ value: 'delete', label: 'Delete' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
function Card({ state }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="card">
|
|
19
|
+
<h3>{state.title}</h3>
|
|
20
|
+
<Menu className="card-actions" label="Actions" items={ACTIONS} />
|
|
21
|
+
<p>{state.last ? `Last action: ${state.last}` : ''}</p>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Card.initialState = { title: 'Groceries', last: '' }
|
|
27
|
+
Card.intent = ({ DOM }) => ({
|
|
28
|
+
ACTION: DOM.select('.card-actions').events('select').detail(),
|
|
29
|
+
})
|
|
30
|
+
Card.model = {
|
|
31
|
+
ACTION: (state, last) => ({ ...state, last }),
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`DOM.select('.card-actions').events('select').detail()` gives the value of the item the user picked (`'rename'`). The menu closes and the focus goes back to the trigger.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Each Zag-based part has a subpath of its own (`sygnal/ui/menu`, `sygnal/ui/select`, `sygnal/ui/combobox`), so `sygnal/ui` (Dialog, Popover, Tabs…) never needs Zag, and a part needs only Zag's runtime and its own machine. They are optional peer dependencies of `sygnal`; install them in one version:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @zag-js/vanilla@~1.45.0 @zag-js/menu@~1.45.0 # Menu (sygnal/ui/menu)
|
|
43
|
+
npm install @zag-js/vanilla@~1.45.0 @zag-js/select@~1.45.0 # Select (sygnal/ui/select)
|
|
44
|
+
npm install @zag-js/vanilla@~1.45.0 @zag-js/combobox@~1.45.0 # Combobox (sygnal/ui/combobox)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Without them, `sygnal/vite` stops with [SYG666](https://sygnal.js.org/reference/errors/#syg666), which names the missing packages.
|
|
48
|
+
|
|
49
|
+
## Props
|
|
50
|
+
|
|
51
|
+
| Prop | | |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `label` | (required) | The trigger's text: its accessible name |
|
|
54
|
+
| `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 |
|
|
55
|
+
| `items` | (required) | Strings, or `{ value, label, disabled }` objects; `{ separator: true }` draws a separator |
|
|
56
|
+
| `open` | | Controlled open state; follow the `open-change` event |
|
|
57
|
+
| `defaultOpen` | `false` | |
|
|
58
|
+
| `closeOnSelect` | `true` | `false` keeps the menu open after a pick |
|
|
59
|
+
| `loopFocus` | `false` | The arrow keys wrap from the last item to the first |
|
|
60
|
+
| `typeahead` | `true` | Typing a letter moves to the next item starting with it |
|
|
61
|
+
| `positioning` | | Zag's positioning options: `{ placement: 'bottom-end', gutter: 4 }` |
|
|
62
|
+
| `dir` | `'ltr'` | |
|
|
63
|
+
|
|
64
|
+
`className`, `id`, `style` and `aria-*` / `data-*` props go on the host `<div>`.
|
|
65
|
+
|
|
66
|
+
## Events and commands
|
|
67
|
+
|
|
68
|
+
| Event | Detail |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `select` | The value of the item picked |
|
|
71
|
+
| `open-change` | `true` when it opens, `false` when it closes |
|
|
72
|
+
|
|
73
|
+
Open or close it from the model with an [element command](../element-commands.md) on its class: `OPEN_ACTIONS: { ELEMENT: { open: '.card-actions' } }`, or `{ close: '.card-actions' }`.
|
|
74
|
+
|
|
75
|
+
## Keyboard
|
|
76
|
+
|
|
77
|
+
| Key | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| Enter, Space, Down | On the trigger: open the menu on the first item (Up: on the last) |
|
|
80
|
+
| Down / Up | The next / previous item; disabled items are skipped |
|
|
81
|
+
| Home / End | The first / last item |
|
|
82
|
+
| A letter | The next item that starts with it |
|
|
83
|
+
| Enter, Space | Pick the highlighted item and close |
|
|
84
|
+
| Escape, Tab | Close; Escape returns the focus to the trigger |
|
|
85
|
+
|
|
86
|
+
A click outside closes it too.
|
|
87
|
+
|
|
88
|
+
## Styling
|
|
89
|
+
|
|
90
|
+
The parts carry Zag's data attributes: `[data-scope="menu"]` with `[data-part="trigger"]`, `"positioner"`, `"content"`, `"item"` and `"separator"`; the content and trigger have `data-state="open" | "closed"`, an item `data-highlighted` and `data-disabled`. The content is `hidden` while closed, and the positioner places it with `position` and CSS variables (`--x`, `--y`).
|
|
91
|
+
|
|
92
|
+
```css live
|
|
93
|
+
.card-actions [data-part='content'] { background: Canvas; border: 1px solid #ddd; border-radius: 6px; padding: 4px; }
|
|
94
|
+
.card-actions [data-part='item'] { padding: 4px 8px; border-radius: 4px; cursor: default; }
|
|
95
|
+
.card-actions [data-part='item'][data-highlighted] { background: rgb(99 102 241 / 0.25); }
|
|
96
|
+
.card-actions [data-part='item'][data-disabled] { opacity: 0.5; }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Positioning
|
|
100
|
+
|
|
101
|
+
The menu 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 menu 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' }}`.
|
|
102
|
+
|
|
103
|
+
## Testing
|
|
104
|
+
|
|
105
|
+
In the default mock DOM the menu renders its host only; `t.widget('.card-actions')` gives its props and dispatches its events:
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
import { it, expect } from 'vitest'
|
|
109
|
+
import { renderComponent } from 'sygnal'
|
|
110
|
+
import Card from './Card.jsx'
|
|
111
|
+
|
|
112
|
+
it('records the picked action', async () => {
|
|
113
|
+
const t = renderComponent(Card)
|
|
114
|
+
await t.ready()
|
|
115
|
+
t.widget('.card-actions').dispatch('select', 'rename')
|
|
116
|
+
await t.next((state) => state.last === 'rename')
|
|
117
|
+
t.dispose()
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To test the keyboard and focus, render it with `dom: 'real'` in jsdom (`renderComponent` adds the `ResizeObserver`, `CSS.escape` and `Element.prototype.scrollTo` that Zag needs and jsdom lacks, for the test's lifetime) or in a real browser. Sygnal's browser suite runs all three Zag parts in Chromium, Firefox and WebKit.
|
|
122
|
+
|
|
123
|
+
## Size
|
|
124
|
+
|
|
125
|
+
Gzipped, in a small app: Menu adds **33 KB** (Zag's menu machine, its positioning and `@zag-js/vanilla` make up almost all of it; `fromZag` is about 2 KB). Menu, Select and Combobox together add 47 KB, because they share most of Zag. For a simple list of links or buttons, a [Popover](./popover.md) costs a fraction of that.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# UI Parts
|
|
3
|
+
|
|
4
|
+
`sygnal/ui` is a set of headless UI parts. Each part wires up what the browser already does well (`<dialog>`, the Popover API, CSS anchor positioning) and adds the state, keyboard handling and ARIA attributes it leaves out. The parts are unstyled: you write the markup and the CSS, and they give you class hooks and data attributes to style.
|
|
5
|
+
|
|
6
|
+
```jsx
|
|
7
|
+
import { dialog, tabs, tabsAttrs, Toaster } from 'sygnal/ui'
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
| Part | What it is | Built on |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| [Dialog](./dialog.md) | A behavior for `uses` | `<dialog>`, `showModal()` |
|
|
13
|
+
| [Popover](./popover.md) | A behavior for `uses` | The Popover API (`popover`, `popovertarget`) |
|
|
14
|
+
| [Tooltip](./tooltip.md) | A behavior for `uses` | A manual popover placed by CSS anchor positioning, with show and hide delays |
|
|
15
|
+
| [Tabs](./tabs.md) | A behavior + `tabsAttrs` | Your buttons and panels; roving focus |
|
|
16
|
+
| [Accordion](./accordion.md) | A behavior + `accordionAttrs` | Your buttons and panels |
|
|
17
|
+
| [Disclosure](./disclosure.md) | A behavior + `disclosureAttrs` | Your button and panel |
|
|
18
|
+
| [Toaster](./toaster.md) | A component | `popover="manual"`, Collection, Transition, timers |
|
|
19
|
+
| [Menu](./menu.md) | A widget tag, from `sygnal/ui/menu` | Zag.js's menu machine ([`fromZag`](../adapters.md#zag-machines-fromzag)) |
|
|
20
|
+
| [Select](./select.md) | A widget tag, from `sygnal/ui/select` | Zag.js's select machine |
|
|
21
|
+
| [Combobox](./combobox.md) | A widget tag, from `sygnal/ui/combobox` | Zag.js's combobox machine |
|
|
22
|
+
|
|
23
|
+
Menu, Select and Combobox need keyboard and focus handling that the browser doesn't give: they run [Zag.js](https://zagjs.com) state machines, each in a subpath of its own (`sygnal/ui/menu`, `sygnal/ui/select`, `sygnal/ui/combobox`). Zag is an optional peer dependency: a part needs `@zag-js/vanilla` and its own machine (`npm install @zag-js/vanilla@~1.45.0 @zag-js/menu@~1.45.0` for Menu), and `sygnal/ui` never needs it. They are [widget](../widgets.md) tags: `<Menu className="actions" label="Actions" items={ITEMS} />`, read with `DOM.select('.actions').events('select').detail()`.
|
|
24
|
+
|
|
25
|
+
## How the parts work
|
|
26
|
+
|
|
27
|
+
Six parts are [behaviors](../behaviors.md): you list them in a component's `uses` under a state key, and they add a slice of state and namespaced actions to it. The component renders its own markup with the classes the behavior's options name:
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
import { dialog } from 'sygnal/ui'
|
|
31
|
+
|
|
32
|
+
function Settings({ state, uid }) {
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<button className="open-settings">Settings</button>
|
|
36
|
+
<dialog className="settings" aria-labelledby={uid('title')}>
|
|
37
|
+
<h2 id={uid('title')}>Settings</h2>
|
|
38
|
+
<button className="close-settings">Close</button>
|
|
39
|
+
</dialog>
|
|
40
|
+
<p>{state.settings.open ? 'Editing settings' : ''}</p>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Settings.uses = { settings: dialog({ dialog: '.settings', trigger: '.open-settings', close: '.close-settings' }) }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`state.settings` is `{ open, returnValue }`, and the actions are `settings.OPEN`, `settings.CLOSE` and so on. Your own intent and model can trigger them and react to them, as with any behavior: an intent action `'settings.OPEN': DOM.click('.edit')` adds a trigger, and a model entry `'settings.CLOSED': (state) => …` runs after the behavior's.
|
|
49
|
+
|
|
50
|
+
Tabs, Accordion and Disclosure have no markup of their own either. A helper gives you the attributes to spread on your elements, computed from the slice: roles, ARIA states, ids that link the buttons and panels, and the roving `tabindex`. It takes the view's `uid`, so each instance of the component gets its own ids:
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
const a = tabsAttrs(state.tabs, uid)
|
|
54
|
+
// <div {...a.list} aria-label="Settings"> <button className="tab" {...a.tab('general')}>General</button> …
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The Toaster renders its own markup, so it is a component: render `<Toaster />` once and send `event('TOAST', { text })` from any component.
|
|
58
|
+
|
|
59
|
+
## Styling
|
|
60
|
+
|
|
61
|
+
Style the parts with your own classes, the ones you pass in the options. The parts add these data attributes:
|
|
62
|
+
|
|
63
|
+
| Attribute | On | Values |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `data-state` | tabs and tab panels | `active`, `inactive` |
|
|
66
|
+
| `data-state` | accordion and disclosure buttons and panels | `open`, `closed` |
|
|
67
|
+
| `data-value` | tabs, accordion buttons | the value you passed |
|
|
68
|
+
| `data-kind` | each toast | `info`, `success`, `warning`, `error` |
|
|
69
|
+
| `data-paused` | the toast region | present while paused |
|
|
70
|
+
|
|
71
|
+
The browser also exposes the open state: `dialog[open]`, `:modal`, `:popover-open`, `dialog::backdrop` and `[popover]::backdrop`.
|
|
72
|
+
|
|
73
|
+
## Browser support
|
|
74
|
+
|
|
75
|
+
The parts target current evergreen browsers: Chromium (Chrome, Edge), Firefox and Safari. They rely on `<dialog>`, the Popover API (including `popovertarget` and `toggle` events), the `:modal` pseudo-class and, for the Tooltip, CSS anchor positioning (`anchor-name`, `position-anchor`, `position-area`). Sygnal's browser suite runs every part in Chromium, Firefox and WebKit.
|
|
76
|
+
|
|
77
|
+
The package has no JavaScript positioning fallback. To support a browser without CSS anchor positioning, position the tooltip with [Floating UI](#floating-ui-for-older-browsers) instead.
|
|
78
|
+
|
|
79
|
+
### Floating UI for older browsers
|
|
80
|
+
|
|
81
|
+
[Floating UI](https://floating-ui.com/) computes a position in JavaScript. Give the tip a [ref](https://sygnal.js.org/advanced/refs/) and run its `autoUpdate` while the tooltip is open, from an `EFFECT` on the behavior's `TOGGLED` action:
|
|
82
|
+
|
|
83
|
+
```jsx live
|
|
84
|
+
import { createRef } from 'sygnal'
|
|
85
|
+
import { tooltip } from 'sygnal/ui'
|
|
86
|
+
import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom'
|
|
87
|
+
|
|
88
|
+
const trigger = createRef()
|
|
89
|
+
const tip = createRef()
|
|
90
|
+
let stop = null
|
|
91
|
+
|
|
92
|
+
function Toolbar({ uid }) {
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
<button className="save" ref={trigger} aria-describedby={uid('tip')}>Save</button>
|
|
96
|
+
<div className="tip" ref={tip} id={uid('tip')} role="tooltip" popover="manual">Save the draft</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Toolbar.uses = { tip: tooltip({ trigger: '.save', tip: '.tip' }) }
|
|
102
|
+
Toolbar.model = {
|
|
103
|
+
'tip.TOGGLED': {
|
|
104
|
+
EFFECT: (state, open) => {
|
|
105
|
+
stop?.()
|
|
106
|
+
stop = open ? autoUpdate(trigger.current, tip.current, () =>
|
|
107
|
+
computePosition(trigger.current, tip.current, { placement: 'top', strategy: 'fixed', middleware: [offset(6), flip(), shift()] })
|
|
108
|
+
.then(({ x, y }) => Object.assign(tip.current.style, { left: `${x}px`, top: `${y}px` }))) : null
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```css live
|
|
115
|
+
.tip { position: fixed; margin: 0; inset: auto; }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A module-level ref is shared by every instance; for a component rendered more than once, keep the refs in a `Map` keyed by an id.
|
|
119
|
+
|
|
120
|
+
## Size
|
|
121
|
+
|
|
122
|
+
`sygnal/ui` is a separate entry. An app that doesn't import it ships none of it, and each part is tree-shaken on its own. The first behavior an app uses also brings in `defineBehavior` (about 1 KB gzipped). Measured in a small app, gzipped:
|
|
123
|
+
|
|
124
|
+
| Part | Adds |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Dialog | 0.4 KB + `defineBehavior` |
|
|
127
|
+
| Popover | 0.2 KB + `defineBehavior` |
|
|
128
|
+
| Tooltip | 0.3 KB + `defineBehavior` + `makeTimerDriver` (0.6 KB) |
|
|
129
|
+
| Tabs, Accordion | about 1 KB each + `defineBehavior` |
|
|
130
|
+
| Disclosure | 0.3 KB + `defineBehavior` |
|
|
131
|
+
| Toaster | 1.1 KB + `makeTimerDriver` |
|
|
132
|
+
| Menu (`sygnal/ui/menu`) | 33 KB, Zag included |
|
|
133
|
+
| Select (`sygnal/ui/select`) | 33 KB, Zag included |
|
|
134
|
+
| Combobox (`sygnal/ui/combobox`) | 34 KB, Zag included |
|
|
135
|
+
| Menu + Select + Combobox | 47 KB (they share Zag's runtime and positioning) |
|
|
136
|
+
|
|
137
|
+
## Testing
|
|
138
|
+
|
|
139
|
+
`renderComponent()` tests the parts like any component. The default mock DOM records `ELEMENT` commands (`showModal`, `focus`) without running them, so a test simulates the browser's events (`close`, `toggle`) itself. The tooltip delays and toast timeouts run on [fake timers](https://sygnal.js.org/integration/testing/). Keyboard navigation in Tabs and Accordion reads the order of the elements on the page, so test it with `dom: 'real'`.
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
// @vitest-environment jsdom
|
|
143
|
+
import { it, expect } from 'vitest'
|
|
144
|
+
import { within } from '@testing-library/dom'
|
|
145
|
+
import userEvent from '@testing-library/user-event'
|
|
146
|
+
import { renderComponent } from 'sygnal'
|
|
147
|
+
import Settings from './Settings.jsx'
|
|
148
|
+
|
|
149
|
+
it('moves between the tabs with the arrow keys', async () => {
|
|
150
|
+
const t = renderComponent(Settings, { dom: 'real' })
|
|
151
|
+
await t.ready()
|
|
152
|
+
const screen = within(t.container)
|
|
153
|
+
const user = userEvent.setup()
|
|
154
|
+
|
|
155
|
+
await user.click(screen.getByRole('tab', { name: 'General' }))
|
|
156
|
+
await user.keyboard('{ArrowRight}')
|
|
157
|
+
await t.waitForState((s) => s.tabs.selected === 'privacy')
|
|
158
|
+
|
|
159
|
+
expect(screen.getByRole('tab', { name: 'Privacy', selected: true })).toBeTruthy()
|
|
160
|
+
expect(screen.getByRole('tabpanel', { name: 'Privacy' })).toBeTruthy()
|
|
161
|
+
expect(t.actions.map((a) => a.type)).toContain('tabs.MOVE')
|
|
162
|
+
t.dispose()
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The [browser suite](https://github.com/tpresley/sygnal/tree/main/browser-tests) runs the parts in real engines, for what a DOM emulation doesn't have: the top layer, focus trapping, light dismiss and anchor positioning.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!-- Generated from docs/src/content/docs/ui by scripts/copy-guides.mjs; online: https://sygnal.js.org/ui/ -->
|
|
2
|
+
# Popover
|
|
3
|
+
|
|
4
|
+
`popover()` is a [behavior](../behaviors.md) for an element with the HTML `popover` attribute. The browser shows it in the top layer, above everything else, and a button with `popovertarget` opens and closes it with no code at all. The browser also closes it when the user clicks outside or presses Escape (light dismiss), sets `aria-expanded` on the button and returns the focus to it. The behavior keeps `open` in state and lets the model open and close the popover.
|
|
5
|
+
|
|
6
|
+
```jsx live
|
|
7
|
+
import { popover } from 'sygnal/ui'
|
|
8
|
+
|
|
9
|
+
function Filters({ state, uid }) {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<button className="filters-button" popovertarget={uid('filters')}>Filters</button>
|
|
13
|
+
<div className="filters" id={uid('filters')} popover="auto" aria-label="Filters">
|
|
14
|
+
<label><input type="checkbox" className="only-open" checked={state.onlyOpen} /> Only open tasks</label>
|
|
15
|
+
<button className="filters-done">Done</button>
|
|
16
|
+
</div>
|
|
17
|
+
<p>{state.filters.open ? 'Choosing filters…' : ''}</p>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Filters.initialState = { onlyOpen: false }
|
|
23
|
+
Filters.uses = { filters: popover({ popover: '.filters', close: '.filters-done' }) }
|
|
24
|
+
Filters.intent = ({ DOM }) => ({ ONLY_OPEN: DOM.change('.only-open').checked() })
|
|
25
|
+
Filters.model = { ONLY_OPEN: (state, onlyOpen) => ({ ...state, onlyOpen }) }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`popovertarget` takes the popover's `id`; `uid()` keeps it unique when the component is rendered more than once.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Option | | |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `popover` | (required) | The popover element: a selector or a [control](https://sygnal.js.org/guide/controls/) |
|
|
35
|
+
| `close` | | A button inside: its clicks close the popover |
|
|
36
|
+
|
|
37
|
+
There is no `trigger` option: open it with a `popovertarget` button. A button that toggled an `auto` popover through the model would open it again right after closing it, because the press outside the popover light-dismisses it first; the browser exempts `popovertarget` buttons from that.
|
|
38
|
+
|
|
39
|
+
## State and actions
|
|
40
|
+
|
|
41
|
+
`state.filters` is `{ open }`. It follows the popover's `toggle` event, whatever opened or closed it.
|
|
42
|
+
|
|
43
|
+
| Action | Data | |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `filters.OPEN` | | Shows the popover (`showPopover`) |
|
|
46
|
+
| `filters.CLOSE` | | Hides it (`hidePopover`) |
|
|
47
|
+
| `filters.TOGGLE` | | Toggles it (`togglePopover`) |
|
|
48
|
+
| `filters.TOGGLED` | `true` / `false` | The `toggle` event: sets `open` |
|
|
49
|
+
| `filters.SYNC` | `false` | The popover left the page while open (a page change, a conditional render): sets `open: false`, as no `toggle` event comes |
|
|
50
|
+
|
|
51
|
+
`OPEN` and `CLOSE` check the popover itself (`:popover-open`), not `state.filters.open`, which follows the `toggle` event a moment later: `OPEN` then `CLOSE` in one go leaves it closed, and `showPopover()` is never called on an open popover.
|
|
52
|
+
|
|
53
|
+
Open it from somewhere else, for example a keyboard shortcut, with an intent action of the same name: `'filters.OPEN': DOM.keydown('document').filter((e) => e.key === 'f' && e.altKey)`.
|
|
54
|
+
|
|
55
|
+
## `auto` or `manual`
|
|
56
|
+
|
|
57
|
+
- `popover="auto"` (or just `popover`): light dismiss, and opening one closes the other `auto` popovers that aren't its ancestors. Menus, pickers and filter panels.
|
|
58
|
+
- `popover="manual"`: only your code and its `popovertarget` button close it. Use it for something that stays open, such as a [tooltip](./tooltip.md) or [toasts](./toaster.md).
|
|
59
|
+
|
|
60
|
+
A popover inside a modal [dialog](./dialog.md) works: it opens above the dialog, the first Escape closes the popover and the second the dialog.
|
|
61
|
+
|
|
62
|
+
## Accessibility
|
|
63
|
+
|
|
64
|
+
- Name the popover (`aria-label` or `aria-labelledby`) when it is a region of controls, as above.
|
|
65
|
+
- `popovertarget` gives the button `aria-expanded` and links it to the popover for assistive technology. The browser returns the focus to the button when the popover closes with the focus inside it.
|
|
66
|
+
- For a menu with arrow-key navigation, use a menu part, not a bare popover.
|
|
67
|
+
|
|
68
|
+
## Styling
|
|
69
|
+
|
|
70
|
+
Browsers center a popover in the viewport by default. To place it next to its button, use [CSS anchor positioning](./tooltip.md#positioning):
|
|
71
|
+
|
|
72
|
+
```css live
|
|
73
|
+
.filters-button { anchor-name: --filters; }
|
|
74
|
+
.filters { position-anchor: --filters; position-area: bottom span-right; margin: 4px 0 0; inset: auto; }
|
|
75
|
+
.filters:popover-open { animation: fade-in 120ms ease-out; }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a component rendered more than once, give each instance its own anchor name, from `uid()` in a `style` prop, as the tooltip does.
|
|
79
|
+
|
|
80
|
+
## Testing
|
|
81
|
+
|
|
82
|
+
The mock DOM doesn't open popovers, so simulate the `toggle` event as the browser sends it:
|
|
83
|
+
|
|
84
|
+
```jsx
|
|
85
|
+
import { it, expect } from 'vitest'
|
|
86
|
+
import { renderComponent } from 'sygnal'
|
|
87
|
+
import Filters from './Filters.jsx'
|
|
88
|
+
|
|
89
|
+
it('follows the popover and closes it from the Done button', async () => {
|
|
90
|
+
const t = renderComponent(Filters)
|
|
91
|
+
await t.ready()
|
|
92
|
+
t.simulateEvent('.filters', 'toggle', { newState: 'open', oldState: 'closed' })
|
|
93
|
+
await t.next((s) => s.filters.open)
|
|
94
|
+
|
|
95
|
+
t.simulateEvent('.filters-done', 'click')
|
|
96
|
+
await t.settle()
|
|
97
|
+
expect(t.commands('ELEMENT')).toEqual([{ hidePopover: '.filters' }])
|
|
98
|
+
expect(t.actions.map((a) => a.type)).toEqual(['INITIALIZE', 'filters.TOGGLED', 'filters.CLOSE'])
|
|
99
|
+
t.dispose()
|
|
100
|
+
})
|
|
101
|
+
```
|