From 1574f2a8124b2b31a9862793c7c5c9505206037a Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Mon, 9 Mar 2026 13:15:44 +0200 Subject: [PATCH 1/4] Created a plan for supporting any trigger in Interact --- .../custom_event_triggers_7d957d99.plan.md | 225 ++++++++++++++++++ packages/interact/src/types.ts | 2 +- 2 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 .cursor/plans/custom_event_triggers_7d957d99.plan.md diff --git a/.cursor/plans/custom_event_triggers_7d957d99.plan.md b/.cursor/plans/custom_event_triggers_7d957d99.plan.md new file mode 100644 index 0000000..45992aa --- /dev/null +++ b/.cursor/plans/custom_event_triggers_7d957d99.plan.md @@ -0,0 +1,225 @@ +--- +name: Custom event triggers +overview: Allow any DOM event name to be used as a `trigger` in Interact, routing unknown trigger names through the existing `eventTrigger` handler. This requires opening up the `TriggerType` union, adding a handler resolution function, and updating add/remove/types accordingly. +todos: + - id: rename-params + content: Rename StateParams -> StateTriggerParams and PointerTriggerParams -> AnimationTriggerParams across all source, rules, and docs files + status: pending + - id: types + content: Update TriggerType and InteractionParamsTypes in src/types.ts (keep eventConfig internal) + status: pending + - id: handler-resolution + content: Add getHandlerForTrigger() to src/handlers/index.ts for dynamic handler resolution + status: pending + - id: core-add + content: Update src/core/add.ts to use getHandlerForTrigger() instead of direct map access + status: pending + - id: tests + content: Add test/eventTrigger.spec.ts with custom event trigger tests + status: pending + - id: demo + content: Create CustomEventDemo.tsx in apps/demo and add to App.tsx + status: pending + - id: rules + content: Add rules/custom-event.md and update rules/full-lean.md + status: pending + - id: docs + content: Update docs/guides/understanding-triggers.md and docs/api/types.md + status: pending +isProject: false +--- + +# Custom Event Triggers for Interact + +## Architecture Summary + +Currently, triggers flow through a static handler map: + +```mermaid +flowchart LR + Config["InteractConfig\n(trigger: TriggerType)"] --> AddTS["add.ts\naddInteraction()"] + AddTS --> HandlerMap["TRIGGER_TO_HANDLER_MODULE_MAP\n(static object)"] + HandlerMap --> ViewEnter["viewEnter handler"] + HandlerMap --> EventTrigger["eventTrigger handler"] + HandlerMap --> ViewProgress["viewProgress handler"] + HandlerMap --> PointerMove["pointerMove handler"] + HandlerMap --> AnimEnd["animationEnd handler"] + EventTrigger --> Presets["EVENT_TRIGGER_PRESETS\nhover/click/activate/interest"] +``` + + + +The key insight: `hover`, `click`, `activate`, and `interest` are all thin wrappers around `eventTrigger.ts`. They just inject a preset event config (e.g., hover -> `{ enter: ['mouseenter'], leave: ['mouseleave'] }`). For custom triggers, we do the same -- the trigger name itself becomes the event config. + +**After the change**, unknown trigger names will be automatically routed through `eventTrigger` using the trigger name as the DOM event: + +```mermaid +flowchart LR + Config["trigger: 'dblclick'"] --> AddTS["add.ts\naddInteraction()"] + AddTS --> GetHandler["getHandlerForTrigger()"] + GetHandler -->|known| HandlerMap["Static map\n(viewEnter, hover, etc.)"] + GetHandler -->|unknown| DynHandler["Dynamic handler\neventConfig = trigger name"] + DynHandler --> EventTrigger["eventTrigger.add()"] +``` + + + +## Part 0: Rename `StateParams` and `PointerTriggerParams` + +Rename across the entire codebase: + +- `StateParams` -> `StateTriggerParams` +- `PointerTriggerParams` -> `AnimationTriggerParams` + +**Files affected (source):** + +- `src/types.ts` -- type definitions, `EventTriggerParams`, `TriggerParams`, `InteractionParamsTypes`, `IInteractElement.toggleEffect` +- `src/handlers/index.ts` -- import and `withEventTriggerConfig` signature +- `src/handlers/eventTrigger.ts` -- import and cast usages in `addEventTriggerHandler` +- `src/handlers/effectHandlers.ts` -- import and function signatures +- `src/web/InteractElement.ts` -- import for `toggleEffect` method +- `src/core/InteractionController.ts` -- import for `toggleEffect` method + +**Files affected (rules/docs):** + +- `rules/full-lean.md` -- references in params documentation +- `rules/click.md` -- references to `StateParams.method` +- `docs/api/types.md` -- type documentation +- `docs/api/interaction-controller.md` -- `toggleEffect` param type +- `docs/api/README.md` -- type references + +Use `replace_all` for each rename. This is a straightforward find-and-replace with no logic changes. + +## Part 1: Type Changes + +**File: [src/types.ts](packages/interact/src/types.ts)** + +1. Open up `TriggerType` to accept any string while preserving IDE autocomplete for known types: + +```typescript +export type TriggerType = + | 'hover' + | 'click' + | 'viewEnter' + | 'pageVisible' + | 'animationEnd' + | 'viewProgress' + | 'pointerMove' + | 'activate' + | 'interest' + | (string & {}); +``` + +1. `TriggerParams` stays the same -- no `EventTriggerParams` added. `eventConfig` remains internal-only and is never exposed in the public config API. Custom event triggers use `StateTriggerParams | AnimationTriggerParams` (same as `click`/`hover`). +2. Add a string index signature to `InteractionParamsTypes` (line 252) so that custom trigger keys resolve properly in generics: + +```typescript +export type InteractionParamsTypes = { + hover: StateTriggerParams | AnimationTriggerParams; + click: StateTriggerParams | AnimationTriggerParams; + // ... existing entries ... + [key: string]: StateTriggerParams | AnimationTriggerParams | ViewEnterParams | PointerMoveParams | AnimationEndParams; +}; +``` + +## Part 2: Handler Resolution + +**File: [src/handlers/index.ts](packages/interact/src/handlers/index.ts)** + +Export a `getHandlerForTrigger(trigger)` function that: + +- Returns the existing handler module for known triggers (hover, click, viewEnter, etc.) +- For unknown triggers, returns a dynamically created handler that internally injects the trigger name as `eventConfig` (the internal property) into `eventTrigger.add()`. The user never sees or provides `eventConfig` -- it is derived from the trigger name. +- All dynamic handlers share `eventTrigger.remove` for cleanup + +```typescript +const KNOWN_HANDLERS = { /* existing map */ }; + +export function getHandlerForTrigger(trigger: string) { + if (trigger in KNOWN_HANDLERS) { + return KNOWN_HANDLERS[trigger as TriggerType]; + } + + return { + add: (source, target, effect, options, interactOptions) => { + // eventConfig is internal-only -- derived from the trigger name, never from user params + eventTrigger.add(source, target, effect, { ...options, eventConfig: trigger }, interactOptions ?? {}); + }, + remove: eventTrigger.remove, + }; +} + +export default KNOWN_HANDLERS; +``` + +## Part 3: Core `add.ts` Changes + +**File: [src/core/add.ts](packages/interact/src/core/add.ts)** + +- Import `getHandlerForTrigger` from `../handlers` +- In `addInteraction()` (line 715), replace: +`TRIGGER_TO_HANDLER_MODULE_MAP[trigger]?.add(...)` with `getHandlerForTrigger(trigger).add(...)` +- In `_processSequences()` (line 446) and `_processSequencesForTarget()` (line 536), apply the same replacement for the sequence handler dispatch + +## Part 4: Core `remove.ts` -- No Changes Needed + +The `removeListItems` function iterates `Object.values(TRIGGER_TO_HANDLER_MODULE_MAP)` and calls `module.remove(element)`. Since `eventTrigger.remove` is already among these values (referenced by hover, click, activate, interest), and since `eventTrigger` uses a single `WeakMap>` for all event-based handlers, calling `eventTrigger.remove` once cleans up ALL event handlers for that element -- including any custom triggers. No changes needed. + +## Part 5: Tests + +**File: [test/eventTrigger.spec.ts](packages/interact/test/eventTrigger.spec.ts)** (new) + +Add tests covering: + +- Custom trigger name (e.g., `trigger: 'dblclick'`) routes through `eventTrigger` and attaches the correct DOM listener +- Custom trigger with `toggle` mode (StateTriggerParams `method: 'toggle'`) +- Custom trigger with `once` mode (AnimationTriggerParams `type: 'once'`) +- Cleanup of custom trigger handlers via `remove()` +- Existing built-in triggers still work as before (regression) + +Use the same mock patterns from existing tests (mock `@wix/motion`, create DOM elements, test event dispatch). + +## Part 6: Demo Playground + +**File: [apps/demo/src/react/components/CustomEventDemo.tsx](apps/demo/src/react/components/CustomEventDemo.tsx)** (new) + +Create a demo component showcasing custom event triggers: + +- A text input that responds to `input` event with a pulse animation +- A card that responds to `dblclick` with a flip animation +- A card that responds to `contextmenu` (right-click) with a shake animation +- Controls to switch between different custom event types +- Follow the same pattern as `Playground.tsx`: `useMemo` for config, `useInteractInstance(config)`, `` components + +**File: [apps/demo/src/react/App.tsx](apps/demo/src/react/App.tsx)** + +- Import and add `` to the page layout + +## Part 7: Rules Update + +**File: [rules/custom-event.md](packages/interact/rules/custom-event.md)** (new) + +Add a succinct rule file for custom event triggers: + +- Explain that any DOM event name can be used as a trigger +- Show the basic pattern: `trigger: 'dblclick'` with `StateTriggerParams` or `AnimationTriggerParams` (same params as click/hover) +- 2-3 concise examples + +**File: [rules/full-lean.md](packages/interact/rules/full-lean.md)** + +- Update the `trigger: TriggerType` section to mention that any DOM event name is accepted as a custom trigger +- Note that custom triggers use the same params as click/hover (`StateTriggerParams | AnimationTriggerParams`) + +## Part 8: Docs Update + +**File: [docs/guides/understanding-triggers.md](packages/interact/docs/guides/understanding-triggers.md)** + +- Add a new section "Custom Event Triggers" explaining arbitrary DOM events as triggers +- Update the trigger overview table to include custom event triggers +- Add examples for `dblclick`, `input`, `contextmenu` using `StateTriggerParams`/`AnimationTriggerParams` + +**File: [docs/api/types.md](packages/interact/docs/api/types.md)** + +- Update `TriggerType` documentation to reflect it now accepts any string +- Note that custom triggers accept `StateTriggerParams | AnimationTriggerParams` (same as click/hover) + diff --git a/packages/interact/src/types.ts b/packages/interact/src/types.ts index d04e885..ab4102b 100644 --- a/packages/interact/src/types.ts +++ b/packages/interact/src/types.ts @@ -191,7 +191,7 @@ export type InteractionTrigger = { key: string; listContainer?: string; listItemSelector?: string; - trigger: TriggerType; + trigger: TriggerType | TriggerType[] | { enter?: TriggerType | TriggerType[] ; leave?: TriggerType | TriggerType[] } ; params?: TriggerParams; conditions?: string[]; selector?: string; From 328b5b05825dfb057f8cc3a8fcbae6b1142f53d1 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Mon, 9 Mar 2026 13:17:35 +0200 Subject: [PATCH 2/4] Fixed format --- .../custom_event_triggers_7d957d99.plan.md | 19 ++++++++++++++++--- packages/interact/src/types.ts | 5 ++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.cursor/plans/custom_event_triggers_7d957d99.plan.md b/.cursor/plans/custom_event_triggers_7d957d99.plan.md index 45992aa..f373b09 100644 --- a/.cursor/plans/custom_event_triggers_7d957d99.plan.md +++ b/.cursor/plans/custom_event_triggers_7d957d99.plan.md @@ -118,7 +118,12 @@ export type InteractionParamsTypes = { hover: StateTriggerParams | AnimationTriggerParams; click: StateTriggerParams | AnimationTriggerParams; // ... existing entries ... - [key: string]: StateTriggerParams | AnimationTriggerParams | ViewEnterParams | PointerMoveParams | AnimationEndParams; + [key: string]: + | StateTriggerParams + | AnimationTriggerParams + | ViewEnterParams + | PointerMoveParams + | AnimationEndParams; }; ``` @@ -133,7 +138,9 @@ Export a `getHandlerForTrigger(trigger)` function that: - All dynamic handlers share `eventTrigger.remove` for cleanup ```typescript -const KNOWN_HANDLERS = { /* existing map */ }; +const KNOWN_HANDLERS = { + /* existing map */ +}; export function getHandlerForTrigger(trigger: string) { if (trigger in KNOWN_HANDLERS) { @@ -143,7 +150,13 @@ export function getHandlerForTrigger(trigger: string) { return { add: (source, target, effect, options, interactOptions) => { // eventConfig is internal-only -- derived from the trigger name, never from user params - eventTrigger.add(source, target, effect, { ...options, eventConfig: trigger }, interactOptions ?? {}); + eventTrigger.add( + source, + target, + effect, + { ...options, eventConfig: trigger }, + interactOptions ?? {}, + ); }, remove: eventTrigger.remove, }; diff --git a/packages/interact/src/types.ts b/packages/interact/src/types.ts index ab4102b..0dcca24 100644 --- a/packages/interact/src/types.ts +++ b/packages/interact/src/types.ts @@ -191,7 +191,10 @@ export type InteractionTrigger = { key: string; listContainer?: string; listItemSelector?: string; - trigger: TriggerType | TriggerType[] | { enter?: TriggerType | TriggerType[] ; leave?: TriggerType | TriggerType[] } ; + trigger: + | TriggerType + | TriggerType[] + | { enter?: TriggerType | TriggerType[]; leave?: TriggerType | TriggerType[] }; params?: TriggerParams; conditions?: string[]; selector?: string; From 88b6eb587739f001ec7960b39db539895a79b87a Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Thu, 12 Mar 2026 10:50:47 +0200 Subject: [PATCH 3/4] Revert type changea --- packages/interact/src/types.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/interact/src/types.ts b/packages/interact/src/types.ts index 0dcca24..d04e885 100644 --- a/packages/interact/src/types.ts +++ b/packages/interact/src/types.ts @@ -191,10 +191,7 @@ export type InteractionTrigger = { key: string; listContainer?: string; listItemSelector?: string; - trigger: - | TriggerType - | TriggerType[] - | { enter?: TriggerType | TriggerType[]; leave?: TriggerType | TriggerType[] }; + trigger: TriggerType; params?: TriggerParams; conditions?: string[]; selector?: string; From e1987c97c300ea62018ddf22089631d63b231458 Mon Sep 17 00:00:00 2001 From: Yehonatan Daniv Date: Thu, 12 Mar 2026 10:53:56 +0200 Subject: [PATCH 4/4] Fixed format --- .cursor/plans/custom_event_triggers_7d957d99.plan.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.cursor/plans/custom_event_triggers_7d957d99.plan.md b/.cursor/plans/custom_event_triggers_7d957d99.plan.md index f373b09..8535677 100644 --- a/.cursor/plans/custom_event_triggers_7d957d99.plan.md +++ b/.cursor/plans/custom_event_triggers_7d957d99.plan.md @@ -47,8 +47,6 @@ flowchart LR EventTrigger --> Presets["EVENT_TRIGGER_PRESETS\nhover/click/activate/interest"] ``` - - The key insight: `hover`, `click`, `activate`, and `interest` are all thin wrappers around `eventTrigger.ts`. They just inject a preset event config (e.g., hover -> `{ enter: ['mouseenter'], leave: ['mouseleave'] }`). For custom triggers, we do the same -- the trigger name itself becomes the event config. **After the change**, unknown trigger names will be automatically routed through `eventTrigger` using the trigger name as the DOM event: @@ -62,8 +60,6 @@ flowchart LR DynHandler --> EventTrigger["eventTrigger.add()"] ``` - - ## Part 0: Rename `StateParams` and `PointerTriggerParams` Rename across the entire codebase: @@ -171,7 +167,7 @@ export default KNOWN_HANDLERS; - Import `getHandlerForTrigger` from `../handlers` - In `addInteraction()` (line 715), replace: -`TRIGGER_TO_HANDLER_MODULE_MAP[trigger]?.add(...)` with `getHandlerForTrigger(trigger).add(...)` + `TRIGGER_TO_HANDLER_MODULE_MAP[trigger]?.add(...)` with `getHandlerForTrigger(trigger).add(...)` - In `_processSequences()` (line 446) and `_processSequencesForTarget()` (line 536), apply the same replacement for the sequence handler dispatch ## Part 4: Core `remove.ts` -- No Changes Needed @@ -235,4 +231,3 @@ Add a succinct rule file for custom event triggers: - Update `TriggerType` documentation to reflect it now accepts any string - Note that custom triggers accept `StateTriggerParams | AnimationTriggerParams` (same as click/hover) -