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
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.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable
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.HTMLElement
import org.w3c.dom.Node
@ -46,9 +44,9 @@ external interface EventListenerOptions {
}
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
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
import org.jetbrains.compose.web.events.SyntheticSubmitEvent
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLFormElement
@ -92,6 +93,20 @@ fun AttrsBuilder<HTMLFormElement>.noValidate() =
fun AttrsBuilder<HTMLFormElement>.target(value: FormTarget) =
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 */
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
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.events.WrappedClipboardEvent
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.WrappedTouchEvent
import org.jetbrains.compose.web.events.GenericWrappedEvent
import org.jetbrains.compose.web.events.*
import org.w3c.dom.HTMLFormElement
import org.w3c.dom.events.EventTarget
private typealias SyntheticMouseEventListener = (SyntheticMouseEvent) -> Unit
private typealias SyntheticWheelEventListener = (SyntheticWheelEvent) -> Unit
@ -17,225 +14,190 @@ private typealias SyntheticDragEventListener = (SyntheticDragEvent) -> Unit
open class EventsListenerBuilder {
protected val listeners = mutableListOf<WrappedEventListener<*>>()
protected val listeners = mutableListOf<SyntheticEventListener<*>>()
/* 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) {
listeners.add(createMouseEventListener(CONTEXTMENU, options, listener))
listeners.add(MouseEventListener(CONTEXTMENU, options, listener))
}
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) {
listeners.add(createMouseEventListener(DBLCLICK, options, listener))
listeners.add(MouseEventListener(DBLCLICK, options, listener))
}
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) {
listeners.add(createMouseEventListener(MOUSEUP, options, listener))
listeners.add(MouseEventListener(MOUSEUP, options, listener))
}
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) {
listeners.add(createMouseEventListener(MOUSELEAVE, options, listener))
listeners.add(MouseEventListener(MOUSELEAVE, options, listener))
}
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) {
listeners.add(createMouseEventListener(MOUSEOUT, options, listener))
listeners.add(MouseEventListener(MOUSEOUT, options, listener))
}
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) {
listeners.add(createMouseWheelEventListener(WHEEL, options, listener))
fun onWheel(options: Options = Options.DEFAULT, listener: SyntheticWheelEventListener) {
listeners.add(MouseWheelEventListener(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))
listeners.add(DragEventListener(DRAG, options, listener))
}
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) {
listeners.add(createDragEventListener(DRAGSTART, options, listener))
listeners.add(DragEventListener(DRAGSTART, options, listener))
}
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) {
listeners.add(createDragEventListener(DRAGOVER, options, listener))
listeners.add(DragEventListener(DRAGOVER, options, listener))
}
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) {
listeners.add(createDragEventListener(DRAGLEAVE, options, listener))
listeners.add(DragEventListener(DRAGLEAVE, options, listener))
}
/* 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))
}
fun onCut(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
fun onCut(options: Options = Options.DEFAULT, listener: (SyntheticClipboardEvent) -> Unit) {
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))
}
fun onGenericInput(
options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit
) {
listeners.add(WrappedEventListener(INPUT, options, listener))
}
/* End of Clipboard Events */
fun onChange(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(CHANGE, options, listener))
}
/* Keyboard Events */
fun onInvalid(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(INVALID, options, listener))
fun onKeyDown(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) {
listeners.add(KeyboardEventListener(KEYDOWN, options, listener))
}
fun onSearch(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(SEARCH, options, listener))
fun onKeyUp(options: Options = Options.DEFAULT, listener: (SyntheticKeyboardEvent) -> Unit) {
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))
}
fun onBlur(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) {
fun onBlur(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
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))
}
fun onFocusOut(options: Options = Options.DEFAULT, listener: (WrappedFocusEvent) -> Unit) {
fun onFocusOut(options: Options = Options.DEFAULT, listener: (SyntheticFocusEvent) -> Unit) {
listeners.add(FocusEventListener(FOCUSOUT, options, listener))
}
fun onKeyDown(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) {
listeners.add(KeyboardEventListener(KEYDOWN, options, listener))
}
/* End of Focus Events */
fun onKeyUp(options: Options = Options.DEFAULT, listener: (WrappedKeyboardEvent) -> Unit) {
listeners.add(KeyboardEventListener(KEYUP, options, listener))
}
/* Touch Events */
fun onScroll(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(SCROLL, options, listener))
}
fun onSelect(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
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))
}
fun onTouchMove(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) {
fun onTouchMove(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
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))
}
fun onTouchStart(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) {
fun onTouchStart(options: Options = Options.DEFAULT, listener: (SyntheticTouchEvent) -> Unit) {
listeners.add(TouchEventListener(TOUCHSTART, options, listener))
}
fun onAnimationEnd(options: Options = Options.DEFAULT, listener: (WrappedTouchEvent) -> Unit) {
listeners.add(WrappedEventListener(ANIMATIONEND, options, listener))
/* End of Touch Events */
/* 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) {
listeners.add(WrappedEventListener(ANIMATIONITERATION, options, listener))
fun onAnimationIteration(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) {
listeners.add(AnimationEventListener(ANIMATIONITERATION, options, listener))
}
fun onAnimationStart(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) {
listeners.add(WrappedEventListener(ANIMATIONSTART, options, listener))
fun onAnimationStart(options: Options = Options.DEFAULT, listener: (SyntheticAnimationEvent) -> Unit) {
listeners.add(AnimationEventListener(ANIMATIONSTART, options, listener))
}
fun onBeforeInput(options: Options = Options.DEFAULT, listener: (WrappedInputEvent) -> Unit) {
listeners.add(InputEventListener(BEFOREINPUT, options, listener))
/* End of Animation Events */
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(
eventName: String,
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) {
@ -282,7 +244,6 @@ open class EventsListenerBuilder {
const val INPUT = "input"
const val CHANGE = "change"
const val INVALID = "invalid"
const val SEARCH = "search"
const val DRAG = "drag"
const val DROP = "drop"
@ -291,5 +252,8 @@ open class EventsListenerBuilder {
const val DRAGOVER = "dragover"
const val DRAGENTER = "dragenter"
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.ComposeNode
import androidx.compose.web.attributes.InputAttrsBuilder
import androidx.compose.web.attributes.TextAreaAttrsBuilder
import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder
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.DomNodeWrapper
import kotlinx.browser.document
@ -595,11 +596,21 @@ fun Form(
@Composable
fun Select(
attrs: AttrBuilderContext<HTMLSelectElement>? = null,
attrs: (SelectAttrsBuilder.() -> Unit)? = null,
multiple: Boolean = false,
content: ContentBuilder<HTMLSelectElement>? = null
) = TagElement(
elementBuilder = Select,
applyAttrs = attrs,
applyAttrs = {
if (multiple) multiple()
if (attrs != null) {
val selectAttrsBuilder = with(SelectAttrsBuilder()) {
attrs()
this
}
copyFrom(selectAttrsBuilder)
}
},
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.NonRestartableComposable
import androidx.compose.web.attributes.InputAttrsBuilder
import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder
import org.jetbrains.compose.web.attributes.*
private fun InputAttrsBuilder<String>.applyAttrsWithStringValue(
value: String,
attrsBuilder: InputAttrsBuilder<String>.() -> Unit
attrs: InputAttrsBuilder<String>.() -> Unit
) {
value(value)
attrsBuilder()
attrs()
}
@Composable
@NonRestartableComposable
fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Checkbox,
attrs = {
if (checked) checked()
this.attrsBuilder()
this.attrs()
}
)
}
@Composable
@NonRestartableComposable
fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun DateInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun EmailInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun FileInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun HiddenInput(attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Hidden, attrs = attrsBuilder)
fun HiddenInput(attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Hidden, attrs = attrs)
}
@Composable
@NonRestartableComposable
fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun MonthInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@ -67,7 +67,7 @@ fun NumberInput(
value: Number? = null,
min: Number? = null,
max: Number? = null,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {}
attrs: InputAttrsBuilder<Number?>.() -> Unit = {}
) {
Input(
type = InputType.Number,
@ -75,25 +75,25 @@ fun NumberInput(
if (value != null) value(value.toString())
if (min != null) min(min.toString())
if (max != null) max(max.toString())
attrsBuilder()
attrs()
}
)
}
@Composable
@NonRestartableComposable
fun PasswordInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun PasswordInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Radio,
attrs = {
if (checked) checked()
attrsBuilder()
attrs()
}
)
}
@ -105,7 +105,7 @@ fun RangeInput(
min: Number? = null,
max: Number? = null,
step: Number = 1,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {}
attrs: InputAttrsBuilder<Number?>.() -> Unit = {}
) {
Input(
type = InputType.Range,
@ -114,49 +114,49 @@ fun RangeInput(
if (min != null) min(min.toString())
if (max != null) max(max.toString())
step(step)
attrsBuilder()
attrs()
}
)
}
@Composable
@NonRestartableComposable
fun SearchInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun SearchInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun SubmitInput(attrsBuilder: InputAttrsBuilder<Unit>.() -> Unit = {}) {
Input(type = InputType.Submit, attrs = attrsBuilder)
fun SubmitInput(attrs: InputAttrsBuilder<Unit>.() -> Unit = {}) {
Input(type = InputType.Submit, attrs = attrs)
}
@Composable
@NonRestartableComposable
fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun TelInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun TextInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun TimeInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun UrlInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrs) })
}
@Composable
@NonRestartableComposable
fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
fun WeekInput(value: String = "", attrs: InputAttrsBuilder<String>.() -> Unit = {}) {
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.EventTarget
open class SyntheticEvent<Element : EventTarget>(
open class SyntheticEvent<Element : EventTarget> internal constructor(
val nativeEvent: Event
) {
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
*/
open class SyntheticMouseEvent(
open class SyntheticMouseEvent internal constructor(
nativeEvent: MouseEvent
) : 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
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.Text
import org.jetbrains.compose.web.renderComposableInBody
@ -32,7 +33,7 @@ internal val testCases = mutableMapOf<String, TestCase>()
fun launchTestCase(testCaseId: String) {
// 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")

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")
id("selectableText")
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) }
}
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 {
Input(type = InputType.Number, attrs = {
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.interactions.Actions
import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.interactions.touch.TouchActions
class EventTests : BaseIntegrationTests() {
@ -117,7 +119,23 @@ class EventTests : BaseIntegrationTests() {
val selectAll = Keys.chord(COMMAND_CROSS_PLATFORM, "a")
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
@ -236,4 +254,157 @@ class EventTests : BaseIntegrationTests() {
Actions(driver).moveToElement(box).moveByOffset(-20, -20).perform()
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.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) {
WebDriverWait(this, 1).until(ExpectedConditions.textToBe(By.id(textId), value))
WebDriverWait(this, 1, 16).until(ExpectedConditions.textToBe(By.id(textId), value))
}
internal object Drivers {

Loading…
Cancel
Save