Skip to content

Commit 2bceece

Browse files
committedSep 23, 2021
Allow reading current event in state generator
1 parent f88c0ea commit 2bceece

File tree

2 files changed

+224
-14
lines changed

2 files changed

+224
-14
lines changed
 

‎src/index.test.ts

Lines changed: 181 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
start,
1515
accumulate,
1616
onceStateChangesTo,
17+
readContext,
1718
} from "./index";
1819

1920
test("node version " + process.version, () => {});
@@ -707,32 +708,205 @@ describe("Wrapping AbortController as a state machine", () => {
707708

708709
describe("Button click", () => {
709710
function ButtonClickListener(button: HTMLButtonElement) {
710-
function* initial() {
711-
yield on("click", clicked);
711+
function* Initial() {
712+
yield on("click", Clicked);
712713
yield listenTo(button, "click");
713714
}
714-
function* clicked() {}
715+
function* Clicked() {}
715716

716-
return initial;
717+
return Initial;
717718
}
718719

719720
it("listens when button clicks", () => {
720721
const button = document.createElement('button');
721722
const machine = start(ButtonClickListener.bind(null, button));
722723

723-
expect(machine.current).toEqual("initial");
724+
expect(machine.current).toEqual("Initial");
724725
expect(machine.changeCount).toEqual(0);
725726

726727
button.click();
727-
expect(machine.current).toEqual("clicked");
728+
expect(machine.current).toEqual("Clicked");
728729
expect(machine.changeCount).toEqual(1);
729730

730731
button.click();
731-
expect(machine.current).toEqual("clicked");
732+
expect(machine.current).toEqual("Clicked");
732733
expect(machine.changeCount).toEqual(1);
733734
});
734735
});
735736

737+
describe("FIXME: Key shortcut click highlighting too many event listeners bug", () => {
738+
function KeyShortcutListener(el: HTMLElement) {
739+
function* Open() {
740+
yield on("keydown", OpenCheckingKey);
741+
yield listenTo(el, "keydown");
742+
}
743+
function* OpenCheckingKey() {
744+
const event: KeyboardEvent = yield readContext("event");
745+
yield cond(event.key === 'Escape', Closed);
746+
// yield revert();
747+
yield always(Open);
748+
}
749+
function* Closed() {
750+
yield on("keydown", ClosedCheckingKey);
751+
yield listenTo(el, "keydown");
752+
}
753+
function* ClosedCheckingKey() {
754+
const event: KeyboardEvent = yield readContext("event");
755+
yield cond(event.key === 'Enter', Open);
756+
// yield revert();
757+
yield always(Closed);
758+
}
759+
760+
return Closed;
761+
}
762+
763+
it("listens when keys are pressed", () => {
764+
// FIXME: there’s lots of event listeners being created!
765+
const input = document.createElement('input');
766+
const machine = start(KeyShortcutListener.bind(null, input));
767+
768+
expect(machine.current).toEqual("Closed");
769+
expect(machine.changeCount).toEqual(0);
770+
771+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
772+
expect(machine.current).toEqual("Open");
773+
expect(machine.changeCount).toEqual(2);
774+
775+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
776+
expect(machine.current).toEqual("Open");
777+
expect(machine.changeCount).toEqual(6);
778+
779+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
780+
expect(machine.current).toEqual("Open");
781+
expect(machine.changeCount).toEqual(14);
782+
783+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
784+
expect(machine.current).toEqual("Closed");
785+
expect(machine.changeCount).toEqual(30);
786+
787+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
788+
expect(machine.current).toEqual("Closed");
789+
expect(machine.changeCount).toEqual(62);
790+
791+
machine.abort();
792+
});
793+
});
794+
795+
describe("Key shortcut cond reading event", () => {
796+
function KeyShortcutListener(el: HTMLElement) {
797+
function* Open() {
798+
yield listenTo(el, "keydown");
799+
yield on(
800+
"keydown",
801+
cond((readContext) => {
802+
const event = readContext("event") as KeyboardEvent;
803+
return event.key === "Escape";
804+
}, Closed)
805+
);
806+
}
807+
function* Closed() {
808+
yield listenTo(el, "keydown");
809+
yield on(
810+
"keydown",
811+
cond((readContext) => {
812+
const event = readContext("event") as KeyboardEvent;
813+
return event.key === "Enter";
814+
}, Open)
815+
);
816+
}
817+
818+
return Closed;
819+
}
820+
821+
it("listens when keys are pressed", () => {
822+
const input = document.createElement('input');
823+
const machine = start(KeyShortcutListener.bind(null, input));
824+
825+
expect(machine.current).toEqual("Closed");
826+
expect(machine.changeCount).toEqual(0);
827+
828+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
829+
expect(machine.current).toEqual("Open");
830+
expect(machine.changeCount).toEqual(1);
831+
832+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
833+
expect(machine.current).toEqual("Open");
834+
expect(machine.changeCount).toEqual(1);
835+
836+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
837+
expect(machine.current).toEqual("Open");
838+
expect(machine.changeCount).toEqual(1);
839+
840+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
841+
expect(machine.current).toEqual("Closed");
842+
expect(machine.changeCount).toEqual(2);
843+
844+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
845+
expect(machine.current).toEqual("Closed");
846+
expect(machine.changeCount).toEqual(2);
847+
848+
machine.abort();
849+
});
850+
});
851+
852+
describe("Element focus", () => {
853+
function* ButtonFocusListener(el: HTMLElement) {
854+
yield listenTo(el.ownerDocument, "focusin");
855+
yield on("focusin", compound(CheckingActive));
856+
857+
function* Inactive() {}
858+
function* Active() {}
859+
function* CheckingActive() {
860+
yield cond(el.ownerDocument.activeElement === el, Active);
861+
yield always(Inactive);
862+
}
863+
864+
return CheckingActive;
865+
}
866+
867+
it("listens when element receives and loses focus", () => {
868+
const button = document.body.appendChild(document.createElement('button'));
869+
const input = document.body.appendChild(document.createElement('input'));
870+
871+
const machine = start(ButtonFocusListener.bind(null, button));
872+
873+
expect(machine.current).toEqual("Inactive");
874+
expect(machine.changeCount).toEqual(0);
875+
876+
button.focus();
877+
expect(machine.current).toEqual("Active");
878+
expect(machine.changeCount).toEqual(2);
879+
880+
button.focus();
881+
expect(machine.current).toEqual("Active");
882+
expect(machine.changeCount).toEqual(2);
883+
884+
input.focus();
885+
expect(machine.current).toEqual("Inactive");
886+
expect(machine.changeCount).toEqual(4);
887+
888+
button.focus();
889+
expect(machine.current).toEqual("Active");
890+
expect(machine.changeCount).toEqual(6);
891+
892+
machine.abort();
893+
button.remove();
894+
input.remove();
895+
});
896+
897+
it("is initially Active if element is already focused when starting", () => {
898+
const button = document.body.appendChild(document.createElement('button'));
899+
900+
button.focus();
901+
const machine = start(ButtonFocusListener.bind(null, button));
902+
903+
expect(machine.current).toEqual("Active");
904+
expect(machine.changeCount).toEqual(0);
905+
906+
button.remove();
907+
})
908+
});
909+
736910
describe("accumulate()", () => {
737911
const messagesKey = Symbol("messages");
738912

‎src/index.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface ExitAction {
2222
export type StateDefinition = () => Generator<Yielded, any, unknown>;
2323
export interface Cond {
2424
type: "cond";
25-
cond: Function | boolean;
25+
cond: ((readContext: (contextName: string | symbol) => unknown) => boolean) | boolean;
2626
target: StateDefinition;
2727
}
2828
export interface Compound {
@@ -60,7 +60,12 @@ export interface Accumulate {
6060
resultKey: symbol;
6161
}
6262

63-
export type Yielded = On | Always | Cond | EntryAction | ExitAction | ListenTo | Accumulate | Call<any>;
63+
export interface ReadContext {
64+
type: "readContext";
65+
contextName: string | symbol;
66+
}
67+
68+
export type Yielded = On | Always | Cond | EntryAction | ExitAction | ListenTo | ReadContext | Accumulate | Call<any>;
6469

6570
export function on<Event extends string | symbol>(event: Event, target: Target): On {
6671
return { type: "on", on: event, target };
@@ -97,7 +102,12 @@ export function always(target: Target): Always {
97102
return { type: "always", target };
98103
}
99104

100-
export function cond(cond: (() => boolean) | boolean, target: StateDefinition): Cond {
105+
export function cond(
106+
cond:
107+
| ((readContext: (contextName: string | symbol) => unknown) => boolean)
108+
| boolean,
109+
target: StateDefinition
110+
): Cond {
101111
return { type: "cond", cond, target };
102112
}
103113

@@ -109,6 +119,10 @@ export function accumulate(eventName: string | symbol, resultKey: symbol): Accum
109119
return { type: "accumulate", eventName, resultKey };
110120
}
111121

122+
export function readContext(contextName: string | symbol): ReadContext {
123+
return { type: "readContext", contextName };
124+
}
125+
112126
export interface MachineInstance extends Iterator<null | string | Record<string, string>, void, string | symbol> {
113127
readonly changeCount: number;
114128
readonly current: null | string | Record<string, string>;
@@ -149,7 +163,7 @@ class Handlers {
149163
this.eventsToAccumulate.splice(0, Infinity);
150164
}
151165

152-
add(value: Yielded) {
166+
add(value: Yielded, readContext: (contextName: string | symbol) => unknown): unknown | void {
153167
if (value.type === "entry") {
154168
this.entryActions.push(value);
155169

@@ -187,7 +201,10 @@ class Handlers {
187201
this.eventsToListenTo.push([value.eventName, value.sender]);
188202
} else if (value.type === 'accumulate') {
189203
this.eventsToAccumulate.push([value.eventName, value.resultKey]);
204+
} else if (value.type === 'readContext') {
205+
return readContext(value.contextName);
190206
}
207+
return undefined;
191208
}
192209

193210
finish(): null | Promise<Record<string, any>> {
@@ -236,6 +253,9 @@ class InternalInstance {
236253
didChangeState: () => void;
237254
didChangeAccumulations: () => void;
238255
sendEvent: (event: string, changeCount?: number) => void;
256+
willHandleEvent: (event: Event) => void;
257+
didHandleEvent: (event: Event) => void;
258+
readContext: (contextName: string | symbol) => unknown;
239259
}
240260
) {
241261
this.definition = machineDefinition;
@@ -297,7 +317,9 @@ class InternalInstance {
297317
}
298318

299319
handleEvent(event: Event) {
320+
this.callbacks.willHandleEvent(event);
300321
this.receive(event);
322+
this.callbacks.didHandleEvent(event);
301323
}
302324

303325
cleanup() {
@@ -319,14 +341,15 @@ class InternalInstance {
319341
const initialReturn = stateGenerator();
320342
if (initialReturn[Symbol.iterator]) {
321343
const iterator = initialReturn[Symbol.iterator]();
344+
let reply: unknown = undefined;
322345
while (true) {
323-
const item = iterator.next()
346+
const item = iterator.next(reply)
324347
if (item.done) {
325348
var initialGenerator = item.value;
326349
break;
327350
}
328351

329-
this.globalHandlers.add(item.value);
352+
reply = this.globalHandlers.add(item.value, this.callbacks.readContext);
330353
}
331354

332355
const promise = this.globalHandlers.finish();
@@ -394,7 +417,7 @@ class InternalInstance {
394417
processTarget(target: Target): boolean {
395418
if ('type' in target) {
396419
if (target.type === "cond") {
397-
const result = typeof target.cond === 'boolean' ? target.cond : target.cond();
420+
const result = typeof target.cond === 'boolean' ? target.cond : target.cond(this.callbacks.readContext);
398421
if (result) {
399422
if (this.parent !== null) {
400423
this.parent.transitionTo(target.target);
@@ -457,6 +480,7 @@ export function start(
457480
machine: (() => StateDefinition) | (() => Generator<Yielded, StateDefinition, never>)
458481
): MachineInstance {
459482
let _changeCount = -1;
483+
let _activeEvent: null | Event = null;
460484
let _aborter: null | AbortController = null;
461485
function ensureAborter(): AbortController {
462486
if (_aborter !== null) return _aborter;
@@ -493,6 +517,18 @@ export function start(
493517
return;
494518
}
495519
instance.receive(event);
520+
},
521+
willHandleEvent(event) {
522+
_activeEvent = event;
523+
},
524+
didHandleEvent(event) {
525+
_activeEvent = null;
526+
},
527+
readContext(key) {
528+
if (key === "event") {
529+
return _activeEvent;
530+
}
531+
return undefined;
496532
}
497533
}
498534
);

0 commit comments

Comments
 (0)
Please sign in to comment.