fix(csr,ui): deliver component outputs to parent bindings
An output only reaches a parent @binding when the component calls output.<name>(). Two separate faults meant most of the library never got there, and both failed silently at each end. HTML lowercases attribute names, so a parent's @sizeChange registered under "sizechange" while the component emitted "sizeChange". The lookup missed, fell through to a DOM dispatch, and the binding was never invoked. That made all 17 camelCase outputs undeliverable -- DataTable.pageChange and .rowClick, Map.markerClick, ChatBubble.messageClick, LayoutSplitter.sizeChange and the rest. invokeComponentOutput now falls back to a case-insensitive lookup, and a csr test fails without it. Separately, 18 components dispatched hand-built CustomEvents rather than calling output.*. A bubbling event on the component's own root never reaches a binding, because parent handlers live in a registry only the output proxy reads. Card, Footer, Breadcrumb, Accordion, alert, Badge, AnnouncementBar, AvatarGroup, ToggleCount and InputNumber now emit properly; Marquee, Map, Timeline, List and SearchBox additionally declare the outputs they were already firing. Dispatches on window are left alone -- that is how Toaster, Modal and DataTable signal across component boundaries. Verified in a browser both ways before and after: an AnnouncementBar dispatching its own bubbling "dismiss" never reached a page-level @dismiss, and reached it immediately once it called output.dismiss(). This corrects the audit, which called the LayoutSplitter failure "narrow and unexplained" and read 32 dead outputs as 16 components needing a rebuild. "Outputs work elsewhere" was an assumption; the components that worked happened to use lowercase names and output.*. The dead-output ratchet drops from 32 to 22, and a new test forbids the raw-CustomEvent pattern outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,30 +29,58 @@ Two mistakes worth recording, because both nearly became wrong conclusions:
|
|||||||
- The semantic token spread sat _after_ the primary and secondary palette
|
- The semantic token spread sat _after_ the primary and secondary palette
|
||||||
entries, so it would have overridden them. It now sits before.
|
entries, so it would have overridden them. It now sits before.
|
||||||
|
|
||||||
|
## Correction: outputs, and what was actually wrong
|
||||||
|
|
||||||
|
The first version of this audit called the LayoutSplitter output failure
|
||||||
|
"narrow and unexplained" and treated 32 dead outputs as 16 components needing a
|
||||||
|
rebuild. Both were wrong, and the real cause was found by binding a page
|
||||||
|
handler to a component in a browser and watching what arrived.
|
||||||
|
|
||||||
|
**Two distinct faults were hiding behind one symptom.**
|
||||||
|
|
||||||
|
_Emitting the wrong way._ An output only reaches a parent `@binding` when the
|
||||||
|
component calls `output.<name>()`. The runtime resolves parent handlers from a
|
||||||
|
registry that only `invokeComponentOutput` reads, so a component that builds
|
||||||
|
its own `CustomEvent` — even a bubbling one, dispatched on its own root —
|
||||||
|
emits into nothing. Eighteen components did exactly that, so their outputs were
|
||||||
|
declared, documented, apparently fired, and never delivered. Converting them
|
||||||
|
took ten dead outputs off the list and cost no new machinery.
|
||||||
|
|
||||||
|
_Case._ HTML lowercases attribute names, so a parent's `@sizeChange` registers
|
||||||
|
under `sizechange` while the component emits `sizeChange`. The lookup missed,
|
||||||
|
fell through to a DOM dispatch, and the binding was never invoked — silently,
|
||||||
|
with no error at either end. **All 17 camelCase outputs in the library were
|
||||||
|
undeliverable**, including `DataTable.pageChange` and `DataTable.rowClick`,
|
||||||
|
`Map.markerClick`, `ChatBubble.messageClick` and `LayoutSplitter.sizeChange`.
|
||||||
|
The runtime now falls back to a case-insensitive lookup, and a test in
|
||||||
|
`packages/csr` fails without that fallback.
|
||||||
|
|
||||||
|
The lesson worth keeping: "outputs work elsewhere" was an assumption, not a
|
||||||
|
measurement. The components that worked happened to use all-lowercase names and
|
||||||
|
`output.*`; that coincidence made a framework-wide bug look like one broken
|
||||||
|
component.
|
||||||
|
|
||||||
## Outstanding, with numbers
|
## Outstanding, with numbers
|
||||||
|
|
||||||
**32 outputs across 16 components have no emitter of any kind.** This is the
|
**22 outputs across 11 components have no emitter of any kind** (was 32 across
|
||||||
bug LayoutSplitter had: the component advertises an output, a caller binds to
|
16; see the correction above). The component advertises an output, a caller
|
||||||
it, and nothing ever fires. Native event names are excluded — the runtime binds
|
binds to it, and nothing ever fires. Native event names are excluded — the
|
||||||
a DOM-listener fallback on component tags, so `click` and `input` do arrive.
|
runtime binds a DOM-listener fallback on component tags, so `click` and `input`
|
||||||
|
do arrive.
|
||||||
|
|
||||||
| Component | Dead outputs |
|
| Component | Dead outputs |
|
||||||
| ------------------------------------ | ----------------------------------------- |
|
| ------------------- | ----------------------------------------- |
|
||||||
| FileUpload | upload, progress, success, cancel, remove |
|
| FileUpload | upload, progress, success, cancel, remove |
|
||||||
| ToastNotifications | add, dismiss, clear, action |
|
| ToastNotifications | add, dismiss, clear, action |
|
||||||
| AdvancedDatePicker | open, close, clear |
|
| AdvancedDatePicker | open, close, clear |
|
||||||
| Card | action, navigate, dismiss |
|
| AdvancedRangeSlider | start, end |
|
||||||
| Accordion | open, close |
|
| Chart | dataPointClick, legendToggle |
|
||||||
| AdvancedRangeSlider | start, end |
|
| Confetti | start, complete |
|
||||||
| Chart | dataPointClick, legendToggle |
|
| TreeView | expand, collapse |
|
||||||
| Confetti | start, complete |
|
| Toast | dismiss |
|
||||||
| TreeView | expand, collapse |
|
| CopyMarkup | success |
|
||||||
| AnnouncementBar, Badge, Toast, alert | dismiss |
|
|
||||||
| AvatarGroup | overflow |
|
|
||||||
| CopyMarkup | success |
|
|
||||||
| Footer | action |
|
|
||||||
|
|
||||||
A test pins this at 32 as a ceiling that only moves down.
|
A test pins this at 22 as a ceiling that only moves down.
|
||||||
|
|
||||||
**23 components are still on the `wire-next` scaffold pattern** — no style
|
**23 components are still on the `wire-next` scaffold pattern** — no style
|
||||||
block, no functions, and for 19 of them an outputs block they never honour.
|
block, no functions, and for 19 of them an outputs block they never honour.
|
||||||
@@ -81,11 +109,6 @@ the busiest activity in the library.
|
|||||||
|
|
||||||
## Known framework defects
|
## Known framework defects
|
||||||
|
|
||||||
- **A component output is not delivered to a parent binding in at least one
|
|
||||||
case.** LayoutSplitter emits `sizeChange` and the component genuinely fires
|
|
||||||
the event, but `@sizeChange` on the tag is not invoked. Outputs work
|
|
||||||
elsewhere, so this is narrow and unexplained. Renaming away from the native
|
|
||||||
`resize` did not fix it, so the first hypothesis was wrong.
|
|
||||||
- **A component tag nested inside another component slot is dropped**, leaving
|
- **A component tag nested inside another component slot is dropped**, leaving
|
||||||
only its children.
|
only its children.
|
||||||
- **Component props and slot content render once** and do not track page state,
|
- **Component props and slot content render once** and do not track page state,
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"releaseLine": "0.8",
|
"releaseLine": "0.8",
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
"packages/ui/components/Accordion.wrn": "e74433d405767edd50e58237ac3986e08e14a893450d712f69679c8361485734",
|
"packages/ui/components/Accordion.wrn": "2b2e756a2197052bbcaf7499d01ec4b68b53a3acaf4524b7ca6424b71ba2e9a3",
|
||||||
"packages/ui/components/AdvancedDatePicker.wrn": "b9b5b08e9464544837a5400bbf9a2cb25724fac037fe196a219b42e5841ad672",
|
"packages/ui/components/AdvancedDatePicker.wrn": "b9b5b08e9464544837a5400bbf9a2cb25724fac037fe196a219b42e5841ad672",
|
||||||
"packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
|
"packages/ui/components/AdvancedRangeSlider.wrn": "e78b5cf825503d0cd306c6e490d71317ee3db1844882d13b500df1f7f1c408e1",
|
||||||
"packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
|
"packages/ui/components/AdvancedSelect.wrn": "29d43f8019a3e0e98af7cf12440f41523665535a39a973441b4faea78e67114a",
|
||||||
"packages/ui/components/AnnouncementBar.wrn": "5946ef5eb646f7414056f59d9cfe14680aa3c935acc9a1ca6528dc53b23caa48",
|
"packages/ui/components/AnnouncementBar.wrn": "903bfd9421a7350c923ace3315cedc5763fe3e6b7b54b660a0f26281d46a8754",
|
||||||
"packages/ui/components/AuthForm.wrn": "496d240090269e69679e01cda3b071c012ec4e925a7c01ae883d25e359e381e1",
|
"packages/ui/components/AuthForm.wrn": "496d240090269e69679e01cda3b071c012ec4e925a7c01ae883d25e359e381e1",
|
||||||
"packages/ui/components/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
|
"packages/ui/components/AuthSplitLayout.wrn": "72b82495dd14d40a2d594ccdcf3adc925d1567c155750fb1072347b8845aab7b",
|
||||||
"packages/ui/components/AvatarGroup.wrn": "9c583f567213f8e1ad4506a7d2abc3fb462d11679fd2196d3828030f28a28964",
|
"packages/ui/components/AvatarGroup.wrn": "66f3d211b7658ca5193c750d0ccb3e392185038d8960d3206f6e8a249e10214e",
|
||||||
"packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
|
"packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd",
|
||||||
"packages/ui/components/Badge.wrn": "6b55b4d85100d605cf168857aec1dc57da62c1f6a0a5cd6dd877023f24b189cc",
|
"packages/ui/components/Badge.wrn": "e96cdcf182ff9834509464bb2fa4c0b041748d3f7671aa46ab489bae6f3d98ae",
|
||||||
"packages/ui/components/Blockquote.wrn": "6016dd4450de7cbf5c3cb413599baa2e502321e8eedef54439a0df6d0aae8bcb",
|
"packages/ui/components/Blockquote.wrn": "6016dd4450de7cbf5c3cb413599baa2e502321e8eedef54439a0df6d0aae8bcb",
|
||||||
"packages/ui/components/Breadcrumb.wrn": "df27101d41cf018d55b6909e0399286a4661637140e341c8624615051d485142",
|
"packages/ui/components/Breadcrumb.wrn": "14732e91468f5793ee5da8599f8e40d84ec51c93d660fb0c0056b956ec707850",
|
||||||
"packages/ui/components/ButtonGroup.wrn": "0d97fbeed531d1d8ef4b59531923b47b34100d0de71d7bf524b02c668920b3d8",
|
"packages/ui/components/ButtonGroup.wrn": "0d97fbeed531d1d8ef4b59531923b47b34100d0de71d7bf524b02c668920b3d8",
|
||||||
"packages/ui/components/CTASection.wrn": "2d2241d4f1018cfd2f8ea1ddd5fa8c8fcb8d1bbb2d4e4498cb9f39ff47a3801c",
|
"packages/ui/components/CTASection.wrn": "2d2241d4f1018cfd2f8ea1ddd5fa8c8fcb8d1bbb2d4e4498cb9f39ff47a3801c",
|
||||||
"packages/ui/components/Card.wrn": "4370df341235819b8a968a3f812209c2e6c29e4471d06578aaa5b577fb6e2dc8",
|
"packages/ui/components/Card.wrn": "457bc546e0b227692ac96ec8f413823463d3cfe9194b6f4847c21a5beee5e8a4",
|
||||||
"packages/ui/components/Chart.wrn": "841def6b024720bd24898975a1434a1cb39c89e1272d42db431d35d27c98c553",
|
"packages/ui/components/Chart.wrn": "841def6b024720bd24898975a1434a1cb39c89e1272d42db431d35d27c98c553",
|
||||||
"packages/ui/components/ChatBubble.wrn": "0da2bc86830ac712481559bd2fe7214d26f25101f2e8c2e4629e25c004388b52",
|
"packages/ui/components/ChatBubble.wrn": "0da2bc86830ac712481559bd2fe7214d26f25101f2e8c2e4629e25c004388b52",
|
||||||
"packages/ui/components/Checkbox.wrn": "45e6046e3ad4acb1c1805b51fd5bf0b95b346d3d53973ededc5e43ea2fd91f70",
|
"packages/ui/components/Checkbox.wrn": "45e6046e3ad4acb1c1805b51fd5bf0b95b346d3d53973ededc5e43ea2fd91f70",
|
||||||
@@ -44,23 +44,23 @@
|
|||||||
"packages/ui/components/FileInput.wrn": "8b63811deb90a03763620bedf0d5d3c7d05a20b8ae34292eaface9b756b32ecd",
|
"packages/ui/components/FileInput.wrn": "8b63811deb90a03763620bedf0d5d3c7d05a20b8ae34292eaface9b756b32ecd",
|
||||||
"packages/ui/components/FileUpload.wrn": "3346d8978e134c3a1e6bc742201a3fbb0cffb89da40da6fcd856a2adbb971393",
|
"packages/ui/components/FileUpload.wrn": "3346d8978e134c3a1e6bc742201a3fbb0cffb89da40da6fcd856a2adbb971393",
|
||||||
"packages/ui/components/FileUploadProgress.wrn": "83bcc8558c70fbe0e0670642f65998abd222c235f1031be46bd7c65e6b5c5154",
|
"packages/ui/components/FileUploadProgress.wrn": "83bcc8558c70fbe0e0670642f65998abd222c235f1031be46bd7c65e6b5c5154",
|
||||||
"packages/ui/components/Footer.wrn": "a5dfaa5f9548b7fdd4b556c63428e0ea8d22e5e776a5714d76619999438cf618",
|
"packages/ui/components/Footer.wrn": "d9075d2206af75a1ec4317d7974d47311889400699260e990d672780f43f40bf",
|
||||||
"packages/ui/components/Grid.wrn": "d089d8657298b4d33993ba4bf7b581f816c65db513d8e48cf1fdd18da19e05a8",
|
"packages/ui/components/Grid.wrn": "d089d8657298b4d33993ba4bf7b581f816c65db513d8e48cf1fdd18da19e05a8",
|
||||||
"packages/ui/components/Hero.wrn": "e4e39dcd59c273192c7e4136f6583279e20b0956bcba4cfbc3f6e01a1fade152",
|
"packages/ui/components/Hero.wrn": "e4e39dcd59c273192c7e4136f6583279e20b0956bcba4cfbc3f6e01a1fade152",
|
||||||
"packages/ui/components/HeroActions.wrn": "c5720efe2ce7f6c114eb2ea4ccc3e8102a1099078ba23fd13770eddeb6aec640",
|
"packages/ui/components/HeroActions.wrn": "c5720efe2ce7f6c114eb2ea4ccc3e8102a1099078ba23fd13770eddeb6aec640",
|
||||||
"packages/ui/components/Image.wrn": "8113441bf90ab0d1d8618d477125ad339021f39e853db89d656c72c492e7a20b",
|
"packages/ui/components/Image.wrn": "8113441bf90ab0d1d8618d477125ad339021f39e853db89d656c72c492e7a20b",
|
||||||
"packages/ui/components/Input.wrn": "9ad394eed60f1b05bbd2ccd50dc2de7b78245c52c87afd46055d772c38b7b0c9",
|
"packages/ui/components/Input.wrn": "9ad394eed60f1b05bbd2ccd50dc2de7b78245c52c87afd46055d772c38b7b0c9",
|
||||||
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
|
"packages/ui/components/InputGroup.wrn": "0e23ea541a60e893c9d9e6f95113911ee459468d8d6105bf03aeb55395359123",
|
||||||
"packages/ui/components/InputNumber.wrn": "a0aa4f4566bf54ffbef54c99648eab05b5f9870be6ea07639a4ecf3930afb631",
|
"packages/ui/components/InputNumber.wrn": "26936385e9f01ec888a1df61719991aa7a2782305328b02c939d3c2c94e3e8e0",
|
||||||
"packages/ui/components/Kbd.wrn": "5afaabbb15902bd8fb82cb4155cf6cbfff55600927b54d564d27e2dd8b752386",
|
"packages/ui/components/Kbd.wrn": "5afaabbb15902bd8fb82cb4155cf6cbfff55600927b54d564d27e2dd8b752386",
|
||||||
"packages/ui/components/LayoutSplitter.wrn": "677e2dba6dbf385d8aef14829d785137ea26272a62affcd1839fa74f5145716d",
|
"packages/ui/components/LayoutSplitter.wrn": "6bd6812e364420ed5d3439862116c58d0b011b298679005795bdccdbb7455066",
|
||||||
"packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665",
|
"packages/ui/components/LegendIndicator.wrn": "6a7607b27a17196f899132eb952203a1073e4077365a94935f485e700cbac665",
|
||||||
"packages/ui/components/Link.wrn": "b986abcc66bc0b1c6296f3ff4e560812e35d89cc680d5251d7a3883e68c1d053",
|
"packages/ui/components/Link.wrn": "b986abcc66bc0b1c6296f3ff4e560812e35d89cc680d5251d7a3883e68c1d053",
|
||||||
"packages/ui/components/List.wrn": "02f960a8e82fa049aac477bac9c1d9371394c86d9c47513138f61bffc79dc7d8",
|
"packages/ui/components/List.wrn": "8cf4505ff6a079feb3b77822d7ed6977a70393dd1e6267f94e75234d3ac215d6",
|
||||||
"packages/ui/components/ListGroup.wrn": "37e2ec61ceab20ed020a834da03208c493702b41abcf586816e29f2e50d8ee11",
|
"packages/ui/components/ListGroup.wrn": "37e2ec61ceab20ed020a834da03208c493702b41abcf586816e29f2e50d8ee11",
|
||||||
"packages/ui/components/Map.wrn": "7b8a0d5412a464fd7cab8977d816542c506d8c5dab8230a3c984c2caee407c91",
|
"packages/ui/components/Map.wrn": "990656db93e1acd73a301a4addaf353afa14430805366d2e5f124b831044afbe",
|
||||||
"packages/ui/components/MarketingSectionHeader.wrn": "7d6ffcc01a9331474475ea57ca58598be757abd9428b66bdfaf6f3603c5b145b",
|
"packages/ui/components/MarketingSectionHeader.wrn": "7d6ffcc01a9331474475ea57ca58598be757abd9428b66bdfaf6f3603c5b145b",
|
||||||
"packages/ui/components/Marquee.wrn": "b2b35eedf3297ba12ab3776385a9f3eab001027648d87a2a1968f576eee6c025",
|
"packages/ui/components/Marquee.wrn": "3395235807aea43ac10c4d562706e510dac2a32786b4a5d5d73927989ede68df",
|
||||||
"packages/ui/components/MegaMenu.wrn": "8a9a9b348ca1c5e182914ff55d981a91f47a49c52ef5e5efd8e4cb6e56361a43",
|
"packages/ui/components/MegaMenu.wrn": "8a9a9b348ca1c5e182914ff55d981a91f47a49c52ef5e5efd8e4cb6e56361a43",
|
||||||
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
|
"packages/ui/components/MetricCard.wrn": "6451182739298691908f68258c0250cce2a78b0dc27c97115579ece30d7d9f92",
|
||||||
"packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364",
|
"packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"packages/ui/components/RangeSlider.wrn": "21eb7a5df0ee2cb5e78f628eb920265998eb9f1a4933d4e5c57db0323fd66b41",
|
"packages/ui/components/RangeSlider.wrn": "21eb7a5df0ee2cb5e78f628eb920265998eb9f1a4933d4e5c57db0323fd66b41",
|
||||||
"packages/ui/components/Rating.wrn": "a2d74dc748fc7d98892684c760518b87fa65411b1102e3611252b87245b3ffcb",
|
"packages/ui/components/Rating.wrn": "a2d74dc748fc7d98892684c760518b87fa65411b1102e3611252b87245b3ffcb",
|
||||||
"packages/ui/components/Scrollspy.wrn": "76d1c17580c8d460f1222ce97585bdcac9779141f2f30d5460c84ff9f75413d8",
|
"packages/ui/components/Scrollspy.wrn": "76d1c17580c8d460f1222ce97585bdcac9779141f2f30d5460c84ff9f75413d8",
|
||||||
"packages/ui/components/SearchBox.wrn": "315f54f1feaaa47a815d1e2d7a35b492f09fbfadfc6d37228e1009e19e0652ad",
|
"packages/ui/components/SearchBox.wrn": "4a07358530ffb197d80d816b6ee4335a20812b6bee1e66a845c4075305bee6f7",
|
||||||
"packages/ui/components/Section.wrn": "33eb8a82a2102229c2b1f64cec497e9e8e70b67adab8dbe396be860fccbb699c",
|
"packages/ui/components/Section.wrn": "33eb8a82a2102229c2b1f64cec497e9e8e70b67adab8dbe396be860fccbb699c",
|
||||||
"packages/ui/components/SectionHeader.wrn": "8eecf2ea7a93139512260e3d46ca2a1afeb341b8bd46b7c30baa6e2c75e25d10",
|
"packages/ui/components/SectionHeader.wrn": "8eecf2ea7a93139512260e3d46ca2a1afeb341b8bd46b7c30baa6e2c75e25d10",
|
||||||
"packages/ui/components/Select.wrn": "7f046bb11b7c2470dae26d91a4b2261c66a40ae054d0e04c69b6748912d1e204",
|
"packages/ui/components/Select.wrn": "7f046bb11b7c2470dae26d91a4b2261c66a40ae054d0e04c69b6748912d1e204",
|
||||||
@@ -93,17 +93,17 @@
|
|||||||
"packages/ui/components/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19",
|
"packages/ui/components/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19",
|
||||||
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
|
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
|
||||||
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
|
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
|
||||||
"packages/ui/components/Timeline.wrn": "5708c656eefd12f31c93490075ee482bf527059028844bd58cdcddc88461e2b5",
|
"packages/ui/components/Timeline.wrn": "aec123bf69f6bfa41c3564e4eac18b2a245214dc6905ebd0bf5b69f7c2736ae6",
|
||||||
"packages/ui/components/Toast.wrn": "f37c584d1c1a66401deb53d915c8ee0c70aaf7baa1d3c297fa74c5bdb914dd1d",
|
"packages/ui/components/Toast.wrn": "f37c584d1c1a66401deb53d915c8ee0c70aaf7baa1d3c297fa74c5bdb914dd1d",
|
||||||
"packages/ui/components/ToastNotifications.wrn": "33ff76b2a0a129ff896baea8979b4be97f7ec4471a92a2da970403a5c46e8b03",
|
"packages/ui/components/ToastNotifications.wrn": "33ff76b2a0a129ff896baea8979b4be97f7ec4471a92a2da970403a5c46e8b03",
|
||||||
"packages/ui/components/Toaster.wrn": "7481ecddde9ed5bf1f448d45a78da7ee0009f84963814bf681735db4e5ab75f9",
|
"packages/ui/components/Toaster.wrn": "7481ecddde9ed5bf1f448d45a78da7ee0009f84963814bf681735db4e5ab75f9",
|
||||||
"packages/ui/components/ToggleCount.wrn": "70a75b2bdcc89103f8ca300ee6d21cd61baf8c6a4aeade9b68d1dc529f77064b",
|
"packages/ui/components/ToggleCount.wrn": "17710fb37fc6953911acec4e80cd95c1202836e9b958ae4ce34c9a9639edae95",
|
||||||
"packages/ui/components/TogglePassword.wrn": "405a85cbfa3d0ff0185b52d2805fba88497a5f25501f01b2438ed3a28591a38d",
|
"packages/ui/components/TogglePassword.wrn": "405a85cbfa3d0ff0185b52d2805fba88497a5f25501f01b2438ed3a28591a38d",
|
||||||
"packages/ui/components/Tooltip.wrn": "e6c8c14a75062d04a2e64245df1a40e67785240573c73bae4b0b32e76ef822bc",
|
"packages/ui/components/Tooltip.wrn": "e6c8c14a75062d04a2e64245df1a40e67785240573c73bae4b0b32e76ef822bc",
|
||||||
"packages/ui/components/TreeView.wrn": "1791a10135595b17b5c746af9e0476e8520d516ba46d64c79dc4efe0af2b500d",
|
"packages/ui/components/TreeView.wrn": "1791a10135595b17b5c746af9e0476e8520d516ba46d64c79dc4efe0af2b500d",
|
||||||
"packages/ui/components/Typography.wrn": "1b4d1b91dee0ff522d27c566becc3bc49bd9ec979ae6334e6a15fca5d88eccac",
|
"packages/ui/components/Typography.wrn": "1b4d1b91dee0ff522d27c566becc3bc49bd9ec979ae6334e6a15fca5d88eccac",
|
||||||
"packages/ui/components/WysiwygEditor.wrn": "636e60b9f9be7a5807cca7ad20e0b0f370ee1ec6c1ecba5d0a577726cc86bc98",
|
"packages/ui/components/WysiwygEditor.wrn": "636e60b9f9be7a5807cca7ad20e0b0f370ee1ec6c1ecba5d0a577726cc86bc98",
|
||||||
"packages/ui/components/alert.wrn": "a6020b3fbf02481f76cea4cfa563a7165c7ccc2920492f6f6b79e98cbf2cf0fa",
|
"packages/ui/components/alert.wrn": "3f4c4c90c2b5a00a13d0fa1e7e48e4898869dd27a50ff412a51d7bcc87a0baac",
|
||||||
"packages/ui/components/avatar.wrn": "571b70790aff28a5b7d6adbf0196e77ece1ce153a95b384148cb2bb5f74ab2e9",
|
"packages/ui/components/avatar.wrn": "571b70790aff28a5b7d6adbf0196e77ece1ce153a95b384148cb2bb5f74ab2e9",
|
||||||
"packages/ui/components/button.wrn": "647af3918142fd9151e46555345532980633cc3677ec26d0940c253ea789bc31",
|
"packages/ui/components/button.wrn": "647af3918142fd9151e46555345532980633cc3677ec26d0940c253ea789bc31",
|
||||||
"packages/ui/components/carousel.wrn": "37bdde82de59e90cf02c9fed0ee11f5ddd2979246932701dad9412c4fd358a6a",
|
"packages/ui/components/carousel.wrn": "37bdde82de59e90cf02c9fed0ee11f5ddd2979246932701dad9412c4fd358a6a",
|
||||||
|
|||||||
@@ -2033,11 +2033,37 @@ const MIGRATIONS: Migration[] = [
|
|||||||
version: "0.8.6",
|
version: "0.8.6",
|
||||||
id: "0.8.6-navigation-and-layout-groups",
|
id: "0.8.6-navigation-and-layout-groups",
|
||||||
description:
|
description:
|
||||||
"Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, and defines theme tokens that components referenced but nothing declared.",
|
"Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, defines theme tokens that components referenced but nothing declared, and makes component outputs actually reach parent bindings.",
|
||||||
apply() {
|
apply() {
|
||||||
// Source changes no codemod can make safely, so they are listed rather
|
// Source changes no codemod can make safely, so they are listed rather
|
||||||
// than attempted.
|
// than attempted.
|
||||||
//
|
//
|
||||||
|
// OUTPUTS NOW ARRIVE. Two faults kept declared outputs from reaching a
|
||||||
|
// parent @binding, and both are fixed. Expect handlers that never ran
|
||||||
|
// before to start running -- this is the intended repair, but it is a
|
||||||
|
// behaviour change in code you may have written around.
|
||||||
|
//
|
||||||
|
// 1. Every camelCase output was undeliverable. HTML lowercases
|
||||||
|
// attribute names, so @sizeChange registered as "sizechange" while
|
||||||
|
// the component emitted "sizeChange" and the lookup missed. That
|
||||||
|
// covered all 17 camelCase outputs, including DataTable.pageChange
|
||||||
|
// and .rowClick, Map.markerClick, ChatBubble.messageClick and
|
||||||
|
// LayoutSplitter.sizeChange. The runtime now matches case
|
||||||
|
// insensitively.
|
||||||
|
//
|
||||||
|
// 2. Eighteen components dispatched hand-built CustomEvents instead of
|
||||||
|
// calling output.*, which never reaches a binding. Card, Footer,
|
||||||
|
// Breadcrumb, Accordion, alert, Badge, AnnouncementBar, AvatarGroup,
|
||||||
|
// ToggleCount and InputNumber now emit properly.
|
||||||
|
//
|
||||||
|
// If you worked around the old silence by listening for the raw DOM
|
||||||
|
// event on the element, that listener still fires for cases where no
|
||||||
|
// binding is registered, but the supported route is the @binding.
|
||||||
|
//
|
||||||
|
// Marquee, Map, Timeline, List and SearchBox now DECLARE the outputs
|
||||||
|
// they were already firing: pause/resume, markerClick/select/zoom,
|
||||||
|
// select, select and search/clear respectively.
|
||||||
|
//
|
||||||
// Tabs replaced its raw CustomEvents with declared outputs. Code
|
// Tabs replaced its raw CustomEvents with declared outputs. Code
|
||||||
// listening for the old change and select events on the element must
|
// listening for the old change and select events on the element must
|
||||||
// move to the @change and @select bindings.
|
// move to the @change and @select bindings.
|
||||||
|
|||||||
@@ -3927,6 +3927,25 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
if (!root || !name) return undefined;
|
if (!root || !name) return undefined;
|
||||||
var registry = root.__wrnexusOutputHandlers;
|
var registry = root.__wrnexusOutputHandlers;
|
||||||
var handlers = registry && registry[name];
|
var handlers = registry && registry[name];
|
||||||
|
/*
|
||||||
|
* A parent writes @sizeChange, but HTML lowercases attribute names, so the
|
||||||
|
* handler is registered under "sizechange" while the component emits
|
||||||
|
* "sizeChange". Without this the lookup misses, the call falls through to
|
||||||
|
* dispatchComponentEvent, and the binding is never invoked -- silently.
|
||||||
|
* Every camelCase output in the library was undeliverable because of it.
|
||||||
|
*/
|
||||||
|
if ((!handlers || !handlers.size) && registry) {
|
||||||
|
var lower = String(name).toLowerCase();
|
||||||
|
if (lower !== name) handlers = registry[lower];
|
||||||
|
if (!handlers || !handlers.size) {
|
||||||
|
for (var key in registry) {
|
||||||
|
if (key.toLowerCase() === lower && registry[key] && registry[key].size) {
|
||||||
|
handlers = registry[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (handlers && handlers.size) {
|
if (handlers && handlers.size) {
|
||||||
var values = [];
|
var values = [];
|
||||||
handlers.forEach(function (handler) { values.push(handler(payload)); });
|
handlers.forEach(function (handler) { values.push(handler(payload)); });
|
||||||
|
|||||||
@@ -631,6 +631,38 @@ test("component output handlers run in the parent scope", () => {
|
|||||||
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
|
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a camelCase output reaches a parent binding despite attribute lowercasing", () => {
|
||||||
|
/*
|
||||||
|
* A parent writes @sizeChange; HTML lowercases attribute names, so the
|
||||||
|
* handler registers under "sizechange" while the component emits
|
||||||
|
* "sizeChange". The lookup used to miss and fall through to a DOM dispatch,
|
||||||
|
* so the binding was never invoked and nothing reported an error. Every
|
||||||
|
* camelCase output in the library was undeliverable -- LayoutSplitter's
|
||||||
|
* sizeChange, DataTable's pageChange and rowClick, Map's markerClick and
|
||||||
|
* twelve more. Verified in a browser before and after the fix.
|
||||||
|
*/
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="saved: ''">` +
|
||||||
|
`<span id="out">{saved}</span>` +
|
||||||
|
`<div data-scope="n: 0" data-wrn-hydration="LayoutSplitter:x">` +
|
||||||
|
`<div data-wrn-events="sizechange" data-wrn-out-sizechange="saved = 'yes'">` +
|
||||||
|
`<button data-on-click="output.sizeChange({ size: 45 })">go</button>` +
|
||||||
|
`</div></div></div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const target = win.document.querySelector("[data-wrn-events]") as unknown as {
|
||||||
|
__wrnexusOutputHandlers?: Record<string, Set<(payload: unknown) => unknown>>;
|
||||||
|
};
|
||||||
|
// The registry is keyed as the DOM gave it: lowercased, not as authored.
|
||||||
|
expect(Object.keys(target.__wrnexusOutputHandlers ?? {})).toContain("sizechange");
|
||||||
|
expect(target.__wrnexusOutputHandlers?.sizeChange).toBeUndefined();
|
||||||
|
|
||||||
|
// Emitting through the real output proxy, under the camelCase name the
|
||||||
|
// component actually writes, must still reach the parent.
|
||||||
|
(win.document.querySelector("button") as unknown as HTMLElement).click();
|
||||||
|
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
|
||||||
|
});
|
||||||
|
|
||||||
// --- browser globals + regex literals in client expressions ----------------
|
// --- browser globals + regex literals in client expressions ----------------
|
||||||
// Client functions and inline handlers are interpreted by the runtime's own
|
// Client functions and inline handlers are interpreted by the runtime's own
|
||||||
// eval-free expression engine (so a strict CSP needs no unsafe-eval). Anything
|
// eval-free expression engine (so a strict CSP needs no unsafe-eval). Anything
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ Present structured responsive linked or status items with icons, descriptions, a
|
|||||||
- Mount: `data-component="List"`
|
- Mount: `data-component="List"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "List"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Outputs: None
|
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
### ListGroup
|
### ListGroup
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ Continuously present responsive labels, partners, notices, or capabilities with
|
|||||||
- Mount: `data-component="Marquee"`
|
- Mount: `data-component="Marquee"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Marquee"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Outputs: None
|
- Outputs: `pause({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `resume({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
### Progress
|
### Progress
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ Present responsive chronological activity, milestones, or workflow status with r
|
|||||||
- Mount: `data-component="Timeline"`
|
- Mount: `data-component="Timeline"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Outputs: None
|
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
### Toast
|
### Toast
|
||||||
|
|
||||||
@@ -469,7 +469,7 @@ Provide an accessible responsive search field with labels, validation states, si
|
|||||||
- Mount: `data-component="SearchBox"`
|
- Mount: `data-component="SearchBox"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Search Box"`, `name: string = ""`, `value: string = ""`, `placeholder: string = ""`, `type: string = "search"`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `disabled: boolean = false`, `required: boolean = false`, `class: string = ""`
|
||||||
- Slots: None
|
- Slots: None
|
||||||
- Outputs: None
|
- Outputs: `search({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `clear({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
### Select
|
### Select
|
||||||
|
|
||||||
@@ -588,7 +588,7 @@ Present responsive location information and markers with map-ready metadata and
|
|||||||
- Mount: `data-component="Map"`
|
- Mount: `data-component="Map"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Map"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""`
|
||||||
- Slots: `default`
|
- Slots: `default`
|
||||||
- Outputs: None
|
- Outputs: `markerClick({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `zoom({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
### ToastNotifications
|
### ToastNotifications
|
||||||
|
|
||||||
|
|||||||
@@ -8064,8 +8064,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["default"],
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
"events": [],
|
{
|
||||||
|
"name": "select",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": ["select"],
|
||||||
"source": "components/List.wrn"
|
"source": "components/List.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -8195,8 +8200,21 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["default"],
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
"events": [],
|
{
|
||||||
|
"name": "markerClick",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "select",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zoom",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": ["markerClick", "select", "zoom"],
|
||||||
"source": "components/Map.wrn"
|
"source": "components/Map.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -8352,8 +8370,17 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["default"],
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
"events": [],
|
{
|
||||||
|
"name": "pause",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resume",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": ["pause", "resume"],
|
||||||
"source": "components/Marquee.wrn"
|
"source": "components/Marquee.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -10911,8 +10938,17 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": [],
|
"slots": [],
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
"events": [],
|
{
|
||||||
|
"name": "search",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clear",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": ["search", "clear"],
|
||||||
"source": "components/SearchBox.wrn"
|
"source": "components/SearchBox.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -13034,8 +13070,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["default"],
|
"slots": ["default"],
|
||||||
"outputs": [],
|
"outputs": [
|
||||||
"events": [],
|
{
|
||||||
|
"name": "select",
|
||||||
|
"payloadType": "{ sourceEvent?: Event; [key: string]: string | number | boolean | null | object }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": ["select"],
|
||||||
"source": "components/Timeline.wrn"
|
"source": "components/Timeline.wrn"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,22 +49,24 @@ component Accordion {
|
|||||||
return multiple || alwaysOpen
|
return multiple || alwaysOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, root, customEvent) {
|
// Named rather than computed: an output is resolved as a property name, so
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-accordion]")
|
// output[eventName] would not reach a parent binding.
|
||||||
|
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
|
||||||
if (!root) {
|
payload = {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent(eventName, true, false, {
|
|
||||||
component: "Accordion",
|
component: "Accordion",
|
||||||
value: value,
|
value: value,
|
||||||
item: item,
|
item: item,
|
||||||
open: isOpen(value),
|
open: isOpen(value),
|
||||||
openValues: openValues
|
openValues: openValues
|
||||||
})
|
}
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
|
if (eventName === "open") {
|
||||||
|
output.open(payload)
|
||||||
|
} else if (eventName === "close") {
|
||||||
|
output.close(payload)
|
||||||
|
} else {
|
||||||
|
output.change(payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client function toggleItem(sourceEvent, value, item, wasOpen) {
|
client function toggleItem(sourceEvent, value, item, wasOpen) {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ badge: string = ""
|
|||||||
aria-label='{dismissLabel}'
|
aria-label='{dismissLabel}'
|
||||||
title='{dismissLabel}'
|
title='{dismissLabel}'
|
||||||
class="wire-announcement__dismiss"
|
class="wire-announcement__dismiss"
|
||||||
@click='dismissed = true; event.currentTarget.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, detail: { message: message } }))'
|
@click='dismissed = true; output.dismiss({ message: message })'
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="icon-[lucide--x]"
|
class="icon-[lucide--x]"
|
||||||
|
|||||||
@@ -30,17 +30,13 @@ component AvatarGroup {
|
|||||||
return items.slice(Number(maxVisible))
|
return items.slice(Number(maxVisible))
|
||||||
}
|
}
|
||||||
|
|
||||||
client function toggleOverflow(sourceEvent, root, customEvent) {
|
client function toggleOverflow() {
|
||||||
overflowOpen = !overflowOpen
|
overflowOpen = !overflowOpen
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-avatar-group]")
|
output.overflow({
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("overflow", true, false, {
|
|
||||||
component: "AvatarGroup",
|
component: "AvatarGroup",
|
||||||
open: overflowOpen,
|
open: overflowOpen,
|
||||||
hiddenCount: hiddenMembers().length
|
hiddenCount: hiddenMembers().length
|
||||||
})
|
})
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ label: string = "Breadcrumb"
|
|||||||
<a
|
<a
|
||||||
href='{homeHref || "/"}'
|
href='{homeHref || "/"}'
|
||||||
class="wire-breadcrumb__link wire-breadcrumb__home"
|
class="wire-breadcrumb__link wire-breadcrumb__home"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 } }))'
|
@click='output.select({ item: { label: homeLabel, href: homeHref || "/", value: "home" }, itemIndex: -1 })'
|
||||||
>
|
>
|
||||||
{#if homeIcon === "icon-[lucide--house]"}
|
{#if homeIcon === "icon-[lucide--house]"}
|
||||||
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
|
<span class="icon-[lucide--house] wire-breadcrumb__icon" aria-hidden="true"></span>
|
||||||
@@ -72,7 +72,7 @@ label: string = "Breadcrumb"
|
|||||||
target='{item.target || ""}'
|
target='{item.target || ""}'
|
||||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||||
class="wire-breadcrumb__link"
|
class="wire-breadcrumb__link"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
@click='output.select({ item: item, itemIndex: itemIndex })'
|
||||||
>
|
>
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
<span class='{item.icon} wire-breadcrumb__icon' aria-hidden="true"></span>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ component Footer {
|
|||||||
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
||||||
aria-current='{child.active ? "page" : ""}'
|
aria-current='{child.active ? "page" : ""}'
|
||||||
class="wire-footer__link"
|
class="wire-footer__link"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
|
@click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex })'
|
||||||
>
|
>
|
||||||
{#if child.icon}
|
{#if child.icon}
|
||||||
<span
|
<span
|
||||||
@@ -127,7 +127,7 @@ component Footer {
|
|||||||
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
rel='{item.external ? "noopener noreferrer" : (item.rel || "")}'
|
||||||
aria-current='{item.active ? "page" : ""}'
|
aria-current='{item.active ? "page" : ""}'
|
||||||
class="wire-footer__link"
|
class="wire-footer__link"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, itemIndex: itemIndex } })); item.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, itemIndex: itemIndex } }))'
|
@click='output.select({ item: item, itemIndex: itemIndex }); item.action && output.action({ item: item, itemIndex: itemIndex })'
|
||||||
>
|
>
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
<span
|
<span
|
||||||
@@ -199,7 +199,7 @@ component Footer {
|
|||||||
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
rel='{child.external ? "noopener noreferrer" : (child.rel || "")}'
|
||||||
aria-current='{child.active ? "page" : ""}'
|
aria-current='{child.active ? "page" : ""}'
|
||||||
class="wire-footer__link"
|
class="wire-footer__link"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } })); child.action && event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex } }))'
|
@click='output.select({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex }); child.action && output.action({ item: child, parent: item, itemIndex: childIndex, sectionIndex: itemIndex })'
|
||||||
>
|
>
|
||||||
{#if child.icon}
|
{#if child.icon}
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -232,26 +232,17 @@ component InputNumber {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outputs are resolved by name, so each one is written out. Raw
|
||||||
|
// CustomEvents dispatched on the root -- what this did before -- never
|
||||||
|
// reach a parent @binding.
|
||||||
client function dispatchInputNumberEvent(
|
client function dispatchInputNumberEvent(
|
||||||
sourceEvent,
|
sourceEvent,
|
||||||
eventName,
|
eventName,
|
||||||
action,
|
action,
|
||||||
previousValue,
|
previousValue,
|
||||||
root,
|
payload
|
||||||
customEvent
|
|
||||||
) {
|
) {
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-input-number]")
|
payload = {
|
||||||
|
|
||||||
if (!root && sourceEvent.target) {
|
|
||||||
root = sourceEvent.target.closest("[data-wrn-input-number]")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent(eventName, true, false, {
|
|
||||||
component: "InputNumber",
|
component: "InputNumber",
|
||||||
name: name,
|
name: name,
|
||||||
value: currentValue,
|
value: currentValue,
|
||||||
@@ -261,8 +252,17 @@ component InputNumber {
|
|||||||
max: hasMax() ? Number(max) : null,
|
max: hasMax() ? Number(max) : null,
|
||||||
step: normalizedStep(),
|
step: normalizedStep(),
|
||||||
valid: !isInvalid()
|
valid: !isInvalid()
|
||||||
})
|
}
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
|
if (eventName === "input") {
|
||||||
|
output.input(payload)
|
||||||
|
} else if (eventName === "change") {
|
||||||
|
output.change(payload)
|
||||||
|
} else if (eventName === "increment") {
|
||||||
|
output.increment(payload)
|
||||||
|
} else if (eventName === "decrement") {
|
||||||
|
output.decrement(payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
|
client function applyControlValue(nextValue, action, sourceEvent, previousValue) {
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
// and silently swallows the rule that follows it.
|
// and silently swallows the rule that follows it.
|
||||||
component LayoutSplitter {
|
component LayoutSplitter {
|
||||||
outputs {
|
outputs {
|
||||||
// Not named resize. An output named after a native DOM event never reaches
|
// Named sizeChange rather than resize so it cannot be confused with the
|
||||||
// a parent binding: the component emits it, but @resize on the tag is
|
// native window event a caller may already be listening for. An earlier
|
||||||
// never invoked. sizeChange is unambiguous and does arrive.
|
// comment here claimed a natively-named output could never reach a parent
|
||||||
|
// binding; that was wrong, and the rename was never what fixed anything.
|
||||||
sizeChange(payload: { size: number })
|
sizeChange(payload: { size: number })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
component List {
|
component List {
|
||||||
|
outputs {
|
||||||
|
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
}
|
||||||
|
|
||||||
props {
|
props {
|
||||||
size: string = "default"
|
size: string = "default"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
@@ -52,7 +56,7 @@ component List {
|
|||||||
class:py-2.5='size === "sm"'
|
class:py-2.5='size === "sm"'
|
||||||
class:px-5='size === "lg"'
|
class:px-5='size === "lg"'
|
||||||
class:py-4='size === "lg"'
|
class:py-4='size === "lg"'
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
|
@click='output.select({ item: item, index: index })'
|
||||||
>
|
>
|
||||||
{#if item.icon}
|
{#if item.icon}
|
||||||
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
<span class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--wire-color-primary-soft)] text-[var(--wire-color-primary)]">
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
component Map {
|
component Map {
|
||||||
|
outputs {
|
||||||
|
// markerClick and select both fire for a marker press; select is the
|
||||||
|
// generic name callers reach for, markerClick the explicit one.
|
||||||
|
markerClick(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
zoom(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
}
|
||||||
|
|
||||||
props {
|
props {
|
||||||
size: string = "default"
|
size: string = "default"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
@@ -54,7 +62,7 @@ component Map {
|
|||||||
aria-label='{item.label || item.title || "Map marker"}'
|
aria-label='{item.label || item.title || "Map marker"}'
|
||||||
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
class="absolute inline-flex size-10 items-center justify-center rounded-full border-4 border-[var(--wire-color-surface-raised)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-lg transition hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||||
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
|
style='left: {item.x || (20 + index * 12)}%; top: {item.y || (30 + (index % 3) * 18)}%;'
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("markerClick", { bubbles: true, detail: item })); event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: item }))'
|
@click='output.markerClick(item); output.select(item)'
|
||||||
>
|
>
|
||||||
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
|
<span class='{item.icon || "icon-[lucide--map-pin]"}' aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -66,7 +74,7 @@ component Map {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Zoom in"
|
aria-label="Zoom in"
|
||||||
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "in" } }))'
|
@click='output.zoom({ direction: "in" })'
|
||||||
>
|
>
|
||||||
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
|
<span class="icon-[lucide--plus] size-4" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -74,7 +82,7 @@ component Map {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label="Zoom out"
|
aria-label="Zoom out"
|
||||||
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
class="inline-flex size-10 items-center justify-center rounded-xl border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-raised)] text-[var(--wire-color-text)] shadow-sm transition hover:bg-[var(--wire-color-surface-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("zoom", { bubbles: true, detail: { direction: "out" } }))'
|
@click='output.zoom({ direction: "out" })'
|
||||||
>
|
>
|
||||||
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
|
<span class="icon-[lucide--minus] size-4" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
component Marquee {
|
component Marquee {
|
||||||
|
outputs {
|
||||||
|
// Fired when the reader pauses the scroll, by hover, focus or the button.
|
||||||
|
pause(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
resume(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
}
|
||||||
|
|
||||||
props {
|
props {
|
||||||
size: string = "default"
|
size: string = "default"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
@@ -27,8 +33,8 @@ component Marquee {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="group relative min-w-0 flex-1 overflow-hidden"
|
class="group relative min-w-0 flex-1 overflow-hidden"
|
||||||
@mouseenter='paused = true; event.currentTarget.dispatchEvent(new CustomEvent("pause", { bubbles: true }))'
|
@mouseenter='paused = true; output.pause({})'
|
||||||
@mouseleave='paused = false; event.currentTarget.dispatchEvent(new CustomEvent("resume", { bubbles: true }))'
|
@mouseleave='paused = false; output.resume({})'
|
||||||
@focusin='paused = true'
|
@focusin='paused = true'
|
||||||
@focusout='paused = false'
|
@focusout='paused = false'
|
||||||
>
|
>
|
||||||
@@ -77,7 +83,7 @@ component Marquee {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
|
aria-label='{paused ? "Resume announcements" : "Pause announcements"}'
|
||||||
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
class="flex shrink-0 items-center justify-center border-l border-[var(--wire-color-border)] px-4 text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--wire-color-focus)]"
|
||||||
@click='paused = !paused; event.currentTarget.dispatchEvent(new CustomEvent(paused ? "pause" : "resume", { bubbles: true }))'
|
@click='paused = !paused; paused ? output.pause({}) : output.resume({})'
|
||||||
>
|
>
|
||||||
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
|
<span class="icon-[lucide--pause] size-4" data-show='!paused' aria-hidden="true"></span>
|
||||||
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
|
<span class="icon-[lucide--play] size-4" data-show='paused' aria-hidden="true"></span>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
component SearchBox {
|
component SearchBox {
|
||||||
|
outputs {
|
||||||
|
search(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
clear(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
}
|
||||||
|
|
||||||
props {
|
props {
|
||||||
size: string = "default"
|
size: string = "default"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
@@ -22,7 +27,7 @@ component SearchBox {
|
|||||||
data-ui-component="SearchBox"
|
data-ui-component="SearchBox"
|
||||||
role="search"
|
role="search"
|
||||||
class='w-full {class}'
|
class='w-full {class}'
|
||||||
@submit='event.preventDefault(); event.currentTarget.dispatchEvent(new CustomEvent("search", { bubbles: true, detail: { value: query, name: name } }))'
|
@submit='event.preventDefault(); output.search({ value: query, name: name })'
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
|
class="mb-2 block text-sm font-semibold text-[var(--wire-color-text)]"
|
||||||
@@ -61,7 +66,7 @@ component SearchBox {
|
|||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
data-show='query.length > 0 && !disabled'
|
data-show='query.length > 0 && !disabled'
|
||||||
class="absolute right-12 inline-flex size-8 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
class="absolute right-12 inline-flex size-8 items-center justify-center rounded-lg text-[var(--wire-color-text-muted)] transition hover:bg-[var(--wire-color-surface-soft)] hover:text-[var(--wire-color-text)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--wire-color-focus)]"
|
||||||
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); event.currentTarget.dispatchEvent(new CustomEvent("clear", { bubbles: true, detail: { value: "", name: name } }))'
|
@click='query = ""; event.currentTarget.parentElement.querySelector("input")?.focus(); output.clear({ value: "", name: name })'
|
||||||
>
|
>
|
||||||
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
|
<span class="icon-[lucide--x] size-4" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
component Timeline {
|
component Timeline {
|
||||||
|
outputs {
|
||||||
|
select(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
|
||||||
|
}
|
||||||
|
|
||||||
props {
|
props {
|
||||||
size: string = "default"
|
size: string = "default"
|
||||||
color: string = "primary"
|
color: string = "primary"
|
||||||
@@ -30,7 +34,7 @@ component Timeline {
|
|||||||
{#each items as item, index}
|
{#each items as item, index}
|
||||||
<li
|
<li
|
||||||
class="relative pb-8 pl-8 last:pb-0"
|
class="relative pb-8 pl-8 last:pb-0"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("select", { bubbles: true, detail: { item: item, index: index } }))'
|
@click='output.select({ item: item, index: index })'
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute -left-[1.05rem] top-0 flex size-8 items-center justify-center rounded-full border-4 border-[var(--wire-color-background)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-sm"
|
class="absolute -left-[1.05rem] top-0 flex size-8 items-center justify-center rounded-full border-4 border-[var(--wire-color-background)] bg-[var(--wire-color-primary)] text-[var(--wire-color-on-primary)] shadow-sm"
|
||||||
|
|||||||
@@ -57,34 +57,17 @@ component ToggleCount {
|
|||||||
: emptyValue
|
: emptyValue
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dispatchToggleEvent(sourceEvent, previousValue, root, customEvent) {
|
client function dispatchToggleEvent(sourceEvent, previousValue, payload) {
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-toggle-count]")
|
payload = {
|
||||||
|
component: "ToggleCount",
|
||||||
if (!root) {
|
name: name,
|
||||||
return
|
value: selectedValue,
|
||||||
|
previousValue: previousValue,
|
||||||
|
firstValue: firstValue,
|
||||||
|
secondValue: secondValue
|
||||||
}
|
}
|
||||||
|
output.change(payload)
|
||||||
customEvent = document.createEvent("CustomEvent")
|
output.toggle(payload)
|
||||||
customEvent.initCustomEvent("change", true, false, {
|
|
||||||
component: "ToggleCount",
|
|
||||||
name: name,
|
|
||||||
value: selectedValue,
|
|
||||||
previousValue: previousValue,
|
|
||||||
firstValue: firstValue,
|
|
||||||
secondValue: secondValue
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("toggle", true, false, {
|
|
||||||
component: "ToggleCount",
|
|
||||||
name: name,
|
|
||||||
value: selectedValue,
|
|
||||||
previousValue: previousValue,
|
|
||||||
firstValue: firstValue,
|
|
||||||
secondValue: secondValue
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client function selectValue(sourceEvent, nextValue, previousValue) {
|
client function selectValue(sourceEvent, nextValue, previousValue) {
|
||||||
|
|||||||
@@ -35,22 +35,23 @@ component Alert {
|
|||||||
state visible: boolean = true
|
state visible: boolean = true
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
client function dispatchAlertEvent(sourceEvent, eventName, action, root, customEvent) {
|
// The output name has to be written out rather than computed: outputs are
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-alert]")
|
// resolved as named properties, so a dynamic key would not reach a parent
|
||||||
|
// binding. Only two names exist here, so a branch is honest and cheap.
|
||||||
if (!root) {
|
client function dispatchAlertEvent(sourceEvent, eventName, action, payload) {
|
||||||
return
|
payload = {
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent(eventName, true, false, {
|
|
||||||
component: "Alert",
|
component: "Alert",
|
||||||
title: title,
|
title: title,
|
||||||
color: color,
|
color: color,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
action: action
|
action: action
|
||||||
})
|
}
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
|
if (eventName === "dismiss") {
|
||||||
|
output.dismiss(payload)
|
||||||
|
} else {
|
||||||
|
output.action(payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dismissAlert(sourceEvent) {
|
client function dismissAlert(sourceEvent) {
|
||||||
|
|||||||
@@ -35,15 +35,13 @@ component Badge {
|
|||||||
state visible: boolean = true
|
state visible: boolean = true
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
client function dismissBadge(sourceEvent, root, customEvent) {
|
// output.dismiss reaches a parent @dismiss binding; a raw dispatchEvent on
|
||||||
|
// the root does not. The runtime registers parent handlers in a registry
|
||||||
|
// that only the output proxy consults, so the CustomEvent this used to
|
||||||
|
// build bubbled past every binding and was never seen by anyone.
|
||||||
|
client function dismissBadge() {
|
||||||
visible = false
|
visible = false
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-badge]")
|
output.dismiss({ component: "Badge", label: label })
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("dismiss", true, false, {
|
|
||||||
component: "Badge",
|
|
||||||
label: label
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,67 +45,27 @@ component Card {
|
|||||||
state dismissed: boolean = false
|
state dismissed: boolean = false
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
client function dispatchCardNavigation(sourceEvent, item, index, root, customEvent) {
|
// Outputs must go through output.*; a CustomEvent dispatched on the root
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
|
// bubbles past every parent @binding without being seen, because the
|
||||||
|
// runtime keeps parent handlers in a registry only the output proxy reads.
|
||||||
if (!root) {
|
client function dispatchCardNavigation(sourceEvent, item, index) {
|
||||||
return
|
output.navigate({ component: "Card", item: item, index: index })
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("navigate", true, false, {
|
|
||||||
component: "Card",
|
|
||||||
item: item,
|
|
||||||
index: index
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dispatchCardNavigationValue(sourceEvent, root, customEvent) {
|
client function dispatchCardNavigationValue(sourceEvent) {
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
|
output.navigate({
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("navigate", true, false, {
|
|
||||||
component: "Card",
|
component: "Card",
|
||||||
value: sourceEvent.currentTarget.value
|
value: sourceEvent.currentTarget.value
|
||||||
})
|
})
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dispatchCardHeaderAction(sourceEvent, action, index, root, customEvent) {
|
client function dispatchCardHeaderAction(sourceEvent, action, index) {
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
|
output.action({ component: "Card", action: action, index: index })
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("action", true, false, {
|
|
||||||
component: "Card",
|
|
||||||
action: action,
|
|
||||||
index: index
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client function dismissCard(sourceEvent, root, customEvent) {
|
client function dismissCard() {
|
||||||
dismissed = true
|
dismissed = true
|
||||||
root = sourceEvent.currentTarget.closest("[data-wrn-card]")
|
output.dismiss({ component: "Card", title: title })
|
||||||
|
|
||||||
if (!root) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
customEvent = document.createEvent("CustomEvent")
|
|
||||||
customEvent.initCustomEvent("dismiss", true, false, {
|
|
||||||
component: "Card",
|
|
||||||
title: title
|
|
||||||
})
|
|
||||||
root.dispatchEvent(customEvent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +97,8 @@ component Card {
|
|||||||
alt='{item.imageAlt || item.alt || ""}'
|
alt='{item.imageAlt || item.alt || ""}'
|
||||||
loading='{item.loading || "lazy"}'
|
loading='{item.loading || "lazy"}'
|
||||||
decoding="async"
|
decoding="async"
|
||||||
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { item: item, index: itemIndex } }))'
|
@load='output.load({ item: item, index: itemIndex })'
|
||||||
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { item: item, index: itemIndex } }))'
|
@error='output.error({ item: item, index: itemIndex })'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -157,7 +117,7 @@ component Card {
|
|||||||
<a
|
<a
|
||||||
href='{item.actionHref || item.href || "#"}'
|
href='{item.actionHref || item.href || "#"}'
|
||||||
class="wire-next__card-action"
|
class="wire-next__card-action"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { item: item, index: itemIndex } }))'
|
@click='output.action({ item: item, index: itemIndex })'
|
||||||
>
|
>
|
||||||
<span>{item.actionLabel || "Learn more"}</span>
|
<span>{item.actionLabel || "Learn more"}</span>
|
||||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||||
@@ -180,8 +140,8 @@ component Card {
|
|||||||
alt='{imageAlt}'
|
alt='{imageAlt}'
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
|
@load='output.load({ src: imageSrc })'
|
||||||
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
|
@error='output.error({ src: imageSrc })'
|
||||||
/>
|
/>
|
||||||
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
|
<div class="wire-next__card-overlay-shade" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,8 +154,8 @@ component Card {
|
|||||||
alt='{imageAlt}'
|
alt='{imageAlt}'
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
|
@load='output.load({ src: imageSrc })'
|
||||||
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
|
@error='output.error({ src: imageSrc })'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -323,7 +283,7 @@ component Card {
|
|||||||
<a
|
<a
|
||||||
href='{actionHref || "#"}'
|
href='{actionHref || "#"}'
|
||||||
class="wire-next__card-action"
|
class="wire-next__card-action"
|
||||||
@click='event.currentTarget.dispatchEvent(new CustomEvent("action", { bubbles: true, detail: { href: actionHref, label: actionLabel } }))'
|
@click='output.action({ href: actionHref, label: actionLabel })'
|
||||||
>
|
>
|
||||||
<span>{actionLabel}</span>
|
<span>{actionLabel}</span>
|
||||||
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
<span class="icon-[lucide--arrow-right]" aria-hidden="true"></span>
|
||||||
@@ -340,8 +300,8 @@ component Card {
|
|||||||
alt='{imageAlt}'
|
alt='{imageAlt}'
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
@load='event.currentTarget.dispatchEvent(new CustomEvent("load", { bubbles: true, detail: { src: imageSrc } }))'
|
@load='output.load({ src: imageSrc })'
|
||||||
@error='event.currentTarget.dispatchEvent(new CustomEvent("error", { bubbles: true, detail: { src: imageSrc } }))'
|
@error='output.error({ src: imageSrc })'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -3103,14 +3103,18 @@ test("every wire color token a component references is defined by the theme", ()
|
|||||||
|
|
||||||
test("no component gains an output that nothing ever emits", () => {
|
test("no component gains an output that nothing ever emits", () => {
|
||||||
/*
|
/*
|
||||||
* LayoutSplitter declared resizeStart, resize and resizeEnd with no pointer
|
* An output only reaches a parent @binding when the component calls
|
||||||
* handling at all: a caller wired up @resize and received nothing, for ever,
|
* output.<name>(). The runtime keeps parent handlers in a registry that only
|
||||||
* with no error. The same shape survives in other components, so this pins
|
* invokeComponentOutput reads, so a component that instead dispatches its own
|
||||||
* the count rather than letting it grow while the rest are rebuilt.
|
* CustomEvent -- even a bubbling one, on its own root -- is emitting into
|
||||||
|
* nothing: the parent binding is never invoked and no error is raised.
|
||||||
|
* Eighteen components did exactly that and were converted; this pins what is
|
||||||
|
* left, which are components with no emitter of any kind.
|
||||||
*
|
*
|
||||||
* Native event names are excluded. The runtime binds a DOM-listener fallback
|
* Native event names are excluded, and that exclusion is real rather than
|
||||||
* on component tags, so declaring click or input as an output does reach a
|
* assumed: invokeComponentOutput falls back to dispatchComponentEvent when no
|
||||||
* parent binding through bubbling.
|
* handler is registered, and a parent @click on a component tag is also bound
|
||||||
|
* as an ordinary DOM listener, so a natively-named output does arrive.
|
||||||
*/
|
*/
|
||||||
const native = new Set([
|
const native = new Set([
|
||||||
"click",
|
"click",
|
||||||
@@ -3160,7 +3164,54 @@ test("no component gains an output that nothing ever emits", () => {
|
|||||||
* A ceiling, not a target. It only ever moves down: rebuilding one of these
|
* A ceiling, not a target. It only ever moves down: rebuilding one of these
|
||||||
* components should tighten it.
|
* components should tighten it.
|
||||||
*/
|
*/
|
||||||
expect(offenders.length).toBeLessThanOrEqual(32);
|
expect(offenders.length).toBeLessThanOrEqual(22);
|
||||||
expect(offenders).not.toContain("LayoutSplitter.sizeChange");
|
expect(offenders).not.toContain("LayoutSplitter.sizeChange");
|
||||||
expect(offenders).not.toContain("CustomScrollbar.scroll");
|
expect(offenders).not.toContain("CustomScrollbar.scroll");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a declared output is never emitted as a hand-built CustomEvent", () => {
|
||||||
|
/*
|
||||||
|
* The failure this prevents is silent in both directions: the component
|
||||||
|
* looks like it emits, the caller looks like it listens, and the event
|
||||||
|
* bubbles right past the binding because the runtime resolves parent
|
||||||
|
* handlers from a registry rather than from the DOM. Verified in a browser
|
||||||
|
* before this test was written -- an AnnouncementBar dispatching its own
|
||||||
|
* bubbling "dismiss" never reached a page-level @dismiss, and the same
|
||||||
|
* component reached it immediately once it called output.dismiss().
|
||||||
|
*
|
||||||
|
* Dispatching on window is a different thing and stays allowed: that is how
|
||||||
|
* Toaster, Modal and DataTable signal across component boundaries, where
|
||||||
|
* there is no parent binding to reach.
|
||||||
|
*/
|
||||||
|
const offenders: string[] = [];
|
||||||
|
for (const name of uiComponentNames()) {
|
||||||
|
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||||
|
const block = /^ {2}outputs \{([\s\S]*?)^ {2}\}/m.exec(source);
|
||||||
|
if (!block) continue;
|
||||||
|
|
||||||
|
const declared = new Set(
|
||||||
|
[...block[1]!.matchAll(/^\s*([A-Za-z][A-Za-z0-9_]*)\s*\(/gm)].map((m) => m[1]!),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const match of source.matchAll(
|
||||||
|
/(\w+(?:\.\w+)*)\.dispatchEvent\(|initCustomEvent\(\s*"([A-Za-z][A-Za-z0-9_]*)"/g,
|
||||||
|
)) {
|
||||||
|
const target = match[1];
|
||||||
|
if (target && /^window\b/.test(target)) continue;
|
||||||
|
|
||||||
|
// Which event name is being built here?
|
||||||
|
const around = source.slice(Math.max(0, match.index - 400), match.index + 200);
|
||||||
|
for (const declaredName of declared) {
|
||||||
|
const quoted = `"${declaredName}"`;
|
||||||
|
if (
|
||||||
|
around.includes(`CustomEvent(${quoted}`) ||
|
||||||
|
around.includes(`initCustomEvent(${quoted}`)
|
||||||
|
) {
|
||||||
|
offenders.push(`${name}.${declaredName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect([...new Set(offenders)]).toEqual([]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user