Browse Source

web: align the rest of events (#887)

* web: update change and beforeInput event listeners

* web: update Clipboard events: copy, cut, paste

* web: update Keyboard events: keyup, keydown

* web: update select event (only for input and textarea)

* web: update FocusEvents: focus, blur, focusin, focusout

* web: update Touch Events

* web: remove search event listener as it's not supported in Firefox

* web: update Animation Events

* web: update scroll event

* web: rename WrappedEventListener to SyntheticEventListener

* refactor internal event listeners to avoid redundant lambdas

* remove WrappedEvent.kt

* move SyntheticInputEvent.kt to `events` package

* Add tests for Select element events

* remove import from another changelist

* web: add Form events: submit and reset

* fix PR discussions

* web: rename `attrsBuilder` to `attrs`

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
pull/905/head
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
6d97c6d055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt
  2. 15
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt
  3. 190
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt
  4. 33
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt
  5. 156
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt
  6. 23
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt
  7. 168
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/WrappedEventListener.kt
  8. 53
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt
  9. 58
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt
  10. 43
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt
  11. 19
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt
  12. 78
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt
  13. 21
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticAnimationEvent.kt
  14. 11
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticChangeEvent.kt
  15. 21
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticClipboardEvent.kt
  16. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt
  17. 12
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticFocusEvent.kt
  18. 20
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticInputEvent.kt
  19. 44
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticKeyboardEvent.kt
  20. 2
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt
  21. 26
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSelectEvent.kt
  22. 9
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSubmitEvent.kt
  23. 18
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticTouchEvent.kt
  24. 107
      web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt
  25. 3
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt
  26. 234
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt
  27. 57
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt
  28. 75
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/SelectElementTests.kt
  29. 173
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt
  30. 48
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt
  31. 49
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/SelectElementTests.kt
  32. 2
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt

8
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt

@ -1,13 +1,11 @@
package org.jetbrains.compose.web package org.jetbrains.compose.web
import androidx.compose.runtime.AbstractApplier import androidx.compose.runtime.AbstractApplier
import org.jetbrains.compose.web.attributes.WrappedEventListener import org.jetbrains.compose.web.attributes.SyntheticEventListener
import org.jetbrains.compose.web.css.StyleHolder import org.jetbrains.compose.web.css.StyleHolder
import org.jetbrains.compose.web.dom.setProperty import org.jetbrains.compose.web.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable import org.jetbrains.compose.web.dom.setVariable
import kotlinx.dom.clear import kotlinx.dom.clear
import org.jetbrains.compose.web.attributes.Options
import org.jetbrains.compose.web.css.jsObject
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.Node import org.w3c.dom.Node
@ -46,9 +44,9 @@ external interface EventListenerOptions {
} }
open class DomNodeWrapper(open val node: Node) { open class DomNodeWrapper(open val node: Node) {
private var currentListeners = emptyList<WrappedEventListener<*>>() private var currentListeners = emptyList<SyntheticEventListener<*>>()
fun updateEventListeners(list: List<WrappedEventListener<*>>) { fun updateEventListeners(list: List<SyntheticEventListener<*>>) {
val htmlElement = node as? HTMLElement ?: return val htmlElement = node as? HTMLElement ?: return
currentListeners.forEach { currentListeners.forEach {

15
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt

@ -1,5 +1,6 @@
package org.jetbrains.compose.web.attributes package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.events.SyntheticSubmitEvent
import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLFormElement import org.w3c.dom.HTMLFormElement
@ -92,6 +93,20 @@ fun AttrsBuilder<HTMLFormElement>.noValidate() =
fun AttrsBuilder<HTMLFormElement>.target(value: FormTarget) = fun AttrsBuilder<HTMLFormElement>.target(value: FormTarget) =
attr("target", value.targetStr) attr("target", value.targetStr)
fun AttrsBuilder<HTMLFormElement>.onSubmit(
options: Options = Options.DEFAULT,
listener: (SyntheticSubmitEvent) -> Unit
) {
addEventListener(eventName = EventsListenerBuilder.SUBMIT, options = options, listener = listener)
}
fun AttrsBuilder<HTMLFormElement>.onReset(
options: Options = Options.DEFAULT,
listener: (SyntheticSubmitEvent) -> Unit
) {
addEventListener(eventName = EventsListenerBuilder.RESET, options = options, listener = listener)
}
/* Input attributes */ /* Input attributes */
fun AttrsBuilder<HTMLInputElement>.type(value: InputType<*>) = fun AttrsBuilder<HTMLInputElement>.type(value: InputType<*>) =

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

@ -1,15 +1,12 @@
package org.jetbrains.compose.web.attributes package org.jetbrains.compose.web.attributes
import androidx.compose.web.events.SyntheticDragEvent import androidx.compose.web.events.SyntheticDragEvent
import androidx.compose.web.events.SyntheticEvent
import androidx.compose.web.events.SyntheticMouseEvent import androidx.compose.web.events.SyntheticMouseEvent
import androidx.compose.web.events.SyntheticWheelEvent import androidx.compose.web.events.SyntheticWheelEvent
import org.jetbrains.compose.web.events.WrappedClipboardEvent import org.jetbrains.compose.web.events.*
import org.jetbrains.compose.web.events.WrappedEvent import org.w3c.dom.HTMLFormElement
import org.jetbrains.compose.web.events.WrappedFocusEvent import org.w3c.dom.events.EventTarget
import org.jetbrains.compose.web.events.WrappedInputEvent
import org.jetbrains.compose.web.events.WrappedKeyboardEvent
import org.jetbrains.compose.web.events.WrappedTouchEvent
import org.jetbrains.compose.web.events.GenericWrappedEvent
private typealias SyntheticMouseEventListener = (SyntheticMouseEvent) -> Unit private typealias SyntheticMouseEventListener = (SyntheticMouseEvent) -> Unit
private typealias SyntheticWheelEventListener = (SyntheticWheelEvent) -> Unit private typealias SyntheticWheelEventListener = (SyntheticWheelEvent) -> Unit
@ -17,225 +14,190 @@ private typealias SyntheticDragEventListener = (SyntheticDragEvent) -> Unit
open class EventsListenerBuilder { open class EventsListenerBuilder {
protected val listeners = mutableListOf<WrappedEventListener<*>>() protected val listeners = mutableListOf<SyntheticEventListener<*>>()
/* Mouse Events */ /* Mouse Events */
private fun createMouseEventListener(
name: String, options: Options, listener: SyntheticMouseEventListener
): MouseEventListener {
return MouseEventListener(
event = name,
options = options,
listener = {
listener(SyntheticMouseEvent(it.nativeEvent))
}
)
}
private fun createMouseWheelEventListener(
name: String, options: Options, listener: SyntheticWheelEventListener
): MouseWheelEventListener {
return MouseWheelEventListener(
event = name,
options = options,
listener = {
listener(SyntheticWheelEvent(it.nativeEvent))
}
)
}
fun onContextMenu(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onContextMenu(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(CONTEXTMENU, options, listener)) listeners.add(MouseEventListener(CONTEXTMENU, options, listener))
} }
fun onClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(CLICK, options, listener)) listeners.add(MouseEventListener(CLICK, options, listener))
} }
fun onDoubleClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onDoubleClick(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(DBLCLICK, options, listener)) listeners.add(MouseEventListener(DBLCLICK, options, listener))
} }
fun onMouseDown(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseDown(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEDOWN, options, listener)) listeners.add(MouseEventListener(MOUSEDOWN, options, listener))
} }
fun onMouseUp(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseUp(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEUP, options, listener)) listeners.add(MouseEventListener(MOUSEUP, options, listener))
} }
fun onMouseEnter(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseEnter(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEENTER, options, listener)) listeners.add(MouseEventListener(MOUSEENTER, options, listener))
} }
fun onMouseLeave(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseLeave(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSELEAVE, options, listener)) listeners.add(MouseEventListener(MOUSELEAVE, options, listener))
} }
fun onMouseMove(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseMove(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEMOVE, options, listener)) listeners.add(MouseEventListener(MOUSEMOVE, options, listener))
} }
fun onMouseOut(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseOut(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEOUT, options, listener)) listeners.add(MouseEventListener(MOUSEOUT, options, listener))
} }
fun onMouseOver(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) { fun onMouseOver(options: Options = Options.DEFAULT, listener: SyntheticMouseEventListener) {
listeners.add(createMouseEventListener(MOUSEOVER, options, listener)) listeners.add(MouseEventListener(MOUSEOVER, options, listener))
} }
fun onWheel(options: Options = Options.DEFAULT, listener: (SyntheticWheelEvent) -> Unit) { fun onWheel(options: Options = Options.DEFAULT, listener: SyntheticWheelEventListener) {
listeners.add(createMouseWheelEventListener(WHEEL, options, listener)) listeners.add(MouseWheelEventListener(WHEEL, options, listener))
} }
/* Drag Events */ /* 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) { fun onDrag(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAG, options, listener)) listeners.add(DragEventListener(DRAG, options, listener))
} }
fun onDrop(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDrop(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DROP, options, listener)) listeners.add(DragEventListener(DROP, options, listener))
} }
fun onDragStart(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDragStart(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGSTART, options, listener)) listeners.add(DragEventListener(DRAGSTART, options, listener))
} }
fun onDragEnd(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDragEnd(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGEND, options, listener)) listeners.add(DragEventListener(DRAGEND, options, listener))
} }
fun onDragOver(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDragOver(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGOVER, options, listener)) listeners.add(DragEventListener(DRAGOVER, options, listener))
} }
fun onDragEnter(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDragEnter(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGENTER, options, listener)) listeners.add(DragEventListener(DRAGENTER, options, listener))
} }
fun onDragLeave(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) { fun onDragLeave(options: Options = Options.DEFAULT, listener: SyntheticDragEventListener) {
listeners.add(createDragEventListener(DRAGLEAVE, options, listener)) listeners.add(DragEventListener(DRAGLEAVE, options, listener))
} }
/* End of Drag Events */ /* End of Drag Events */
fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { /* Clipboard Events */
fun onCopy(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(COPY, options, listener)) listeners.add(ClipboardEventListener(COPY, options, listener))
} }
fun onCut(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { fun onCut(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(CUT, options, listener)) listeners.add(ClipboardEventListener(CUT, options, listener))
} }
fun onPaste(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { fun onPaste(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(PASTE, options, listener)) listeners.add(ClipboardEventListener(PASTE, options, listener))
} }
fun onGenericInput( /* End of Clipboard Events */
options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit
) {
listeners.add(WrappedEventListener(INPUT, options, listener))
}
fun onChange(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { /* Keyboard Events */
listeners.add(WrappedEventListener(CHANGE, options, listener))
}
fun onInvalid(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { fun onKeyDown(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) {
listeners.add(WrappedEventListener(INVALID, options, listener)) listeners.add(KeyboardEventListener(KEYDOWN, options, listener))
} }
fun onSearch(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { fun onKeyUp(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) {
listeners.add(WrappedEventListener(SEARCH, options, listener)) listeners.add(KeyboardEventListener(KEYUP, options, listener))
} }
fun onFocus(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { /* End of Keyboard Events */
/* Focus Events */
fun onFocus(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
listeners.add(FocusEventListener(FOCUS, options, listener)) listeners.add(FocusEventListener(FOCUS, options, listener))
} }
fun onBlur(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { fun onBlur(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
listeners.add(FocusEventListener(BLUR, options, listener)) listeners.add(FocusEventListener(BLUR, options, listener))
} }
fun onFocusIn(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { fun onFocusIn(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
listeners.add(FocusEventListener(FOCUSIN, options, listener)) listeners.add(FocusEventListener(FOCUSIN, options, listener))
} }
fun onFocusOut(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) { fun onFocusOut(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
listeners.add(FocusEventListener(FOCUSOUT, options, listener)) listeners.add(FocusEventListener(FOCUSOUT, options, listener))
} }
fun onKeyDown(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) { /* End of Focus Events */
listeners.add(KeyboardEventListener(KEYDOWN, options, listener))
}
fun onKeyUp(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) {
listeners.add(KeyboardEventListener(KEYUP, options, listener))
}
fun onScroll(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(SCROLL, options, listener))
}
fun onSelect(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { /* Touch Events */
listeners.add(WrappedEventListener(SELECT, options, listener))
}
fun onTouchCancel(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { fun onTouchCancel(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
listeners.add(TouchEventListener(TOUCHCANCEL, options, listener)) listeners.add(TouchEventListener(TOUCHCANCEL, options, listener))
} }
fun onTouchMove(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { fun onTouchMove(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
listeners.add(TouchEventListener(TOUCHMOVE, options, listener)) listeners.add(TouchEventListener(TOUCHMOVE, options, listener))
} }
fun onTouchEnd(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { fun onTouchEnd(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
listeners.add(TouchEventListener(TOUCHEND, options, listener)) listeners.add(TouchEventListener(TOUCHEND, options, listener))
} }
fun onTouchStart(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { fun onTouchStart(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
listeners.add(TouchEventListener(TOUCHSTART, options, listener)) listeners.add(TouchEventListener(TOUCHSTART, options, listener))
} }
fun onAnimationEnd(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) { /* End of Touch Events */
listeners.add(WrappedEventListener(ANIMATIONEND, options, listener))
/* Animation Events */
fun onAnimationEnd(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) {
listeners.add(AnimationEventListener(ANIMATIONEND, options, listener))
} }
fun onAnimationIteration(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { fun onAnimationIteration(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) {
listeners.add(WrappedEventListener(ANIMATIONITERATION, options, listener)) listeners.add(AnimationEventListener(ANIMATIONITERATION, options, listener))
} }
fun onAnimationStart(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { fun onAnimationStart(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) {
listeners.add(WrappedEventListener(ANIMATIONSTART, options, listener)) listeners.add(AnimationEventListener(ANIMATIONSTART, options, listener))
} }
fun onBeforeInput(options: Options = Options.DEFAULT, listener: (WrappedInputEvent) -> Unit) { /* End of Animation Events */
listeners.add(InputEventListener(BEFOREINPUT, options, listener))
fun onScroll(options: Options = Options.DEFAULT, listener: (SyntheticEvent<EventTarget>) -> Unit) {
listeners.add(SyntheticEventListener(SCROLL, options, listener))
} }
fun collectListeners(): List<WrappedEventListener<*>> = listeners fun collectListeners(): List<SyntheticEventListener<*>> = listeners
fun <T : SyntheticEvent<out EventTarget>> addEventListener(
eventName: String,
options: Options = Options.DEFAULT,
listener: (T) -> Unit
) {
listeners.add(SyntheticEventListener(eventName, options, listener))
}
fun addEventListener( fun addEventListener(
eventName: String, eventName: String,
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (WrappedEvent) -> Unit listener: (SyntheticEvent<EventTarget>) -> Unit
) { ) {
listeners.add(WrappedEventListener(eventName, options, listener)) listeners.add(SyntheticEventListener(eventName, options, listener))
} }
internal fun copyListenersFrom(from: EventsListenerBuilder) { internal fun copyListenersFrom(from: EventsListenerBuilder) {
@ -282,7 +244,6 @@ open class EventsListenerBuilder {
const val INPUT = "input" const val INPUT = "input"
const val CHANGE = "change" const val CHANGE = "change"
const val INVALID = "invalid" const val INVALID = "invalid"
const val SEARCH = "search"
const val DRAG = "drag" const val DRAG = "drag"
const val DROP = "drop" const val DROP = "drop"
@ -291,5 +252,8 @@ open class EventsListenerBuilder {
const val DRAGOVER = "dragover" const val DRAGOVER = "dragover"
const val DRAGENTER = "dragenter" const val DRAGENTER = "dragenter"
const val DRAGLEAVE = "dragleave" const val DRAGLEAVE = "dragleave"
const val SUBMIT = "submit"
const val RESET = "reset"
} }
} }

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

@ -1,33 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
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.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticInputEvent<ValueType, Element : EventTarget>(
val value: ValueType,
nativeEvent: Event
) : SyntheticEvent<Element>(
nativeEvent = nativeEvent
)
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))
}
}
}

156
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt

@ -0,0 +1,156 @@
package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.events.SyntheticInputEvent
import androidx.compose.web.events.SyntheticDragEvent
import androidx.compose.web.events.SyntheticEvent
import androidx.compose.web.events.SyntheticMouseEvent
import androidx.compose.web.events.SyntheticWheelEvent
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHANGE
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.SELECT
import org.jetbrains.compose.web.events.*
import org.w3c.dom.DragEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.*
open class SyntheticEventListener<T : SyntheticEvent<*>> internal constructor(
val event: String,
val options: Options,
val listener: (T) -> Unit
) : EventListener {
@Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) {
listener(SyntheticEvent<EventTarget>(event).unsafeCast<T>())
}
}
class Options {
// TODO: add options for addEventListener
companion object {
val DEFAULT = Options()
}
}
internal class AnimationEventListener(
event: String,
options: Options,
listener: (SyntheticAnimationEvent) -> Unit
) : SyntheticEventListener<SyntheticAnimationEvent>(
event, options, listener
) {
override fun handleEvent(event: Event) {
listener(SyntheticAnimationEvent(event, event.unsafeCast<AnimationEventDetails>()))
}
}
internal class MouseEventListener(
event: String,
options: Options,
listener: (SyntheticMouseEvent) -> Unit
) : SyntheticEventListener<SyntheticMouseEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticMouseEvent(event.unsafeCast<MouseEvent>()))
}
}
internal class MouseWheelEventListener(
event: String,
options: Options,
listener: (SyntheticWheelEvent) -> Unit
) : SyntheticEventListener<SyntheticWheelEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticWheelEvent(event.unsafeCast<WheelEvent>()))
}
}
internal class KeyboardEventListener(
event: String,
options: Options,
listener: (SyntheticKeyboardEvent) -> Unit
) : SyntheticEventListener<SyntheticKeyboardEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticKeyboardEvent(event.unsafeCast<KeyboardEvent>()))
}
}
internal class FocusEventListener(
event: String,
options: Options,
listener: (SyntheticFocusEvent) -> Unit
) : SyntheticEventListener<SyntheticFocusEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticFocusEvent(event.unsafeCast<FocusEvent>()))
}
}
internal class TouchEventListener(
event: String,
options: Options,
listener: (SyntheticTouchEvent) -> Unit
) : SyntheticEventListener<SyntheticTouchEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticTouchEvent(event.unsafeCast<TouchEvent>()))
}
}
internal class DragEventListener(
event: String,
options: Options,
listener: (SyntheticDragEvent) -> Unit
) : SyntheticEventListener<SyntheticDragEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticDragEvent(event.unsafeCast<DragEvent>()))
}
}
internal class ClipboardEventListener(
event: String,
options: Options,
listener: (SyntheticClipboardEvent) -> Unit
) : SyntheticEventListener<SyntheticClipboardEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(SyntheticClipboardEvent(event.unsafeCast<ClipboardEvent>()))
}
}
internal class InputEventListener<InputValueType, Target: EventTarget>(
eventName: String = INPUT,
options: Options,
val inputType: InputType<InputValueType>,
listener: (SyntheticInputEvent<InputValueType, Target>) -> Unit
) : SyntheticEventListener<SyntheticInputEvent<InputValueType, Target>>(
eventName, options, listener
) {
override fun handleEvent(event: Event) {
val value = inputType.inputValue(event)
listener(SyntheticInputEvent(value, event))
}
}
internal class ChangeEventListener<InputValueType, Target: EventTarget>(
options: Options,
val inputType: InputType<InputValueType>,
listener: (SyntheticChangeEvent<InputValueType, Target>) -> Unit
) : SyntheticEventListener<SyntheticChangeEvent<InputValueType, Target>>(
CHANGE, options, listener
) {
override fun handleEvent(event: Event) {
val value = inputType.inputValue(event)
listener(SyntheticChangeEvent(value, event))
}
}
internal class SelectEventListener<Target: EventTarget>(
options: Options,
listener: (SyntheticSelectEvent<Target>) -> Unit
) : SyntheticEventListener<SyntheticSelectEvent<Target>>(
SELECT, options, listener
) {
override fun handleEvent(event: Event) {
listener(SyntheticSelectEvent(event, event.target.unsafeCast<SelectionInfoDetails>()))
}
}

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

@ -1,23 +0,0 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.web.attributes
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.Options
import org.w3c.dom.HTMLTextAreaElement
class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() {
fun onInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit
) {
addEventListener(INPUT, options) {
val text = it.nativeEvent.target.asDynamic().value.unsafeCast<String>()
listener(SyntheticInputEvent(text, it.nativeEvent))
}
}
}

168
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/WrappedEventListener.kt

@ -1,168 +0,0 @@
package org.jetbrains.compose.web.attributes
import org.jetbrains.compose.web.events.GenericWrappedEvent
import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent
import org.jetbrains.compose.web.events.WrappedClipboardEvent
import org.jetbrains.compose.web.events.WrappedDragEvent
import org.jetbrains.compose.web.events.WrappedEventImpl
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.WrappedPointerEvent
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.w3c.dom.DragEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.Event
import org.w3c.dom.events.FocusEvent
import org.w3c.dom.events.InputEvent
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
import org.w3c.dom.pointerevents.PointerEvent
open class WrappedEventListener<T : GenericWrappedEvent<*>>(
val event: String,
val options: Options,
val listener: (T) -> Unit
) : org.w3c.dom.events.EventListener {
@Suppress("UNCHECKED_CAST")
override fun handleEvent(event: Event) {
listener(WrappedEventImpl(event) as T)
}
}
class Options {
// TODO: add options for addEventListener
companion object {
val DEFAULT = Options()
}
}
internal class MouseEventListener(
event: String,
options: Options,
listener: (WrappedMouseEvent) -> Unit
) : WrappedEventListener<WrappedMouseEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedMouseEvent(event as MouseEvent))
}
}
internal class MouseWheelEventListener(
event: String,
options: Options,
listener: (WrappedWheelEvent) -> Unit
) : WrappedEventListener<WrappedWheelEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedWheelEvent(event as WheelEvent))
}
}
internal class KeyboardEventListener(
event: String,
options: Options,
listener: (WrappedKeyboardEvent) -> Unit
) : WrappedEventListener<WrappedKeyboardEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedKeyboardEvent(event as KeyboardEvent))
}
}
internal class FocusEventListener(
event: String,
options: Options,
listener: (WrappedFocusEvent) -> Unit
) : WrappedEventListener<WrappedFocusEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedFocusEvent(event as FocusEvent))
}
}
internal class TouchEventListener(
event: String,
options: Options,
listener: (WrappedTouchEvent) -> Unit
) : WrappedEventListener<WrappedTouchEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedTouchEvent(event as TouchEvent))
}
}
internal class DragEventListener(
event: String,
options: Options,
listener: (WrappedDragEvent) -> Unit
) : WrappedEventListener<WrappedDragEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedDragEvent(event as DragEvent))
}
}
internal class PointerEventListener(
event: String,
options: Options,
listener: (WrappedPointerEvent) -> Unit
) : WrappedEventListener<WrappedPointerEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedPointerEvent(event as PointerEvent))
}
}
internal class ClipboardEventListener(
event: String,
options: Options,
listener: (WrappedClipboardEvent) -> Unit
) : WrappedEventListener<WrappedClipboardEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedClipboardEvent(event as ClipboardEvent))
}
}
internal class InputEventListener(
event: String,
options: Options,
listener: (WrappedInputEvent) -> Unit
) : WrappedEventListener<WrappedInputEvent>(event, options, listener) {
override fun handleEvent(event: Event) {
listener(WrappedInputEvent(event as InputEvent))
}
}
internal class RadioInputEventListener(
options: Options,
listener: (WrappedRadioInputEvent) -> Unit
) : WrappedEventListener<WrappedRadioInputEvent>(EventsListenerBuilder.INPUT, options, listener) {
override fun handleEvent(event: Event) {
val checked = event.target.asDynamic().checked as Boolean
listener(WrappedRadioInputEvent(event, checked))
}
}
internal class CheckBoxInputEventListener(
options: Options,
listener: (WrappedCheckBoxInputEvent) -> Unit
) : WrappedEventListener<WrappedCheckBoxInputEvent>(
EventsListenerBuilder.INPUT, options, listener
) {
override fun handleEvent(event: Event) {
val checked = event.target.asDynamic().checked as Boolean
listener(WrappedCheckBoxInputEvent(event, checked))
}
}
internal class TextInputEventListener(
options: Options,
listener: (WrappedTextInputEvent) -> Unit
) : WrappedEventListener<WrappedTextInputEvent>(EventsListenerBuilder.INPUT, options, listener) {
override fun handleEvent(event: Event) {
val text = event.target.asDynamic().value as String
listener(WrappedTextInputEvent(event as InputEvent, text))
}
}

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

@ -0,0 +1,53 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.web.attributes.builders
import androidx.compose.web.events.SyntheticEvent
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.events.SyntheticChangeEvent
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.jetbrains.compose.web.events.SyntheticSelectEvent
import org.w3c.dom.HTMLInputElement
class InputAttrsBuilder<ValueType>(
val inputType: InputType<ValueType>
) : AttrsBuilder<HTMLInputElement>() {
fun onInvalid(
options: Options = Options.DEFAULT,
listener: (SyntheticEvent<HTMLInputElement>) -> Unit
) {
addEventListener(INVALID, options, listener)
}
fun onInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<ValueType, HTMLInputElement>) -> Unit
) {
listeners.add(InputEventListener(eventName = INPUT, options, inputType, listener))
}
fun onChange(
options: Options = Options.DEFAULT,
listener: (SyntheticChangeEvent<ValueType, HTMLInputElement>) -> Unit
) {
listeners.add(ChangeEventListener(options, inputType, listener))
}
fun onBeforeInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<ValueType, HTMLInputElement>) -> Unit
) {
listeners.add(InputEventListener(eventName = BEFOREINPUT, options, inputType, listener))
}
fun onSelect(
options: Options = Options.DEFAULT,
listener: (SyntheticSelectEvent<HTMLInputElement>) -> Unit
) {
listeners.add(SelectEventListener(options, listener))
}
}

58
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt

@ -0,0 +1,58 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package androidx.compose.web.attributes
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.CHANGE
import org.jetbrains.compose.web.attributes.EventsListenerBuilder.Companion.INPUT
import org.jetbrains.compose.web.attributes.Options
import org.jetbrains.compose.web.attributes.SyntheticEventListener
import org.jetbrains.compose.web.events.SyntheticChangeEvent
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.events.Event
class SelectAttrsBuilder : AttrsBuilder<HTMLSelectElement>() {
fun onInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String?, HTMLSelectElement>) -> Unit
) {
listeners.add(SelectInputEventListener(INPUT, options, listener))
}
fun onChange(
options: Options = Options.DEFAULT,
listener: (SyntheticChangeEvent<String?, HTMLSelectElement>) -> Unit
) {
listeners.add(SelectChangeEventListener(options, listener))
}
}
private class SelectInputEventListener(
eventName: String = INPUT,
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String?, HTMLSelectElement>) -> Unit
) : SyntheticEventListener<SyntheticInputEvent<String?, HTMLSelectElement>>(
eventName, options, listener
) {
override fun handleEvent(event: Event) {
val value = event.target?.asDynamic().value?.toString()
listener(SyntheticInputEvent(value, event))
}
}
private class SelectChangeEventListener(
options: Options = Options.DEFAULT,
listener: (SyntheticChangeEvent<String?, HTMLSelectElement>) -> Unit
): SyntheticEventListener<SyntheticChangeEvent<String?, HTMLSelectElement>>(
CHANGE, options, listener
) {
override fun handleEvent(event: Event) {
val value = event.target?.asDynamic().value?.toString()
listener(SyntheticChangeEvent(value, event))
}
}

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

@ -0,0 +1,43 @@
/*
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package org.jetbrains.compose.web.attributes.builders
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.events.SyntheticChangeEvent
import org.jetbrains.compose.web.events.SyntheticSelectEvent
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.w3c.dom.HTMLTextAreaElement
class TextAreaAttrsBuilder : AttrsBuilder<HTMLTextAreaElement>() {
fun onInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit
) {
listeners.add(InputEventListener(INPUT, options, InputType.Text, listener))
}
fun onChange(
options: Options = Options.DEFAULT,
listener: (SyntheticChangeEvent<String, HTMLTextAreaElement>) -> Unit
) {
listeners.add(ChangeEventListener(options, InputType.Text, listener))
}
fun onBeforeInput(
options: Options = Options.DEFAULT,
listener: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit
) {
listeners.add(InputEventListener(BEFOREINPUT, options, InputType.Text, listener))
}
fun onSelect(
options: Options = Options.DEFAULT,
listener: (SyntheticSelectEvent<HTMLTextAreaElement>) -> Unit
) {
listeners.add(SelectEventListener(options, listener))
}
}

19
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt

@ -2,8 +2,9 @@ package org.jetbrains.compose.web.dom
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.ComposeNode
import androidx.compose.web.attributes.InputAttrsBuilder import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder
import androidx.compose.web.attributes.TextAreaAttrsBuilder import androidx.compose.web.attributes.SelectAttrsBuilder
import org.jetbrains.compose.web.attributes.builders.TextAreaAttrsBuilder
import org.jetbrains.compose.web.DomApplier import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomNodeWrapper import org.jetbrains.compose.web.DomNodeWrapper
import kotlinx.browser.document import kotlinx.browser.document
@ -595,11 +596,21 @@ fun Form(
@Composable @Composable
fun Select( fun Select(
attrs: AttrBuilderContext<HTMLSelectElement>? = null, attrs: (SelectAttrsBuilder.() -> Unit)? = null,
multiple: Boolean = false,
content: ContentBuilder<HTMLSelectElement>? = null content: ContentBuilder<HTMLSelectElement>? = null
) = TagElement( ) = TagElement(
elementBuilder = Select, elementBuilder = Select,
applyAttrs = attrs, applyAttrs = {
if (multiple) multiple()
if (attrs != null) {
val selectAttrsBuilder = with(SelectAttrsBuilder()) {
attrs()
this
}
copyFrom(selectAttrsBuilder)
}
},
content = content content = content
) )

78
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt

@ -2,63 +2,63 @@ package org.jetbrains.compose.web.dom
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.web.attributes.InputAttrsBuilder import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder
import org.jetbrains.compose.web.attributes.* import org.jetbrains.compose.web.attributes.*
private fun InputAttrsBuilder<String>.applyAttrsWithStringValue( private fun InputAttrsBuilder<String>.applyAttrsWithStringValue(
value: String, value: String,
attrsBuilder: InputAttrsBuilder<String>.() -> Unit attrs: InputAttrsBuilder<String>.() -> Unit
) { ) {
value(value) value(value)
attrsBuilder() attrs()
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) { fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input( Input(
type = InputType.Checkbox, type = InputType.Checkbox,
attrs = { attrs = {
if (checked) checked() if (checked) checked()
this.attrsBuilder() this.attrs()
} }
) )
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun DateInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun EmailInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun FileInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun HiddenInput(attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun HiddenInput(attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Hidden, attrs = attrsBuilder) Input(type = InputType.Hidden, attrs = attrs)
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun MonthInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@ -67,7 +67,7 @@ fun NumberInput(
value: Number? = null, value: Number? = null,
min: Number? = null, min: Number? = null,
max: Number? = null, max: Number? = null,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {} attrs: InputAttrsBuilder<Number?>.() -> Unit = {}
) { ) {
Input( Input(
type = InputType.Number, type = InputType.Number,
@ -75,25 +75,25 @@ fun NumberInput(
if (value != null) value(value.toString()) if (value != null) value(value.toString())
if (min != null) min(min.toString()) if (min != null) min(min.toString())
if (max != null) max(max.toString()) if (max != null) max(max.toString())
attrsBuilder() attrs()
} }
) )
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun PasswordInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun PasswordInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) { fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input( Input(
type = InputType.Radio, type = InputType.Radio,
attrs = { attrs = {
if (checked) checked() if (checked) checked()
attrsBuilder() attrs()
} }
) )
} }
@ -105,7 +105,7 @@ fun RangeInput(
min: Number? = null, min: Number? = null,
max: Number? = null, max: Number? = null,
step: Number = 1, step: Number = 1,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {} attrs: InputAttrsBuilder<Number?>.() -> Unit = {}
) { ) {
Input( Input(
type = InputType.Range, type = InputType.Range,
@ -114,49 +114,49 @@ fun RangeInput(
if (min != null) min(min.toString()) if (min != null) min(min.toString())
if (max != null) max(max.toString()) if (max != null) max(max.toString())
step(step) step(step)
attrsBuilder() attrs()
} }
) )
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun SearchInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun SearchInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun SubmitInput(attrsBuilder: InputAttrsBuilder<Unit>.() -> Unit = {}) { fun SubmitInput(attrs: InputAttrsBuilder<Unit>.() -> Unit = {}) {
Input(type = InputType.Submit, attrs = attrsBuilder) Input(type = InputType.Submit, attrs = attrs)
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun TelInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun TextInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun TimeInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun UrlInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrs) })
} }
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) { fun WeekInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrs) })
} }

21
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticAnimationEvent.kt

@ -0,0 +1,21 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticAnimationEvent internal constructor(
nativeEvent: Event,
animationEventDetails: AnimationEventDetails
) : SyntheticEvent<EventTarget>(nativeEvent) {
val animationName: String = animationEventDetails.animationName
val elapsedTime: Number = animationEventDetails.elapsedTime
val pseudoElement: String = animationEventDetails.pseudoElement
}
internal external interface AnimationEventDetails {
val animationName: String
val elapsedTime: Number
val pseudoElement: String
}

11
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticChangeEvent.kt

@ -0,0 +1,11 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticChangeEvent<Value, Element : EventTarget> internal constructor(
val value: Value,
nativeEvent: Event,
) : SyntheticEvent<Element>(nativeEvent)

21
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticClipboardEvent.kt

@ -0,0 +1,21 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.DataTransfer
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.EventTarget
class SyntheticClipboardEvent internal constructor(
nativeEvent: ClipboardEvent
) : SyntheticEvent<EventTarget>(nativeEvent) {
val clipboardData: DataTransfer? = nativeEvent.clipboardData
fun getData(format: String): String? {
return clipboardData?.getData(format)
}
fun setData(format: String, data: String) {
clipboardData?.setData(format, data)
}
}

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

@ -3,7 +3,7 @@ package androidx.compose.web.events
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget import org.w3c.dom.events.EventTarget
open class SyntheticEvent<Element : EventTarget>( open class SyntheticEvent<Element : EventTarget> internal constructor(
val nativeEvent: Event val nativeEvent: Event
) { ) {
val target: Element = nativeEvent.target.unsafeCast<Element>() val target: Element = nativeEvent.target.unsafeCast<Element>()

12
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticFocusEvent.kt

@ -0,0 +1,12 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.events.EventTarget
import org.w3c.dom.events.FocusEvent
class SyntheticFocusEvent internal constructor(
nativeEvent: FocusEvent,
) : SyntheticEvent<EventTarget>(nativeEvent) {
val relatedTarget: EventTarget? = nativeEvent.relatedTarget
}

20
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticInputEvent.kt

@ -0,0 +1,20 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.DataTransfer
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
// @param nativeEvent: Event - we don't use [org.w3c.dom.events.InputEvent] here,
// since for cases it can be just [org.w3c.dom.events.Event]
class SyntheticInputEvent<ValueType, Element : EventTarget> internal constructor(
val value: ValueType,
nativeEvent: Event
) : SyntheticEvent<Element>(
nativeEvent = nativeEvent
) {
val data: String? = nativeEvent.asDynamic().data?.unsafeCast<String>()
val dataTransfer: DataTransfer? = nativeEvent.asDynamic().dataTransfer?.unsafeCast<DataTransfer>()
val inputType: String? = nativeEvent.asDynamic().inputType?.unsafeCast<String>()
val isComposing: Boolean = nativeEvent.asDynamic().isComposing?.unsafeCast<Boolean>() ?: false
}

44
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticKeyboardEvent.kt

@ -0,0 +1,44 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.events.EventTarget
import org.w3c.dom.events.KeyboardEvent
class SyntheticKeyboardEvent internal constructor(
nativeEvent: KeyboardEvent
) : SyntheticEvent<EventTarget>(nativeEvent) {
private val keyboardEvent = nativeEvent
val altKey: Boolean = nativeEvent.altKey
val code: String = nativeEvent.code
val ctrlKey: Boolean = nativeEvent.ctrlKey
val isComposing: Boolean = nativeEvent.isComposing
val key: String = nativeEvent.key
val locale: String = nativeEvent.asDynamic().locale.toString()
val location: Int = nativeEvent.location
val metaKey: Boolean = nativeEvent.metaKey
val repeat: Boolean = nativeEvent.repeat
val shiftKey: Boolean = nativeEvent.shiftKey
fun getModifierState(keyArg: String): Boolean = keyboardEvent.getModifierState(keyArg)
fun getNormalizedKey(): String = key.let {
normalizedKeys[it] ?: it
}
}
private val normalizedKeys = mapOf(
"Esc" to "Escape",
"Spacebar" to " ",
"Left" to "ArrowLeft",
"Up" to "ArrowUp",
"Right" to "ArrowRight",
"Down" to "ArrowDown",
"Del" to "Delete",
"Apps" to "ContextMenu",
"Menu" to "ContextMenu",
"Scroll" to "ScrollLock",
"MozPrintableKey" to "Unidentified",
)
// Firefox bug for Windows key https://bugzilla.mozilla.org/show_bug.cgi?id=1232918

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

@ -10,7 +10,7 @@ import org.w3c.dom.events.WheelEvent
/** /**
* https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
*/ */
open class SyntheticMouseEvent( open class SyntheticMouseEvent internal constructor(
nativeEvent: MouseEvent nativeEvent: MouseEvent
) : SyntheticEvent<EventTarget>(nativeEvent) { ) : SyntheticEvent<EventTarget>(nativeEvent) {

26
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSelectEvent.kt

@ -0,0 +1,26 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticSelectEvent<Element : EventTarget> internal constructor(
nativeEvent: Event,
selectionInfoDetails: SelectionInfoDetails
) : SyntheticEvent<Element>(nativeEvent) {
val selectionStart: Int = selectionInfoDetails.selectionStart
val selectionEnd: Int = selectionInfoDetails.selectionEnd
fun selection(): String {
return nativeEvent.target.asDynamic().value.unsafeCast<String?>()?.substring(
selectionStart, selectionEnd
) ?: ""
}
}
internal external interface SelectionInfoDetails {
val selectionStart: Int
val selectionEnd: Int
}

9
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSubmitEvent.kt

@ -0,0 +1,9 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticSubmitEvent internal constructor(
nativeEvent: Event
) : SyntheticEvent<EventTarget>(nativeEvent)

18
web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticTouchEvent.kt

@ -0,0 +1,18 @@
package org.jetbrains.compose.web.events
import androidx.compose.web.events.SyntheticEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.TouchList
import org.w3c.dom.events.EventTarget
class SyntheticTouchEvent(
nativeEvent: TouchEvent,
) : SyntheticEvent<EventTarget>(nativeEvent) {
val altKey: Boolean = nativeEvent.altKey
val changedTouches: TouchList = nativeEvent.changedTouches
val ctrlKey: Boolean = nativeEvent.ctrlKey
val metaKey: Boolean = nativeEvent.metaKey
val shiftKey: Boolean = nativeEvent.shiftKey
val touches: TouchList = nativeEvent.touches
}

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

@ -1,107 +0,0 @@
package org.jetbrains.compose.web.events
import org.w3c.dom.DragEvent
import org.w3c.dom.TouchEvent
import org.w3c.dom.clipboard.ClipboardEvent
import org.w3c.dom.events.CompositionEvent
import org.w3c.dom.events.Event
import org.w3c.dom.events.FocusEvent
import org.w3c.dom.events.InputEvent
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.MouseEvent
import org.w3c.dom.events.WheelEvent
import org.w3c.dom.pointerevents.PointerEvent
interface GenericWrappedEvent<T : Event> {
val nativeEvent: T
}
interface WrappedEvent : GenericWrappedEvent<Event>
internal open class WrappedMouseEvent(
override val nativeEvent: MouseEvent
) : GenericWrappedEvent<MouseEvent> {
// MouseEvent doesn't support movementX and movementY on IE6-11, and it's OK for now.
val movementX: Double
get() = nativeEvent.asDynamic().movementX as Double
val movementY: Double
get() = nativeEvent.asDynamic().movementY as Double
}
internal open class WrappedWheelEvent(
override val nativeEvent: WheelEvent
) : GenericWrappedEvent<WheelEvent>
open class WrappedInputEvent(
override val nativeEvent: Event
) : GenericWrappedEvent<Event>
open class WrappedKeyboardEvent(
override val nativeEvent: KeyboardEvent
) : GenericWrappedEvent<KeyboardEvent> {
fun getNormalizedKey(): String = nativeEvent.key.let {
normalizedKeys[it] ?: it
}
companion object {
private val normalizedKeys = mapOf(
"Esc" to "Escape",
"Spacebar" to " ",
"Left" to "ArrowLeft",
"Up" to "ArrowUp",
"Right" to "ArrowRight",
"Down" to "ArrowDown",
"Del" to "Delete",
"Apps" to "ContextMenu",
"Menu" to "ContextMenu",
"Scroll" to "ScrollLock",
"MozPrintableKey" to "Unidentified",
)
// Firefox bug for Windows key https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
}
}
open class WrappedFocusEvent(
override val nativeEvent: FocusEvent
) : GenericWrappedEvent<FocusEvent>
open class WrappedTouchEvent(
override val nativeEvent: TouchEvent
) : GenericWrappedEvent<TouchEvent>
open class WrappedCompositionEvent(
override val nativeEvent: CompositionEvent
) : GenericWrappedEvent<CompositionEvent>
open class WrappedDragEvent(
override val nativeEvent: DragEvent
) : GenericWrappedEvent<DragEvent>
open class WrappedPointerEvent(
override val nativeEvent: PointerEvent
) : GenericWrappedEvent<PointerEvent>
open class WrappedClipboardEvent(
override val nativeEvent: ClipboardEvent
) : GenericWrappedEvent<ClipboardEvent>
class WrappedTextInputEvent(
nativeEvent: Event,
val inputValue: String
) : WrappedInputEvent(nativeEvent)
class WrappedCheckBoxInputEvent(
override val nativeEvent: Event,
val checked: Boolean
) : GenericWrappedEvent<Event>
class WrappedRadioInputEvent(
override val nativeEvent: Event,
val checked: Boolean
) : GenericWrappedEvent<Event>
class WrappedEventImpl(
override val nativeEvent: Event
) : WrappedEvent

3
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt

@ -1,6 +1,7 @@
package org.jetbrains.compose.web.sample.tests package org.jetbrains.compose.web.sample.tests
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.web.sample.tests.SelectElementTests
import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.renderComposableInBody import org.jetbrains.compose.web.renderComposableInBody
@ -32,7 +33,7 @@ internal val testCases = mutableMapOf<String, TestCase>()
fun launchTestCase(testCaseId: String) { fun launchTestCase(testCaseId: String) {
// this makes test cases get initialised: // this makes test cases get initialised:
listOf<Any>(TestCases1(), InputsTests(), EventsTests()) listOf<Any>(TestCases1(), InputsTests(), EventsTests(), SelectElementTests())
if (testCaseId !in testCases) error("Test Case '$testCaseId' not found") if (testCaseId !in testCases) error("Test Case '$testCaseId' not found")

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

@ -97,7 +97,23 @@ class EventsTests {
value("This is a text to be selected") value("This is a text to be selected")
id("selectableText") id("selectableText")
onSelect { onSelect {
state = "Text Selected" state = it.selection()
}
})
}
val selectEventInTextAreaUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
var selectedIndexes by remember { mutableStateOf("None") }
P(attrs = { style { height(50.px) } }) { TestText(state) }
P(attrs = { style { height(50.px) } }) { TestText(selectedIndexes, id = "txt2") }
TextArea(value = "This is a text to be selected", attrs = {
id("textArea")
onSelect {
state = it.selection()
selectedIndexes = "${it.selectionStart},${it.selectionEnd}"
} }
}) })
} }
@ -174,4 +190,220 @@ class EventsTests {
} }
) { TestText(state) } ) { TestText(state) }
} }
val copyPasteEventsTest by testCase {
var state by remember { mutableStateOf("None") }
P {
TestText(state)
}
Div(attrs = {
onCopy {
it.preventDefault()
it.setData("text", "COPIED_TEXT_WAS_OVERRIDDEN")
}
}) {
TestText("SomeTestTextToCopy1", id = "txt_to_copy")
}
Div {
TestText("SomeTestTextToCopy2", id = "txt_to_copy2")
}
TextInput(value = state) {
id("textinput")
onPaste {
state = it.getData("text")?.lowercase() ?: "None"
}
}
}
val cutPasteEventsTest by testCase {
var state by remember { mutableStateOf("None") }
var stateToCut by remember { mutableStateOf("TextToCut") }
P {
TestText(state)
}
TextInput(value = stateToCut) {
id("textinput1")
onCut {
state = "Text was cut"
stateToCut = ""
}
}
TextInput(value = state) {
id("textinput2")
onPaste {
state = "Modified pasted text = ${it.getData("text")}"
}
}
}
val keyDownKeyUpTest by testCase {
var stateDown by remember { mutableStateOf("None") }
var stateUp by remember { mutableStateOf("None") }
P {
TestText(stateDown, id = "txt_down")
}
P {
TestText(stateUp, id = "txt_up")
}
TextInput(value = "") {
id("textinput")
onKeyDown {
stateDown = "keydown = ${it.key}"
it.preventDefault()
}
onKeyUp {
stateUp = "keyup = ${it.key}"
it.preventDefault()
}
}
}
val touchEventsDispatched by testCase {
var touchStart by remember { mutableStateOf("None") }
var touchMove by remember { mutableStateOf("None") }
var touchEnd by remember { mutableStateOf("None") }
P {
TestText(touchStart, id = "txt_start")
}
P {
TestText(touchMove, id = "txt_move")
}
P {
TestText(touchEnd, id = "txt_end")
}
Div(attrs = {
id("box")
onTouchStart {
touchStart = "STARTED"
}
onTouchMove {
touchMove = "MOVED"
}
onTouchEnd {
touchEnd = "ENDED"
}
style {
width(300.px)
height(300.px)
backgroundColor("red")
}
}) {
Text("Touch me and move the pointer")
}
}
val animationEventsDispatched by testCase {
var animationStart by remember { mutableStateOf("None") }
var animationEnd by remember { mutableStateOf("None") }
var shouldAddBounceClass by remember { mutableStateOf(false) }
Style(AppStyleSheetWithAnimation)
P {
TestText(value = animationStart, id = "txt_start")
}
P {
TestText(value = animationEnd, id = "txt_end")
}
Div(attrs = {
id("box")
if (shouldAddBounceClass) classes(AppStyleSheetWithAnimation.bounceClass)
onClick {
shouldAddBounceClass = true
}
onAnimationStart {
animationStart = "STARTED - ${it.animationName}"
}
onAnimationEnd {
shouldAddBounceClass = false
animationEnd = "ENDED"
}
style {
backgroundColor("red")
}
}) {
Text("Click to Animate")
}
}
val onSubmitEventForFormDispatched by testCase {
var state by remember { mutableStateOf("None") }
P { TestText(value = state) }
Form(action = "#", attrs = {
onSubmit {
it.preventDefault()
state = "Form submitted"
}
}) {
Button(attrs = {
id("send_form_btn")
type(ButtonType.Submit)
}) {
Text("Send Form")
}
}
}
val onResetEventForFormDispatched by testCase {
var state by remember { mutableStateOf("None") }
P { TestText(value = state) }
Form(action = "#", attrs = {
onReset {
it.preventDefault()
state = "Form reset"
}
}) {
Button(attrs = {
id("reset_form_btn")
type(ButtonType.Reset)
}) {
Text("Send Form")
}
}
}
}
object AppStyleSheetWithAnimation : StyleSheet() {
val bounce by keyframes {
from {
property("transform", "translateX(50%)")
}
to {
property("transform", "translateX(-50%)")
}
}
val bounceClass by style {
color("green")
animation(bounce) {
duration(500.ms)
timingFunction(AnimationTimingFunction.EaseIn)
direction(AnimationDirection.Alternate)
}
}
} }

57
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt

@ -216,7 +216,62 @@ class InputsTests {
Div { Div {
Input(type = InputType.Number, attrs = { Input(type = InputType.Number, attrs = {
id("numberInput") id("numberInput")
onChange { state = "INPUT HAS CHANGED" } onChange { state = it.value!!.toString() }
})
}
}
val changeEventInTextAreaUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P { TestText(state) }
Div {
TextArea(value = state, attrs = {
id("textArea")
onChange { state = it.value }
})
}
}
val beforeInputEventUpdatesText by testCase {
var inputState by remember { mutableStateOf("") }
var state by remember { mutableStateOf("None") }
P { TestText(state) }
P { TestText(inputState, id = "txt2") }
Div {
TextInput(value = "", attrs = {
id("textInput")
onBeforeInput {
state = it.data ?: ""
}
onInput {
inputState = it.value
}
})
}
}
val beforeInputEventInTextAreaUpdatesText by testCase {
var inputState by remember { mutableStateOf("") }
var state by remember { mutableStateOf("None") }
P { TestText(state) }
P { TestText(inputState, id = "txt2") }
Div {
TextArea(value = "", attrs = {
id("textArea")
onBeforeInput {
state = it.data ?: ""
}
onInput {
inputState = it.value
}
}) })
} }
} }

75
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/SelectElementTests.kt

@ -0,0 +1,75 @@
package androidx.compose.web.sample.tests
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.events.SyntheticChangeEvent
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.jetbrains.compose.web.sample.tests.TestText
import org.jetbrains.compose.web.sample.tests.testCase
import org.w3c.dom.HTMLSelectElement
import org.w3c.dom.asList
class SelectElementTests {
@Composable
private fun ChooseAPetSelect(
canSelectMultiple: Boolean = false,
onInput: (SyntheticInputEvent<String?, HTMLSelectElement>) -> Unit,
onChange: (SyntheticChangeEvent<String?, HTMLSelectElement>) -> Unit
) {
Label(forId = "pet-select") {
Text("Choose a pet:")
}
Select(multiple = canSelectMultiple, attrs = {
name("pets")
id("pet-select")
onInput { onInput(it) }
onChange { onChange(it) }
}) {
Option(value = "") { Text("--Please choose an option--") }
Option(value = "dog") { Text("Dog") }
Option(value = "cat") { Text("Cat") }
Option(value = "hamster") { Text("Hamster") }
Option(value = "parrot") { Text("Parrot") }
Option(value = "spider") { Text("Spider") }
Option(value = "goldfish") { Text("Goldfish") }
}
}
val selectDispatchesInputAndChangeAndBeforeInputEvents by testCase {
var onInputState by remember { mutableStateOf("None") }
var onChangeState by remember { mutableStateOf("None") }
P { TestText(value = onInputState, id = "txt_oninput") }
P { TestText(value = onChangeState, id = "txt_onchange") }
ChooseAPetSelect(
onInput = { onInputState = it.value ?: "" },
onChange = { onChangeState = it.value ?: "" }
)
}
val selectMultipleItems by testCase {
var selectedItemsText by remember { mutableStateOf("None") }
P { TestText(value = selectedItemsText) }
ChooseAPetSelect(
canSelectMultiple = true,
onInput = {},
onChange = {
selectedItemsText = it.target.selectedOptions.asList().joinToString(separator = ", ") {
it.asDynamic().value
}
}
)
}
}

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

@ -13,6 +13,8 @@ import org.openqa.selenium.By
import org.openqa.selenium.Keys import org.openqa.selenium.Keys
import org.openqa.selenium.interactions.Actions import org.openqa.selenium.interactions.Actions
import org.openqa.selenium.WebDriver import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.interactions.touch.TouchActions
class EventTests : BaseIntegrationTests() { class EventTests : BaseIntegrationTests() {
@ -117,7 +119,23 @@ class EventTests : BaseIntegrationTests() {
val selectAll = Keys.chord(COMMAND_CROSS_PLATFORM, "a") val selectAll = Keys.chord(COMMAND_CROSS_PLATFORM, "a")
selectableText.sendKeys(selectAll) selectableText.sendKeys(selectAll)
driver.waitTextToBe(value = "Text Selected") driver.waitTextToBe(value = "This is a text to be selected")
}
@ResolveDrivers
fun `select event in TextArea update the txt`(driver: WebDriver) {
driver.openTestPage("selectEventInTextAreaUpdatesText")
driver.waitTextToBe(value = "None")
driver.waitTextToBe(value = "None", textId = "txt2")
val selectableText = driver.findElement(By.id("textArea"))
val selectAll = Keys.chord(COMMAND_CROSS_PLATFORM, "a")
selectableText.sendKeys(selectAll)
val expectedText = "This is a text to be selected"
driver.waitTextToBe(value = expectedText)
driver.waitTextToBe(value = "0,${expectedText.length}", textId = "txt2")
} }
@ResolveDrivers @ResolveDrivers
@ -236,4 +254,157 @@ class EventTests : BaseIntegrationTests() {
Actions(driver).moveToElement(box).moveByOffset(-20, -20).perform() Actions(driver).moveToElement(box).moveByOffset(-20, -20).perform()
driver.waitTextToBe(value = "88,88|80,80") driver.waitTextToBe(value = "88,88|80,80")
} }
private fun WebDriver.copyElementTextToClipboard(element: WebElement) {
Actions(this)
.doubleClick(element)
.keyDown(COMMAND_CROSS_PLATFORM)
.sendKeys("C")
.keyUp(COMMAND_CROSS_PLATFORM)
.perform()
}
private fun WebDriver.cutElementTextToClipboard(element: WebElement) {
Actions(this)
.doubleClick(element)
.keyDown(COMMAND_CROSS_PLATFORM)
.sendKeys("X")
.keyUp(COMMAND_CROSS_PLATFORM)
.perform()
}
private fun WebDriver.pasteFromClipboardInto(element: WebElement) {
Actions(this)
.click(element)
.keyDown(COMMAND_CROSS_PLATFORM)
.sendKeys("V")
.keyUp(COMMAND_CROSS_PLATFORM)
.perform()
}
@ResolveDrivers
fun copyPasteOverriddenEventsTest(driver: WebDriver) {
driver.openTestPage("copyPasteEventsTest")
driver.waitTextToBe(value = "None")
val textToCopy1 = driver.findElement(By.id("txt_to_copy"))
val textinput1 = driver.findElement(By.id("textinput"))
driver.copyElementTextToClipboard(textToCopy1)
driver.pasteFromClipboardInto(textinput1)
// Copied text should be changed by the onCopy handler
driver.waitTextToBe(value = "COPIED_TEXT_WAS_OVERRIDDEN".lowercase())
}
@ResolveDrivers
fun copyPasteUnchangedEventsTest(driver: WebDriver) {
driver.openTestPage("copyPasteEventsTest")
driver.waitTextToBe(value = "None")
val textinput2 = driver.findElement(By.id("textinput"))
val textToCopy2 = driver.findElement(By.id("txt_to_copy2"))
driver.copyElementTextToClipboard(textToCopy2)
driver.pasteFromClipboardInto(textinput2)
// Copied text should not be changed by the onCopy handler
driver.waitTextToBe(value = "SomeTestTextToCopy2".lowercase())
}
@ResolveDrivers
fun cutPasteEventsTests(driver: WebDriver) {
driver.openTestPage("cutPasteEventsTest")
driver.waitTextToBe(value = "None")
val textinput1 = driver.findElement(By.id("textinput1"))
val textinput2 = driver.findElement(By.id("textinput2"))
driver.cutElementTextToClipboard(textinput1)
driver.waitTextToBe(value = "Text was cut")
driver.pasteFromClipboardInto(textinput2)
driver.waitTextToBe(value = "Modified pasted text = TextToCut")
}
@ResolveDrivers
fun keyDownKeyUpTest(driver: WebDriver) {
driver.openTestPage("keyDownKeyUpTest")
driver.waitTextToBe(value = "None", textId = "txt_down")
driver.waitTextToBe(value = "None", textId = "txt_up")
val input = driver.findElement(By.id("textinput"))
input.sendKeys("a")
driver.waitTextToBe(value = "keydown = a", textId = "txt_down")
driver.waitTextToBe(value = "keyup = a", textId = "txt_up")
Actions(driver).keyDown(input, Keys.SHIFT).perform()
driver.waitTextToBe(value = "keydown = Shift", textId = "txt_down")
driver.waitTextToBe(value = "keyup = a", textId = "txt_up")
Actions(driver).keyUp(input, Keys.SHIFT).perform()
driver.waitTextToBe(value = "keydown = Shift", textId = "txt_down")
driver.waitTextToBe(value = "keyup = Shift", textId = "txt_up")
}
//@ResolveDrivers
// TODO: at least chrome driver has mobile emulation, so we can try it out (didn't work right away)
fun touchEventsDispatched(driver: WebDriver) {
driver.openTestPage("touchEventsDispatched")
driver.waitTextToBe(value = "None", textId = "txt_start")
driver.waitTextToBe(value = "None", textId = "txt_move")
driver.waitTextToBe(value = "None", textId = "txt_end")
val box = driver.findElement(By.id("box"))
val boxMiddleX = box.rect.x + box.rect.width / 2
val boxMiddleY = box.rect.y + box.rect.height / 2
val boxRightBottomX = box.rect.x + box.rect.width
val boxRightBottomY = box.rect.y + box.rect.height
TouchActions(driver)
.down(boxMiddleX, boxMiddleY)
.move(boxRightBottomX, boxRightBottomY)
.up(boxRightBottomX, boxRightBottomY)
.perform()
driver.waitTextToBe(value = "STARTED", textId = "txt_start")
driver.waitTextToBe(value = "MOVED", textId = "txt_move")
driver.waitTextToBe(value = "ENDED", textId = "txt_end")
}
@ResolveDrivers
fun animationEventsDispatched(driver: WebDriver) {
driver.openTestPage("animationEventsDispatched")
driver.waitTextToBe(value = "None", textId = "txt_start")
driver.waitTextToBe(value = "None", textId = "txt_end")
driver.findElement(By.id("box")).click()
driver.waitTextToBe(value = "STARTED - AppStyleSheetWithAnimation-bounce", textId = "txt_start")
driver.waitTextToBe(value = "ENDED", textId = "txt_end")
}
@ResolveDrivers
fun onSubmitEventForFormDispatched(driver: WebDriver) {
driver.openTestPage("onSubmitEventForFormDispatched")
driver.waitTextToBe(value = "None")
driver.findElement(By.id("send_form_btn")).click()
driver.waitTextToBe(value = "Form submitted")
}
@ResolveDrivers
fun onResetEventForFormDispatched(driver: WebDriver) {
driver.openTestPage("onResetEventForFormDispatched")
driver.waitTextToBe(value = "None")
driver.findElement(By.id("reset_form_btn")).click()
driver.waitTextToBe(value = "Form reset")
}
} }

48
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt

@ -163,6 +163,52 @@ class InputsTests : BaseIntegrationTests() {
driver.waitTextToBe(value = "None") driver.waitTextToBe(value = "None")
driver.findElement(By.id("txt")).click() // to change the focus - triggers onChange driver.findElement(By.id("txt")).click() // to change the focus - triggers onChange
driver.waitTextToBe(value = "INPUT HAS CHANGED") driver.waitTextToBe(value = "1")
}
@ResolveDrivers
fun `onChange in TextArea updates the text`(driver: WebDriver) {
driver.openTestPage("changeEventInTextAreaUpdatesText")
driver.waitTextToBe(value = "None")
val textArea = driver.findElement(By.id("textArea"))
textArea.sendKeys("NewText")
driver.waitTextToBe(value = "None")
driver.findElement(By.id("txt")).click() // to change the focus - triggers onChange
driver.waitTextToBe(value = "NoneNewText")
}
@ResolveDrivers
fun `onBeforeInput updates the text`(driver: WebDriver) {
driver.openTestPage("beforeInputEventUpdatesText")
driver.waitTextToBe(value = "None")
val input = driver.findElement(By.id("textInput"))
input.sendKeys("1")
driver.waitTextToBe(value = "1")
driver.waitTextToBe(value = "1", textId = "txt2")
input.sendKeys("2")
driver.waitTextToBe(value = "2")
driver.waitTextToBe(value = "12", textId = "txt2")
}
@ResolveDrivers
fun `onBeforeInput in TextArea updates the text`(driver: WebDriver) {
driver.openTestPage("beforeInputEventInTextAreaUpdatesText")
driver.waitTextToBe(value = "None")
val textArea = driver.findElement(By.id("textArea"))
textArea.sendKeys("1")
driver.waitTextToBe(value = "1")
driver.waitTextToBe(value = "1", textId = "txt2")
textArea.sendKeys("2")
driver.waitTextToBe(value = "2")
driver.waitTextToBe(value = "12", textId = "txt2")
} }
} }

49
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/SelectElementTests.kt

@ -0,0 +1,49 @@
package org.jetbrains.compose.web.tests.integration
import org.jetbrains.compose.web.tests.integration.common.BaseIntegrationTests
import org.jetbrains.compose.web.tests.integration.common.ResolveDrivers
import org.jetbrains.compose.web.tests.integration.common.openTestPage
import org.jetbrains.compose.web.tests.integration.common.waitTextToBe
import org.openqa.selenium.By
import org.openqa.selenium.WebDriver
import org.openqa.selenium.support.ui.Select
class SelectElementTests : BaseIntegrationTests() {
@ResolveDrivers
fun selectDispatchesInputAndChangeAndBeforeInputEvents(driver: WebDriver) {
driver.openTestPage("selectDispatchesInputAndChangeAndBeforeInputEvents")
driver.waitTextToBe(textId = "txt_oninput", value = "None")
driver.waitTextToBe(textId = "txt_onchange", value = "None")
// Commented code does force onChange, but doesn't force onInput for some reason
// val select = Select(driver.findElement(By.name("pets")))
// select.selectByIndex(0)
// Code below works properly: forces both onInput and onChange
driver.findElement(By.name("pets")).sendKeys("Dog")
driver.waitTextToBe(textId = "txt_onchange", value = "dog")
driver.waitTextToBe(textId = "txt_oninput", value = "dog")
}
@ResolveDrivers
fun selectMultipleItems(driver: WebDriver) {
driver.openTestPage("selectMultipleItems")
driver.waitTextToBe(value = "None")
val select = Select(driver.findElement(By.name("pets")))
select.selectByIndex(1)
driver.waitTextToBe(value = "dog")
select.selectByIndex(2)
driver.waitTextToBe(value = "dog, cat")
select.selectByIndex(3)
driver.waitTextToBe(value = "dog, cat, hamster")
select.deselectByIndex(2)
driver.waitTextToBe(value = "dog, hamster")
}
}

2
web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt

@ -21,7 +21,7 @@ fun WebDriver.openTestPage(test: String) {
} }
fun WebDriver.waitTextToBe(textId: String = "txt", value: String) { fun WebDriver.waitTextToBe(textId: String = "txt", value: String) {
WebDriverWait(this, 1).until(ExpectedConditions.textToBe(By.id(textId), value)) WebDriverWait(this, 1, 16).until(ExpectedConditions.textToBe(By.id(textId), value))
} }
internal object Drivers { internal object Drivers {

Loading…
Cancel
Save