Compare commits
2
Commits
84e58b1798
...
0ddb481159
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ddb481159 | ||
|
|
3441b96362 |
@@ -341,7 +341,7 @@
|
||||
},
|
||||
"packages/csr": {
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
@@ -661,7 +661,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.22",
|
||||
"version": "0.8.23",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"packages/ui/components/TogglePassword.wrn": "6bd4d9fa82b0c8e80178430d5a42b1b3ec9725fb62be158585f68af9338c901a",
|
||||
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
||||
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
||||
"packages/ui/components/WysiwygEditor.wrn": "3f9d1765184581e3c1f335769a9f19969917450258bc95ba076980d99f5610b5",
|
||||
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
|
||||
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
|
||||
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
||||
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.26",
|
||||
"version": "0.8.27",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -2724,9 +2724,17 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
);
|
||||
}
|
||||
};
|
||||
node.addEventListener(outName, domListener);
|
||||
/*
|
||||
* focus and blur do not bubble, so a bubble-phase listener on the
|
||||
* component root never hears its own input or button take focus --
|
||||
* the binding silently did nothing. Capture reaches the descendant.
|
||||
* The same flag must be passed to removeEventListener or the
|
||||
* listener outlives the component.
|
||||
*/
|
||||
var capture = outName === "focus" || outName === "blur";
|
||||
node.addEventListener(outName, domListener, capture);
|
||||
cleanupCallbacks.push(function () {
|
||||
node.removeEventListener(outName, domListener);
|
||||
node.removeEventListener(outName, domListener, capture);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1781,3 +1781,57 @@ test("a class binding inside data-for follows state the row never mentions", ()
|
||||
expect(items()[0]?.classList.contains("is-active")).toBe(false);
|
||||
expect(items()[1]?.classList.contains("is-active")).toBe(true);
|
||||
});
|
||||
|
||||
test("a @focus binding on a component tag fires for a focusable element inside it", () => {
|
||||
/*
|
||||
* focus and blur do NOT bubble. The runtime bound every output-named DOM
|
||||
* fallback listener in the bubble phase, so a parent writing @focus on a
|
||||
* component tag never heard the component's own input or button take focus:
|
||||
* the event fired on the descendant and stopped there. Nothing errored --
|
||||
* the binding simply did nothing, which is why the library still declares
|
||||
* focus/blur outputs on components that could never deliver them
|
||||
* (button, TextLink, WysiwygEditor).
|
||||
*
|
||||
* The ui ratchet for "outputs nothing emits" excludes natively-named
|
||||
* outputs on the stated grounds that a native event reaches the root
|
||||
* anyway. That reasoning holds for click and change, which bubble, and
|
||||
* fails for focus and blur, which do not.
|
||||
*/
|
||||
const win = mount(
|
||||
`<div data-scope="seen: ''">` +
|
||||
`<span id="out">{seen}</span>` +
|
||||
`<div data-scope="n: 0" data-wrn-hydration="Field:x">` +
|
||||
`<div data-wrn-events="focus" data-wrn-out-focus="seen = 'focused'">` +
|
||||
`<button id="inner">press</button>` +
|
||||
`</div></div></div>`,
|
||||
);
|
||||
|
||||
const inner = win.document.querySelector("#inner") as unknown as {
|
||||
dispatchEvent: (event: unknown) => boolean;
|
||||
};
|
||||
// A real focus event: fires on the descendant, does not bubble.
|
||||
const FocusEventCtor = (win as unknown as { Event: typeof Event }).Event;
|
||||
inner.dispatchEvent(new FocusEventCtor("focus", { bubbles: false }));
|
||||
|
||||
expect(win.document.querySelector("#out")?.textContent).toBe("focused");
|
||||
});
|
||||
|
||||
test("a @change binding still fires for a bubbling event inside a component", () => {
|
||||
// The companion case: change DOES bubble, and must keep working unchanged.
|
||||
const win = mount(
|
||||
`<div data-scope="seen: ''">` +
|
||||
`<span id="out">{seen}</span>` +
|
||||
`<div data-scope="n: 0" data-wrn-hydration="Field:y">` +
|
||||
`<div data-wrn-events="change" data-wrn-out-change="seen = 'changed'">` +
|
||||
`<input id="inner" />` +
|
||||
`</div></div></div>`,
|
||||
);
|
||||
|
||||
const inner = win.document.querySelector("#inner") as unknown as {
|
||||
dispatchEvent: (event: unknown) => boolean;
|
||||
};
|
||||
const EventCtor = (win as unknown as { Event: typeof Event }).Event;
|
||||
inner.dispatchEvent(new EventCtor("change", { bubbles: true }));
|
||||
|
||||
expect(win.document.querySelector("#out")?.textContent).toBe("changed");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
component WysiwygEditor {
|
||||
// A chrome shell around a slotted editor, not an editor itself: it renders a
|
||||
// heading, a description, an optional item strip and a slot. It therefore
|
||||
// emits none of these outputs on its own -- they forward whatever the slotted
|
||||
// control raises. Slot a textarea or input and a parent @change or @input
|
||||
// hears it, because those events bubble to this root; @focus and @blur arrive
|
||||
// too, since the runtime binds the non-bubbling pair in the capture phase.
|
||||
// Slot nothing interactive and there is nothing to hear.
|
||||
outputs {
|
||||
input(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.22",
|
||||
"version": "0.8.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -3320,6 +3320,14 @@ test("no component gains an output that nothing ever emits", () => {
|
||||
* assumed: invokeComponentOutput falls back to dispatchComponentEvent when no
|
||||
* 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.
|
||||
*
|
||||
* That reasoning held only for events that bubble. focus and blur do not, so
|
||||
* the bubble-phase fallback never heard a descendant take focus and every
|
||||
* focus/blur output here was undeliverable -- silently, since a missing
|
||||
* output raises nothing. The runtime now binds those two in the capture
|
||||
* phase, which is what makes their exclusion honest rather than convenient.
|
||||
* See "a @focus binding on a component tag fires for a focusable element
|
||||
* inside it" in packages/csr/test/reactive.test.ts.
|
||||
*/
|
||||
const native = new Set([
|
||||
"click",
|
||||
|
||||
Reference in New Issue
Block a user