Browse Source

web: Use SyntheticMouseEvent as a type for a value in mouse events listeners (#862)

* web: Use SyntheticMouseEvent as a type for a value in mouse events listeners

* add tests for mouse event

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
pull/893/head
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
ac76dc5f0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 199
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt
  2. 33
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt
  3. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt
  4. 24
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt
  5. 67
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt
  6. 4
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt
  7. 73
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt
  8. 62
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt

199
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt

@ -1,45 +1,148 @@
package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent
import androidx.compose.web.events.SyntheticDragEvent
import androidx.compose.web.events.SyntheticMouseEvent
import androidx.compose.web.events.SyntheticWheelEvent
import org.jetbrains.compose.web.events.WrappedClipboardEvent
import org.jetbrains.compose.web.events.WrappedDragEvent
import org.jetbrains.compose.web.events.WrappedEvent
import org.jetbrains.compose.web.events.WrappedFocusEvent
import org.jetbrains.compose.web.events.WrappedInputEvent
import org.jetbrains.compose.web.events.WrappedKeyboardEvent
import org.jetbrains.compose.web.events.WrappedMouseEvent
import org.jetbrains.compose.web.events.WrappedRadioInputEvent
import org.jetbrains.compose.web.events.WrappedTextInputEvent
import org.jetbrains.compose.web.events.WrappedTouchEvent
import org.jetbrains.compose.web.events.WrappedWheelEvent
import org.jetbrains.compose.web.events.GenericWrappedEvent
private typealias SyntheticMouseEventListener = (SyntheticMouseEvent) -> Unit
private typealias SyntheticWheelEventListener = (SyntheticWheelEvent) -> Unit
private typealias SyntheticDragEventListener = (SyntheticDragEvent) -> Unit
open class EventsListenerBuilder {
protected val listeners = mutableListOf<WrappedEventListener<*>>()
fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(COPY, options, listener))
/* Mouse Events */
private fun createMouseEventListener(
name: String, options: Options, listener: SyntheticMouseEventListener
): MouseEventListener {
return MouseEventListener(
event = name,
options = options,
listener = {
listener(SyntheticMouseEvent(it.nativeEvent))
}
)
}
fun onCut(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(CUT, options, listener))
private fun createMouseWheelEventListener(
name: String, options: Options, listener: SyntheticWheelEventListener
): MouseWheelEventListener {
return MouseWheelEventListener(
event = name,
options = options,
listener = {
listener(SyntheticWheelEvent(it.nativeEvent))
}
)
}
fun onPaste(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(PASTE, options, listener))
fun onContextMenu(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(CONTEXTMENU, options, listener))
}
fun onClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(CLICK, options, listener))
}
fun onDoubleClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(DBLCLICK, options, listener))
}
fun onMouseDown(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEDOWN, options, listener))
}
fun onMouseUp(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEUP, options, listener))
}
fun onMouseEnter(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEENTER, options, listener))
}
fun onMouseLeave(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSELEAVE, options, listener))
}
fun onMouseMove(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEMOVE, options, listener))
}
fun onMouseOut(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEOUT, options, listener))
}
fun onMouseOver(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEOVER, options, listener))
}
fun onWheel(options: Options = Options.DEFAULT, listener: (SyntheticWheelEvent) -> Unit) {
listeners.add(createMouseWheelEventListener(WHEEL, options, listener))
}
/* Drag Events */
private fun createDragEventListener(
name: String, options: Options, listener: SyntheticDragEventListener
): DragEventListener {
return DragEventListener(
event = name,
options = options,
listener = {
listener(SyntheticDragEvent(it.nativeEvent))
}
)
}
fun onDrag(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAG, options, listener))
}
fun onDrop(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DROP, options, listener))
}
fun onDragStart(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGSTART, options, listener))
}
fun onDragEnd(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGEND, options, listener))
}
fun onDragOver(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGOVER, options, listener))
}
fun onContextMenu(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(CONTEXTMENU, options, listener))
fun onDragEnter(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGENTER, options, listener))
}
fun onClick(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(CLICK, options, listener))
fun onDragLeave(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGLEAVE, options, listener))
}
fun onDoubleClick(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(DBLCLICK, options, listener))
/* End of Drag Events */
fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(COPY, options, listener))
}
fun onCut(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(CUT, options, listener))
}
fun onPaste(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(PASTE, options, listener))
}
fun onGenericInput(
@ -85,38 +188,6 @@ open class EventsListenerBuilder {
listeners.add(KeyboardEventListener(KEYUP, options, listener))
}
fun onMouseDown(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEDOWN, options, listener))
}
fun onMouseUp(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEUP, options, listener))
}
fun onMouseEnter(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEENTER, options, listener))
}
fun onMouseLeave(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSELEAVE, options, listener))
}
fun onMouseMove(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEMOVE, options, listener))
}
fun onMouseOut(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEOUT, options, listener))
}
fun onMouseOver(options: Options = Options.DEFAULT, listener: (WrappedMouseEvent) -> Unit) {
listeners.add(MouseEventListener(MOUSEOVER, options, listener))
}
fun onWheel(options: Options = Options.DEFAULT, listener: (WrappedWheelEvent) -> Unit) {
listeners.add(MouseWheelEventListener(WHEEL, options, listener))
}
fun onScroll(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(SCROLL, options, listener))
}
@ -157,34 +228,6 @@ open class EventsListenerBuilder {
listeners.add(InputEventListener(BEFOREINPUT, options, listener))
}
fun onDrag(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAG, options, listener))
}
fun onDrop(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DROP, options, listener))
}
fun onDragStart(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAGSTART, options, listener))
}
fun onDragEnd(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAGEND, options, listener))
}
fun onDragOver(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAGOVER, options, listener))
}
fun onDragEnter(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAGENTER, options, listener))
}
fun onDragLeave(options: Options = Options.DEFAULT, listener: (WrappedDragEvent) -> Unit) {
listeners.add(DragEventListener(DRAGLEAVE, options, listener))
}
fun collectListeners(): List<WrappedEventListener<*>> = listeners
fun addEventListener(

33
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt

@ -5,42 +5,29 @@
package androidx.compose.web.attributes
import androidx.compose.web.events.SyntheticEvent
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.Options
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticInputEvent<ValueType, Element : HTMLElement>(
class SyntheticInputEvent<ValueType, Element : EventTarget>(
val value: ValueType,
val target: Element,
val nativeEvent: Event
) {
nativeEvent: Event
) : SyntheticEvent<Element>(
nativeEvent = nativeEvent
)
val bubbles: Boolean = nativeEvent.bubbles
val cancelable: Boolean = nativeEvent.cancelable
val composed: Boolean = nativeEvent.composed
val currentTarget: HTMLElement? = nativeEvent.currentTarget.unsafeCast<HTMLInputElement?>()
val eventPhase: Short = nativeEvent.eventPhase
val defaultPrevented: Boolean = nativeEvent.defaultPrevented
val timestamp: Number = nativeEvent.timeStamp
val type: String = nativeEvent.type
val isTrusted: Boolean = nativeEvent.isTrusted
fun preventDefault(): Unit = nativeEvent.preventDefault()
fun stopPropagation(): Unit = nativeEvent.stopPropagation()
fun stopImmediatePropagation(): Unit = nativeEvent.stopImmediatePropagation()
fun composedPath(): Array<EventTarget> = nativeEvent.composedPath()
}
class InputAttrsBuilder<T>(val inputType: InputType<T>) : AttrsBuilder<HTMLInputElement>() {
class InputAttrsBuilder<T>(
val inputType: InputType<T>
) : AttrsBuilder<HTMLInputElement>() {
fun onInput(options: Options = Options.DEFAULT, listener: (SyntheticInputEvent<T, HTMLInputElement>) -> Unit) {
addEventListener(INPUT, options) {
val value = inputType.inputValue(it.nativeEvent)
listener(SyntheticInputEvent(value, it.nativeEvent.target as HTMLInputElement, it.nativeEvent))
listener(SyntheticInputEvent(value, it.nativeEvent))
}
}
}

2
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt

@ -17,7 +17,7 @@ class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() {
) {
addEventListener(INPUT, options) {
val text = it.nativeEvent.target.asDynamic().value.unsafeCast<String>()
listener(SyntheticInputEvent(text, it.nativeEvent.target as HTMLTextAreaElement, it.nativeEvent))
listener(SyntheticInputEvent(text, it.nativeEvent))
}
}
}

24
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt

@ -0,0 +1,24 @@
package androidx.compose.web.events
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
open class SyntheticEvent<Element : EventTarget>(
val nativeEvent: Event
) {
val target: Element = nativeEvent.target.unsafeCast<Element>()
val bubbles: Boolean = nativeEvent.bubbles
val cancelable: Boolean = nativeEvent.cancelable
val composed: Boolean = nativeEvent.composed
val currentTarget: EventTarget? = nativeEvent.currentTarget
val eventPhase: Short = nativeEvent.eventPhase
val defaultPrevented: Boolean = nativeEvent.defaultPrevented
val timestamp: Number = nativeEvent.timeStamp
val type: String = nativeEvent.type
val isTrusted: Boolean = nativeEvent.isTrusted
fun preventDefault(): Unit = nativeEvent.preventDefault()
fun stopPropagation(): Unit = nativeEvent.stopPropagation()
fun stopImmediatePropagation(): Unit = nativeEvent.stopImmediatePropagation()
fun composedPath(): Array<EventTarget> = nativeEvent.composedPath()
}

67
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt

@ -0,0 +1,67 @@
package androidx.compose.web.events
import org.w3c.dom.DataTransfer
import org.w3c.dom.DragEvent
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.EventTarget
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
/**
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
*/
open class SyntheticMouseEvent(
nativeEvent: MouseEvent
) : SyntheticEvent<EventTarget>(nativeEvent) {
private val mouseEvent = nativeEvent
val altKey: Boolean = nativeEvent.altKey
val button: Short = nativeEvent.button
val buttons: Short = nativeEvent.buttons
val clientX: Int = nativeEvent.clientX
val clientY: Int = nativeEvent.clientY
val ctrlKey: Boolean = nativeEvent.ctrlKey
val metaKey: Boolean = nativeEvent.metaKey
val movementX: Int = nativeEvent.asDynamic().movementX as Int
val movementY: Int = nativeEvent.asDynamic().movementY as Int
val offsetX: Double = nativeEvent.offsetX
val offsetY: Double = nativeEvent.offsetY
val pageX: Double = nativeEvent.pageX
val pageY: Double = nativeEvent.pageY
val region: String? = nativeEvent.region
val relatedTarget: EventTarget? = nativeEvent.relatedTarget
val screenX: Int = nativeEvent.screenX
val screenY: Int = nativeEvent.screenY
val shiftKey: Boolean = nativeEvent.shiftKey
val x: Double = nativeEvent.x
val y: Double = nativeEvent.y
fun getModifierState(keyArg: String): Boolean = mouseEvent.getModifierState(keyArg)
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
*/
class SyntheticWheelEvent(
nativeEvent: WheelEvent
) : SyntheticMouseEvent(
nativeEvent
) {
val deltaX: Double = nativeEvent.deltaX
val deltaY: Double = nativeEvent.deltaY
val deltaZ: Double = nativeEvent.deltaZ
val deltaMode: Int = nativeEvent.deltaMode
}
/**
* https://developer.mozilla.org/en-US/docs/Web/API/DragEvent
*/
class SyntheticDragEvent(
nativeEvent: DragEvent
) : SyntheticMouseEvent(
nativeEvent
) {
val dataTransfer: DataTransfer? = nativeEvent.dataTransfer
}

4
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt

@ -18,7 +18,7 @@ interface GenericWrappedEvent<T : Event> {
interface WrappedEvent : GenericWrappedEvent<Event>
open class WrappedMouseEvent(
internal open class WrappedMouseEvent(
override val nativeEvent: MouseEvent
) : GenericWrappedEvent<MouseEvent> {
@ -29,7 +29,7 @@ open class WrappedMouseEvent(
get() = nativeEvent.asDynamic().movementY as Double
}
open class WrappedWheelEvent(
internal open class WrappedWheelEvent(
override val nativeEvent: WheelEvent
) : GenericWrappedEvent<WheelEvent>

73
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt

@ -101,4 +101,77 @@ class EventsTests {
}
})
}
val mouseEnterPlusExtraButtonsPressedUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P(
attrs = {
style { height(50.px) }
}
) { TestText(state) }
Div(attrs = {
id("box")
style {
backgroundColor("red")
padding(50.px)
width(300.px)
}
onMouseEnter {
val buttonsPressed = mutableListOf<String>()
if (it.altKey) buttonsPressed.add("ALT")
if (it.ctrlKey) buttonsPressed.add("CTRL")
if (it.shiftKey) buttonsPressed.add("SHIFT")
if (it.metaKey) buttonsPressed.add("META")
state = "ENTERED+${buttonsPressed.joinToString(separator = ",")}"
}
}) {
Text("Enter mouse over me with buttons pressed (META, ALT, SHIFT, CTRL)")
}
}
val onMouseContextMenuUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P(
attrs = {
id("box")
style { height(50.px) }
onContextMenu {
if (it.button == 2.toShort()) {
it.preventDefault()
it.stopImmediatePropagation()
state = "MOUSE CONTEXT MENU"
}
}
}
) { TestText(state) }
}
val displayMouseCoordinates by testCase {
var state by remember { mutableStateOf("None") }
Div(
attrs = {
id("box")
style {
width(200.px)
height(200.px)
backgroundColor("red")
}
onMouseMove {
state = "${it.x},${it.y}|${it.offsetX},${it.offsetY}"
}
}
)
P(
attrs = {
style { height(50.px) }
}
) { TestText(state) }
}
}

62
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt

@ -174,4 +174,66 @@ class EventTests : BaseIntegrationTests() {
driver.waitTextToBe(value = "childInput")
driver.waitTextToBe(textId = "txt2", value = "None")
}
@ResolveDrivers
fun mouseEnterPlusExtraButtonsPressedUpdatesText(driver: WebDriver) {
driver.openTestPage("mouseEnterPlusExtraButtonsPressedUpdatesText")
driver.waitTextToBe(value = "None")
val box = driver.findElement(By.id("box"))
Actions(driver).moveToElement(box).perform()
driver.waitTextToBe(value = "ENTERED+")
Actions(driver).moveByOffset(0, 100)
.keyDown(Keys.CONTROL)
.moveToElement(box)
.keyUp(Keys.CONTROL)
.perform()
driver.waitTextToBe(value = "ENTERED+CTRL")
Actions(driver).moveByOffset(0, 100)
.keyDown(Keys.SHIFT)
.moveToElement(box)
.keyUp(Keys.SHIFT)
.perform()
driver.waitTextToBe(value = "ENTERED+SHIFT")
Actions(driver).moveByOffset(0, 100)
.keyDown(Keys.ALT)
.moveToElement(box)
.keyUp(Keys.ALT)
.perform()
driver.waitTextToBe(value = "ENTERED+ALT")
}
@ResolveDrivers
fun onMouseContextMenuUpdatesText(driver: WebDriver) {
driver.openTestPage("onMouseContextMenuUpdatesText")
driver.waitTextToBe(value = "None")
val box = driver.findElement(By.id("box"))
Actions(driver).contextClick(box).perform()
driver.waitTextToBe(value = "MOUSE CONTEXT MENU")
}
@ResolveDrivers
fun displayMouseCoordinates(driver: WebDriver) {
driver.openTestPage("displayMouseCoordinates")
driver.waitTextToBe(value = "None")
val box = driver.findElement(By.id("box"))
Actions(driver).moveToElement(box).perform()
driver.waitTextToBe(value = "108,108|100,100")
Actions(driver).moveToElement(box).moveByOffset(-20, -20).perform()
driver.waitTextToBe(value = "88,88|80,80")
}
}

Loading…
Cancel
Save