diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt index fc81be01b7..d3ee896f66 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/DomApplier.kt +++ b/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>() + private var currentListeners = emptyList>() - fun updateEventListeners(list: List>) { + fun updateEventListeners(list: List>) { val htmlElement = node as? HTMLElement ?: return currentListeners.forEach { diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt index 0b46009039..38224a2a45 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/Attrs.kt +++ b/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.noValidate() = fun AttrsBuilder.target(value: FormTarget) = attr("target", value.targetStr) +fun AttrsBuilder.onSubmit( + options: Options = Options.DEFAULT, + listener: (SyntheticSubmitEvent) -> Unit +) { + addEventListener(eventName = EventsListenerBuilder.SUBMIT, options = options, listener = listener) +} + +fun AttrsBuilder.onReset( + options: Options = Options.DEFAULT, + listener: (SyntheticSubmitEvent) -> Unit +) { + addEventListener(eventName = EventsListenerBuilder.RESET, options = options, listener = listener) +} + /* Input attributes */ fun AttrsBuilder.type(value: InputType<*>) = diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt index 82eb55679c..13fe73f02c 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/EventsListenerBuilder.kt +++ b/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>() + protected val listeners = mutableListOf>() /* 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)) - } - - 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)) - } + /* End of Focus Events */ - fun onSelect(options: Options = Options.DEFAULT, listener: (WrappedEvent) -> Unit) { - listeners.add(WrappedEventListener(SELECT, options, listener)) - } + /* Touch Events */ - 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) -> Unit) { + listeners.add(SyntheticEventListener(SCROLL, options, listener)) } - fun collectListeners(): List> = listeners + fun collectListeners(): List> = listeners + + fun > 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) -> 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" } } diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt deleted file mode 100644 index d6ea224411..0000000000 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/InputAttrsBuilder.kt +++ /dev/null @@ -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( - val value: ValueType, - nativeEvent: Event -) : SyntheticEvent( - nativeEvent = nativeEvent -) - -class InputAttrsBuilder( - val inputType: InputType -) : AttrsBuilder() { - - fun onInput(options: Options = Options.DEFAULT, listener: (SyntheticInputEvent) -> Unit) { - addEventListener(INPUT, options) { - val value = inputType.inputValue(it.nativeEvent) - listener(SyntheticInputEvent(value, it.nativeEvent)) - } - } -} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/SyntheticEventListener.kt new file mode 100644 index 0000000000..fa7926eaba --- /dev/null +++ b/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> internal constructor( + val event: String, + val options: Options, + val listener: (T) -> Unit +) : EventListener { + + @Suppress("UNCHECKED_CAST") + override fun handleEvent(event: Event) { + listener(SyntheticEvent(event).unsafeCast()) + } +} + +class Options { + // TODO: add options for addEventListener + + companion object { + val DEFAULT = Options() + } +} + +internal class AnimationEventListener( + event: String, + options: Options, + listener: (SyntheticAnimationEvent) -> Unit +) : SyntheticEventListener( + event, options, listener +) { + override fun handleEvent(event: Event) { + listener(SyntheticAnimationEvent(event, event.unsafeCast())) + } +} + +internal class MouseEventListener( + event: String, + options: Options, + listener: (SyntheticMouseEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticMouseEvent(event.unsafeCast())) + } +} + +internal class MouseWheelEventListener( + event: String, + options: Options, + listener: (SyntheticWheelEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticWheelEvent(event.unsafeCast())) + } +} + +internal class KeyboardEventListener( + event: String, + options: Options, + listener: (SyntheticKeyboardEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticKeyboardEvent(event.unsafeCast())) + } +} + +internal class FocusEventListener( + event: String, + options: Options, + listener: (SyntheticFocusEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticFocusEvent(event.unsafeCast())) + } +} + +internal class TouchEventListener( + event: String, + options: Options, + listener: (SyntheticTouchEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticTouchEvent(event.unsafeCast())) + } +} + +internal class DragEventListener( + event: String, + options: Options, + listener: (SyntheticDragEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticDragEvent(event.unsafeCast())) + } +} + +internal class ClipboardEventListener( + event: String, + options: Options, + listener: (SyntheticClipboardEvent) -> Unit +) : SyntheticEventListener(event, options, listener) { + override fun handleEvent(event: Event) { + listener(SyntheticClipboardEvent(event.unsafeCast())) + } +} + +internal class InputEventListener( + eventName: String = INPUT, + options: Options, + val inputType: InputType, + listener: (SyntheticInputEvent) -> Unit +) : SyntheticEventListener>( + eventName, options, listener +) { + override fun handleEvent(event: Event) { + val value = inputType.inputValue(event) + listener(SyntheticInputEvent(value, event)) + } +} + +internal class ChangeEventListener( + options: Options, + val inputType: InputType, + listener: (SyntheticChangeEvent) -> Unit +) : SyntheticEventListener>( + CHANGE, options, listener +) { + override fun handleEvent(event: Event) { + val value = inputType.inputValue(event) + listener(SyntheticChangeEvent(value, event)) + } +} + +internal class SelectEventListener( + options: Options, + listener: (SyntheticSelectEvent) -> Unit +) : SyntheticEventListener>( + SELECT, options, listener +) { + override fun handleEvent(event: Event) { + listener(SyntheticSelectEvent(event, event.target.unsafeCast())) + } +} + diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt deleted file mode 100644 index 76513a0856..0000000000 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/TextAreaAttrsBuilder.kt +++ /dev/null @@ -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() { - - fun onInput( - options: Options = Options.DEFAULT, - listener: (SyntheticInputEvent) -> Unit - ) { - addEventListener(INPUT, options) { - val text = it.nativeEvent.target.asDynamic().value.unsafeCast() - listener(SyntheticInputEvent(text, it.nativeEvent)) - } - } -} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/WrappedEventListener.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/WrappedEventListener.kt deleted file mode 100644 index 2fd626a7ba..0000000000 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/WrappedEventListener.kt +++ /dev/null @@ -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>( - 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(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedMouseEvent(event as MouseEvent)) - } -} - -internal class MouseWheelEventListener( - event: String, - options: Options, - listener: (WrappedWheelEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedWheelEvent(event as WheelEvent)) - } -} - -internal class KeyboardEventListener( - event: String, - options: Options, - listener: (WrappedKeyboardEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedKeyboardEvent(event as KeyboardEvent)) - } -} - -internal class FocusEventListener( - event: String, - options: Options, - listener: (WrappedFocusEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedFocusEvent(event as FocusEvent)) - } -} - -internal class TouchEventListener( - event: String, - options: Options, - listener: (WrappedTouchEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedTouchEvent(event as TouchEvent)) - } -} - -internal class DragEventListener( - event: String, - options: Options, - listener: (WrappedDragEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedDragEvent(event as DragEvent)) - } -} - -internal class PointerEventListener( - event: String, - options: Options, - listener: (WrappedPointerEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedPointerEvent(event as PointerEvent)) - } -} - -internal class ClipboardEventListener( - event: String, - options: Options, - listener: (WrappedClipboardEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedClipboardEvent(event as ClipboardEvent)) - } -} - -internal class InputEventListener( - event: String, - options: Options, - listener: (WrappedInputEvent) -> Unit -) : WrappedEventListener(event, options, listener) { - override fun handleEvent(event: Event) { - listener(WrappedInputEvent(event as InputEvent)) - } -} - -internal class RadioInputEventListener( - options: Options, - listener: (WrappedRadioInputEvent) -> Unit -) : WrappedEventListener(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( - 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(EventsListenerBuilder.INPUT, options, listener) { - override fun handleEvent(event: Event) { - val text = event.target.asDynamic().value as String - listener(WrappedTextInputEvent(event as InputEvent, text)) - } -} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/InputAttrsBuilder.kt new file mode 100644 index 0000000000..efbd88b53e --- /dev/null +++ b/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( + val inputType: InputType +) : AttrsBuilder() { + + fun onInvalid( + options: Options = Options.DEFAULT, + listener: (SyntheticEvent) -> Unit + ) { + addEventListener(INVALID, options, listener) + } + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(eventName = INPUT, options, inputType, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(ChangeEventListener(options, inputType, listener)) + } + + fun onBeforeInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(eventName = BEFOREINPUT, options, inputType, listener)) + } + + fun onSelect( + options: Options = Options.DEFAULT, + listener: (SyntheticSelectEvent) -> Unit + ) { + listeners.add(SelectEventListener(options, listener)) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/SelectAttrsBuilder.kt new file mode 100644 index 0000000000..8cbcf602f5 --- /dev/null +++ b/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() { + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(SelectInputEventListener(INPUT, options, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(SelectChangeEventListener(options, listener)) + } +} + +private class SelectInputEventListener( + eventName: String = INPUT, + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit +) : SyntheticEventListener>( + 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) -> Unit +): SyntheticEventListener>( + CHANGE, options, listener +) { + override fun handleEvent(event: Event) { + val value = event.target?.asDynamic().value?.toString() + listener(SyntheticChangeEvent(value, event)) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/attributes/builders/TextAreaAttrsBuilder.kt new file mode 100644 index 0000000000..6dd7f6d49e --- /dev/null +++ b/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() { + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(INPUT, options, InputType.Text, listener)) + } + + fun onChange( + options: Options = Options.DEFAULT, + listener: (SyntheticChangeEvent) -> Unit + ) { + listeners.add(ChangeEventListener(options, InputType.Text, listener)) + } + + fun onBeforeInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + listeners.add(InputEventListener(BEFOREINPUT, options, InputType.Text, listener)) + } + + fun onSelect( + options: Options = Options.DEFAULT, + listener: (SyntheticSelectEvent) -> Unit + ) { + listeners.add(SelectEventListener(options, listener)) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt index fd823474e7..38542fff30 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/Elements.kt +++ b/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? = null, + attrs: (SelectAttrsBuilder.() -> Unit)? = null, + multiple: Boolean = false, content: ContentBuilder? = null ) = TagElement( elementBuilder = Select, - applyAttrs = attrs, + applyAttrs = { + if (multiple) multiple() + if (attrs != null) { + val selectAttrsBuilder = with(SelectAttrsBuilder()) { + attrs() + this + } + copyFrom(selectAttrsBuilder) + } + }, content = content ) diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt index 8a9d7e98fd..41aade6239 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/elements/InputElements.kt +++ b/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.applyAttrsWithStringValue( value: String, - attrsBuilder: InputAttrsBuilder.() -> Unit + attrs: InputAttrsBuilder.() -> Unit ) { value(value) - attrsBuilder() + attrs() } @Composable @NonRestartableComposable -fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { +fun CheckboxInput(checked: Boolean = false, attrs: InputAttrsBuilder.() -> Unit = {}) { Input( type = InputType.Checkbox, attrs = { if (checked) checked() - this.attrsBuilder() + this.attrs() } ) } @Composable @NonRestartableComposable -fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun DateInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun DateTimeLocalInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun EmailInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun FileInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun HiddenInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Hidden, attrs = attrsBuilder) +fun HiddenInput(attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Hidden, attrs = attrs) } @Composable @NonRestartableComposable -fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun MonthInput(value: String = "", attrs: InputAttrsBuilder.() -> 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.() -> Unit = {} + attrs: InputAttrsBuilder.() -> 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.() -> Unit = {}) { - Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun PasswordInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { +fun RadioInput(checked: Boolean = false, attrs: InputAttrsBuilder.() -> 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.() -> Unit = {} + attrs: InputAttrsBuilder.() -> 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.() -> Unit = {}) { - Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun SearchInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun SubmitInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Submit, attrs = attrsBuilder) +fun SubmitInput(attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Submit, attrs = attrs) } @Composable @NonRestartableComposable -fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun TelInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun TextInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun TimeInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun UrlInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrs) }) } @Composable @NonRestartableComposable -fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { - Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +fun WeekInput(value: String = "", attrs: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrs) }) } diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticAnimationEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticAnimationEvent.kt new file mode 100644 index 0000000000..167c57ec23 --- /dev/null +++ b/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(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 +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticChangeEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticChangeEvent.kt new file mode 100644 index 0000000000..81b632ea72 --- /dev/null +++ b/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 internal constructor( + val value: Value, + nativeEvent: Event, +) : SyntheticEvent(nativeEvent) diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticClipboardEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticClipboardEvent.kt new file mode 100644 index 0000000000..99d45bd589 --- /dev/null +++ b/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(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) + } +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt index d61cd91a71..4142bdf418 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticEvent.kt +++ b/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( +open class SyntheticEvent internal constructor( val nativeEvent: Event ) { val target: Element = nativeEvent.target.unsafeCast() diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticFocusEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticFocusEvent.kt new file mode 100644 index 0000000000..a6084f3ba5 --- /dev/null +++ b/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(nativeEvent) { + + val relatedTarget: EventTarget? = nativeEvent.relatedTarget +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticInputEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticInputEvent.kt new file mode 100644 index 0000000000..4c1e90ccb9 --- /dev/null +++ b/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 internal constructor( + val value: ValueType, + nativeEvent: Event +) : SyntheticEvent( + nativeEvent = nativeEvent +) { + val data: String? = nativeEvent.asDynamic().data?.unsafeCast() + val dataTransfer: DataTransfer? = nativeEvent.asDynamic().dataTransfer?.unsafeCast() + val inputType: String? = nativeEvent.asDynamic().inputType?.unsafeCast() + val isComposing: Boolean = nativeEvent.asDynamic().isComposing?.unsafeCast() ?: false +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticKeyboardEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticKeyboardEvent.kt new file mode 100644 index 0000000000..ab3e43885b --- /dev/null +++ b/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(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 diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt index 818b2cbdc6..37f3ae9da7 100644 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticMouseEvent.kt +++ b/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(nativeEvent) { diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSelectEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSelectEvent.kt new file mode 100644 index 0000000000..773c8bee9a --- /dev/null +++ b/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 internal constructor( + nativeEvent: Event, + selectionInfoDetails: SelectionInfoDetails +) : SyntheticEvent(nativeEvent) { + + val selectionStart: Int = selectionInfoDetails.selectionStart + val selectionEnd: Int = selectionInfoDetails.selectionEnd + + + fun selection(): String { + return nativeEvent.target.asDynamic().value.unsafeCast()?.substring( + selectionStart, selectionEnd + ) ?: "" + } +} + +internal external interface SelectionInfoDetails { + val selectionStart: Int + val selectionEnd: Int +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSubmitEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticSubmitEvent.kt new file mode 100644 index 0000000000..325e8f996d --- /dev/null +++ b/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(nativeEvent) diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticTouchEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/SyntheticTouchEvent.kt new file mode 100644 index 0000000000..6cd9f20aec --- /dev/null +++ b/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(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 +} diff --git a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt b/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt deleted file mode 100644 index 3994204b84..0000000000 --- a/web/core/src/jsMain/kotlin/org/jetbrains/compose/web/events/WrappedEvent.kt +++ /dev/null @@ -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 { - val nativeEvent: T -} - -interface WrappedEvent : GenericWrappedEvent - -internal open class WrappedMouseEvent( - override val nativeEvent: MouseEvent -) : GenericWrappedEvent { - - // 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 - -open class WrappedInputEvent( - override val nativeEvent: Event -) : GenericWrappedEvent - -open class WrappedKeyboardEvent( - override val nativeEvent: KeyboardEvent -) : GenericWrappedEvent { - - 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 - -open class WrappedTouchEvent( - override val nativeEvent: TouchEvent -) : GenericWrappedEvent - -open class WrappedCompositionEvent( - override val nativeEvent: CompositionEvent -) : GenericWrappedEvent - -open class WrappedDragEvent( - override val nativeEvent: DragEvent -) : GenericWrappedEvent - -open class WrappedPointerEvent( - override val nativeEvent: PointerEvent -) : GenericWrappedEvent - -open class WrappedClipboardEvent( - override val nativeEvent: ClipboardEvent -) : GenericWrappedEvent - -class WrappedTextInputEvent( - nativeEvent: Event, - val inputValue: String -) : WrappedInputEvent(nativeEvent) - -class WrappedCheckBoxInputEvent( - override val nativeEvent: Event, - val checked: Boolean -) : GenericWrappedEvent - -class WrappedRadioInputEvent( - override val nativeEvent: Event, - val checked: Boolean -) : GenericWrappedEvent - -class WrappedEventImpl( - override val nativeEvent: Event -) : WrappedEvent diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt index 1a941f28e9..6c8f7e25b0 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt +++ b/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() fun launchTestCase(testCaseId: String) { // this makes test cases get initialised: - listOf(TestCases1(), InputsTests(), EventsTests()) + listOf(TestCases1(), InputsTests(), EventsTests(), SelectElementTests()) if (testCaseId !in testCases) error("Test Case '$testCaseId' not found") diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt index f467126124..7815fc763e 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt +++ b/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) + } + } } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt index 8adab2dbdd..ab525d76ce 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt +++ b/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 + } }) } } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/SelectElementTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/SelectElementTests.kt new file mode 100644 index 0000000000..3a526f3cf4 --- /dev/null +++ b/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) -> Unit, + onChange: (SyntheticChangeEvent) -> 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 + } + } + ) + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt index 8daf8328b0..197a79eecc 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt +++ b/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") + } } diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt index 59f0155687..bd7c46fa56 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt +++ b/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") } } diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/SelectElementTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/SelectElementTests.kt new file mode 100644 index 0000000000..3fb8c66043 --- /dev/null +++ b/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") + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt index 686dfb48f2..af1ba5aae3 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt +++ b/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 {