diff --git a/.vscode/launch.json b/.vscode/launch.json index 9bd8c2409..4a1ff481a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "--async-stack-traces" ], "args": [ - "${fileBasename}", + "${file}", "--verbose", "--no-cache", "-i" diff --git a/packages/@dcl/ecs/src/systems/events.ts b/packages/@dcl/ecs/src/systems/events.ts index 91d92dc2e..9789d2772 100644 --- a/packages/@dcl/ecs/src/systems/events.ts +++ b/packages/@dcl/ecs/src/systems/events.ts @@ -52,6 +52,20 @@ export interface PointerEventsSystem { */ removeOnPointerUp(entity: Entity): void + /** + * @public + * Remove the callback for onPointerHoverEnter event + * @param entity - Entity where the callback was attached + */ + removeOnPointerHoverEnter(entity: Entity): void + + /** + * @public + * Remove the callback for onPointerHoverLeave event + * @param entity - Entity where the callback was attached + */ + removeOnPointerHoverLeave(entity: Entity): void + /** * @internal * Execute callback when the user clicks the entity. @@ -88,6 +102,28 @@ export interface PointerEventsSystem { * @param opts - Opts to trigger Feedback and Button */ onPointerUp(entity: Entity, cb: EventSystemCallback, opts?: Partial): void + + /** + * @public + * Execute callback when the user place the pointer over the entity + * @param pointerData - Entity to attach the callback - Opts to trigger Feedback and Button + * @param cb - Function to execute when click fires + */ + onPointerHoverEnter( + pointerData: { entity: Entity; opts?: Partial }, + cb: EventSystemCallback + ): void + + /** + * @public + * Execute callback when the user take the pointer out of the entity + * @param pointerData - Entity to attach the callback - Opts to trigger Feedback and Button + * @param cb - Function to execute when click fires + */ + onPointerHoverLeave( + pointerData: { entity: Entity; opts?: Partial }, + cb: EventSystemCallback + ): void } /** @@ -100,7 +136,9 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy enum EventType { Click, Down, - Up + Up, + HoverEnter, + HoverLeave } type EventMapType = Map @@ -135,6 +173,10 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy function getPointerEvent(eventType: EventType) { if (eventType === EventType.Up) { return PointerEventType.PET_UP + } else if (eventType === EventType.HoverLeave) { + return PointerEventType.PET_HOVER_LEAVE + } else if (eventType === EventType.HoverEnter) { + return PointerEventType.PET_HOVER_ENTER } return PointerEventType.PET_DOWN } @@ -164,7 +206,12 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy checkNotThenable(cb(command.up), 'Click event returned a thenable. Only synchronous functions are allowed') } - if (eventType === EventType.Down || eventType === EventType.Up) { + if ( + eventType === EventType.Down || + eventType === EventType.Up || + eventType === EventType.HoverEnter || + eventType === EventType.HoverLeave + ) { const command = inputSystem.getInputCommand(opts.button, getPointerEvent(eventType), entity) if (command) { checkNotThenable(cb(command), 'Event handler returned a thenable. Only synchronous functions are allowed') @@ -198,6 +245,24 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy setPointerEvent(entity, PointerEventType.PET_UP, options) } + const onPointerHoverEnter: PointerEventsSystem['onPointerHoverEnter'] = (...args) => { + const [data, cb] = args + const { entity, opts } = data + const options = getDefaultOpts(opts) + removeEvent(entity, EventType.HoverEnter) + getEvent(entity).set(EventType.HoverEnter, { cb, opts: options }) + setPointerEvent(entity, PointerEventType.PET_HOVER_ENTER, options) + } + + const onPointerHoverLeave: PointerEventsSystem['onPointerHoverLeave'] = (...args) => { + const [data, cb] = args + const { entity, opts } = data + const options = getDefaultOpts(opts) + removeEvent(entity, EventType.HoverLeave) + getEvent(entity).set(EventType.HoverLeave, { cb, opts: options }) + setPointerEvent(entity, PointerEventType.PET_HOVER_LEAVE, options) + } + return { removeOnClick(entity: Entity) { removeEvent(entity, EventType.Click) @@ -211,6 +276,14 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy removeEvent(entity, EventType.Up) }, + removeOnPointerHoverEnter(entity: Entity) { + removeEvent(entity, EventType.HoverEnter) + }, + + removeOnPointerHoverLeave(entity: Entity) { + removeEvent(entity, EventType.HoverLeave) + }, + onClick(value, cb) { const { entity } = value const options = getDefaultOpts(value.opts) @@ -223,6 +296,8 @@ export function createPointerEventsSystem(engine: IEngine, inputSystem: IInputSy }, onPointerDown, - onPointerUp + onPointerUp, + onPointerHoverEnter, + onPointerHoverLeave } } diff --git a/packages/@dcl/playground-assets/etc/playground-assets.api.md b/packages/@dcl/playground-assets/etc/playground-assets.api.md index ac86f6d71..23bca36c0 100644 --- a/packages/@dcl/playground-assets/etc/playground-assets.api.md +++ b/packages/@dcl/playground-assets/etc/playground-assets.api.md @@ -1082,6 +1082,8 @@ export type EntityComponents = { uiDropdown: PBUiDropdown; onMouseDown: Callback; onMouseUp: Callback; + onMouseEnter: Callback; + onMouseLeave: Callback; }; // @public (undocumented) @@ -1558,6 +1560,8 @@ export interface LastWriteWinElementSetComponentDefinition extends BaseCompon export type Listeners = { onMouseDown?: Callback; onMouseUp?: Callback; + onMouseEnter?: Callback; + onMouseLeave?: Callback; }; // @public (undocumented) @@ -3313,6 +3317,14 @@ export interface PointerEventsSystem { }, cb: EventSystemCallback): void; // @deprecated (undocumented) onPointerDown(entity: Entity, cb: EventSystemCallback, opts?: Partial): void; + onPointerHoverEnter(pointerData: { + entity: Entity; + opts?: Partial; + }, cb: EventSystemCallback): void; + onPointerHoverLeave(pointerData: { + entity: Entity; + opts?: Partial; + }, cb: EventSystemCallback): void; onPointerUp(pointerData: { entity: Entity; opts?: Partial; @@ -3320,6 +3332,8 @@ export interface PointerEventsSystem { // @deprecated (undocumented) onPointerUp(entity: Entity, cb: EventSystemCallback, opts?: Partial): void; removeOnPointerDown(entity: Entity): void; + removeOnPointerHoverEnter(entity: Entity): void; + removeOnPointerHoverLeave(entity: Entity): void; removeOnPointerUp(entity: Entity): void; } diff --git a/packages/@dcl/react-ecs/src/components/Button/index.tsx b/packages/@dcl/react-ecs/src/components/Button/index.tsx index f74a36781..2e43a74fb 100644 --- a/packages/@dcl/react-ecs/src/components/Button/index.tsx +++ b/packages/@dcl/react-ecs/src/components/Button/index.tsx @@ -35,7 +35,7 @@ function getButtonProps(props: UiButtonProps) { */ /* @__PURE__ */ export function Button(props: UiButtonProps) { - const { uiTransform, uiBackground, onMouseDown, onMouseUp, ...otherProps } = props + const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props const buttonProps = getButtonProps(props) const uiBackgroundProps = parseUiBackground({ ...buttonProps.uiBackground, @@ -64,6 +64,8 @@ export function Button(props: UiButtonProps) { } diff --git a/packages/@dcl/react-ecs/src/components/Input/index.tsx b/packages/@dcl/react-ecs/src/components/Input/index.tsx index a6dde2d75..a88f95047 100644 --- a/packages/@dcl/react-ecs/src/components/Input/index.tsx +++ b/packages/@dcl/react-ecs/src/components/Input/index.tsx @@ -41,13 +41,15 @@ function parseUiInput(props: Partial): PBUiInput { * @category Component */ /* @__PURE__ */ export function Input(props: EntityPropTypes & Partial) { - const { uiTransform, uiBackground, onMouseDown, onMouseUp, ...otherProps } = props + const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...otherProps } = props const inputProps = parseUiInput(otherProps) const commonProps = parseProps({ uiTransform, uiBackground, onMouseDown, - onMouseUp + onMouseUp, + onMouseEnter, + onMouseLeave }) return } diff --git a/packages/@dcl/react-ecs/src/components/Label/index.tsx b/packages/@dcl/react-ecs/src/components/Label/index.tsx index f7a06dbc8..777caa77a 100644 --- a/packages/@dcl/react-ecs/src/components/Label/index.tsx +++ b/packages/@dcl/react-ecs/src/components/Label/index.tsx @@ -23,13 +23,15 @@ export { scaleFontSize } from './utils' /* @__PURE__ */ export function Label(props: EntityPropTypes & UiLabelProps) { - const { uiTransform, uiBackground, onMouseDown, onMouseUp, ...uiTextProps } = props + const { uiTransform, uiBackground, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, ...uiTextProps } = props const commonProps = parseProps({ uiTransform, uiBackground, onMouseDown, - onMouseUp + onMouseUp, + onMouseEnter, + onMouseLeave }) const { font, textAlign, fontSize, textWrap, ...textProps } = uiTextProps const uiText: PBUiText = { diff --git a/packages/@dcl/react-ecs/src/components/listeners/types.ts b/packages/@dcl/react-ecs/src/components/listeners/types.ts index 447cd60ef..04fd9a8e9 100644 --- a/packages/@dcl/react-ecs/src/components/listeners/types.ts +++ b/packages/@dcl/react-ecs/src/components/listeners/types.ts @@ -13,11 +13,17 @@ export type Listeners = { onMouseDown?: Callback /** triggered on mouse up event */ onMouseUp?: Callback + /** triggered on mouse hover event */ + onMouseEnter?: Callback + /** triggered on mouse leave event */ + onMouseLeave?: Callback } const listeners: Listeners = { onMouseDown: undefined, - onMouseUp: undefined + onMouseUp: undefined, + onMouseEnter: undefined, + onMouseLeave: undefined } const listenersKey = Object.keys(listeners) diff --git a/packages/@dcl/react-ecs/src/react-ecs.ts b/packages/@dcl/react-ecs/src/react-ecs.ts index c385491ff..f29fcf574 100644 --- a/packages/@dcl/react-ecs/src/react-ecs.ts +++ b/packages/@dcl/react-ecs/src/react-ecs.ts @@ -21,6 +21,8 @@ export type EntityComponents = { uiDropdown: PBUiDropdown onMouseDown: Callback onMouseUp: Callback + onMouseEnter: Callback + onMouseLeave: Callback } /** diff --git a/packages/@dcl/react-ecs/src/reconciler/index.ts b/packages/@dcl/react-ecs/src/reconciler/index.ts index 20fcefad7..6c5ab974a 100644 --- a/packages/@dcl/react-ecs/src/reconciler/index.ts +++ b/packages/@dcl/react-ecs/src/reconciler/index.ts @@ -27,7 +27,9 @@ import { componentKeys, isNotUndefined, noopConfig, propsChanged } from './utils function getPointerEnum(pointerKey: keyof Listeners): PointerEventType { const pointers: { [key in keyof Required]: PointerEventType } = { onMouseDown: PointerEventType.PET_DOWN, - onMouseUp: PointerEventType.PET_UP + onMouseUp: PointerEventType.PET_UP, + onMouseEnter: PointerEventType.PET_HOVER_ENTER, + onMouseLeave: PointerEventType.PET_HOVER_LEAVE } return pointers[pointerKey] } @@ -80,13 +82,20 @@ export function createReconciler( upsertComponent(instance, props as { rightOf: number; parent: number }, 'uiTransform') } - function upsertListener(instance: Instance, update: Changes>) { + function upsertListener( + instance: Instance, + update: Changes> + ) { if (update.type === 'delete' || !update.props) { clickEvents.get(instance.entity)?.delete(getPointerEnum(update.component)) if (update.component === 'onMouseDown') { pointerEvents.removeOnPointerDown(instance.entity) } else if (update.component === 'onMouseUp') { pointerEvents.removeOnPointerUp(instance.entity) + } else if (update.component === 'onMouseEnter') { + pointerEvents.removeOnPointerHoverEnter(instance.entity) + } else if (update.component === 'onMouseLeave') { + pointerEvents.removeOnPointerHoverLeave(instance.entity) } return } @@ -101,13 +110,30 @@ export function createReconciler( if (alreadyHasPointerEvent) return const pointerEventSystem = - update.component === 'onMouseDown' ? pointerEvents.onPointerDown : pointerEvents.onPointerUp - pointerEventSystem(instance.entity, () => pointerEventCallback(instance.entity, pointerEvent), { - button: InputAction.IA_POINTER, - // We add this showFeedBack so the pointerEventSystem creates a PointerEvent component with our entity - // This is needed for the renderer to know which entities are clickeables - showFeedback: true - }) + update.component === 'onMouseDown' + ? pointerEvents.onPointerDown + : update.component === 'onMouseUp' + ? pointerEvents.onPointerUp + : update.component === 'onMouseEnter' + ? pointerEvents.onPointerHoverEnter + : update.component === 'onMouseLeave' + ? pointerEvents.onPointerHoverLeave + : undefined + + if (pointerEventSystem !== undefined) { + pointerEventSystem( + { + entity: instance.entity, + opts: { + button: InputAction.IA_POINTER, + // We add this showFeedBack so the pointerEventSystem creates a PointerEvent component with our entity + // This is needed for the renderer to know which entities are clickeables + showFeedback: true + } + }, + () => pointerEventCallback(instance.entity, pointerEvent) + ) + } } } diff --git a/packages/@dcl/react-ecs/src/reconciler/utils.ts b/packages/@dcl/react-ecs/src/reconciler/utils.ts index 49862fe98..5f9349b06 100644 --- a/packages/@dcl/react-ecs/src/reconciler/utils.ts +++ b/packages/@dcl/react-ecs/src/reconciler/utils.ts @@ -58,6 +58,8 @@ const entityComponent: EntityComponents = { uiTransform: undefined as any, onMouseDown: undefined as any, onMouseUp: undefined as any, + onMouseEnter: undefined as any, + onMouseLeave: undefined as any, uiInput: undefined as any, uiDropdown: undefined as any } diff --git a/test/ecs/events/system.spec.ts b/test/ecs/events/system.spec.ts index 807900a39..8da4537d0 100644 --- a/test/ecs/events/system.spec.ts +++ b/test/ecs/events/system.spec.ts @@ -259,4 +259,88 @@ describe('Events System', () => { EventsSystem.removeOnPointerUp(entity) }) + + it('should run default onHoverEnter', async () => { + const entity = engine.addEntity() + const PointerEvents = components.PointerEvents(engine) + let counter = 0 + EventsSystem.onPointerHoverEnter( + { + entity + }, + () => { + counter += 1 + } + ) + fakePointer(entity, PointerEventType.PET_HOVER_ENTER) + await engine.update(1) + expect(counter).toBe(1) + expect(PointerEvents.getOrNull(entity)).toBe(null) + }) + + it('should remove pointer hover enter', async () => { + const entity = engine.addEntity() + const PointerEvents = components.PointerEvents(engine) + let counter = 0 + EventsSystem.onPointerHoverEnter( + { + entity, + opts: { hoverText: 'test' } + }, + () => { + counter += 1 + EventsSystem.removeOnPointerHoverEnter(entity) + } + ) + fakePointer(entity, PointerEventType.PET_HOVER_ENTER) + await engine.update(1) + expect(counter).toBe(1) + + await engine.update(1) + expect(counter).toBe(1) + const feedback = PointerEvents.getOrNull(entity)?.pointerEvents + expect(feedback?.length).toBe(0) + }) + + it('should run default onHoverLeave', async () => { + const entity = engine.addEntity() + const PointerEvents = components.PointerEvents(engine) + let counter = 0 + EventsSystem.onPointerHoverLeave( + { + entity + }, + () => { + counter += 1 + } + ) + fakePointer(entity, PointerEventType.PET_HOVER_LEAVE) + await engine.update(1) + expect(counter).toBe(1) + expect(PointerEvents.getOrNull(entity)).toBe(null) + }) + + it('should remove pointer hover leave', async () => { + const entity = engine.addEntity() + const PointerEvents = components.PointerEvents(engine) + let counter = 0 + EventsSystem.onPointerHoverLeave( + { + entity, + opts: { hoverText: 'test' } + }, + () => { + counter += 1 + EventsSystem.removeOnPointerHoverLeave(entity) + } + ) + fakePointer(entity, PointerEventType.PET_HOVER_LEAVE) + await engine.update(1) + expect(counter).toBe(1) + + await engine.update(1) + expect(counter).toBe(1) + const feedback = PointerEvents.getOrNull(entity)?.pointerEvents + expect(feedback?.length).toBe(0) + }) }) diff --git a/test/react-ecs/listeners.spec.tsx b/test/react-ecs/listeners.spec.tsx index b74950277..a8ffcef4c 100644 --- a/test/react-ecs/listeners.spec.tsx +++ b/test/react-ecs/listeners.spec.tsx @@ -125,3 +125,59 @@ describe('Ui MouseUp React Ecs', () => { expect(counter).toBe(8888) }) }) + +describe('Ui MouseEnter React Ecs', () => { + const { engine, uiRenderer } = setupEngine() + const PointerEventsResult = components.PointerEventsResult(engine) + const uiEntity = ((engine.addEntity() as number) + 1) as Entity + let fakeCounter = 0 + const mouseEnterEvent = () => { + PointerEventsResult.addValue( + uiEntity, + createTestPointerDownCommand(uiEntity, fakeCounter + 1, PointerEventType.PET_HOVER_ENTER) + ) + fakeCounter += 1 + } + let counter = 0 + let onMouseEnter: (() => void) | undefined = () => { + counter++ + } + + const ui = () => + uiRenderer.setUiRenderer(ui) + + it('the counter should be 0 at the begginning', async () => { + expect(counter).toBe(0) + await engine.update(1) + }) + it('if we create a mouseDown event, then the onMouseEnter fn must be called and increment the counter by 1', async () => { + // Click with the current onMouseEnter + mouseEnterEvent() + await engine.update(1) + expect(counter).toBe(1) + }) + + it('remove onMouseEnter handler and verify that the counter is still 1', async () => { + // Remove onMouseEnter + onMouseEnter = undefined + await engine.update(1) + expect(counter).toBe(1) + }) + + it('create a mouseDown event and verify that the counter is not being incremented', async () => { + mouseEnterEvent() + await engine.update(1) + expect(counter).toBe(1) + }) + + it('replace onMouseEnter callback with a custom counter setter, and create an event to see if its being called', async () => { + // Add a new onMouseEnter + onMouseEnter = () => { + counter = 8888 + } + await engine.update(1) + mouseEnterEvent() + await engine.update(1) + expect(counter).toBe(8888) + }) +})