Browse Source

Web: Input element and onInput event refactoring (#799)

* web: Add more tests for the event handlers

* web: Inputs refactoring (wip)

* web: Add `Options` for `addEventListener`

* web: Add basic methods and properties to the SyntheticInputEvent to align it with `org.w3c.dom.events.Event`

* web: Add a test for `capture = true` Event Listener Option

* web: Update PR to make contain only relevant changes + add specific functions for Inputs

* web: Update PR to align with master after rebase

Co-authored-by: Oleksandr Karpovich <oleksandr.karpovich@jetbrains.com>
pull/822/head
Oleksandr Karpovich 3 years ago committed by GitHub
parent
commit
29a5297907
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt
  2. 11
      web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt
  3. 4
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt
  4. 14
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt
  5. 37
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt
  6. 93
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt
  7. 71
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt
  8. 32
      web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt
  9. 5
      web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt
  10. 2
      web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt
  11. 42
      web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt
  12. 192
      web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt
  13. 6
      web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt
  14. 92
      web/core/src/jsTest/kotlin/elements/AttributesTests.kt
  15. 8
      web/core/src/jsTest/kotlin/elements/EventTests.kt
  16. 388
      web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt
  17. 15
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt
  18. 4
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt
  19. 106
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt
  20. 182
      web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt
  21. 174
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt
  22. 31
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt
  23. 2
      web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt
  24. 3
      web/widgets/src/jsMain/kotlin/layouts/slider.kt

3
web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt

@ -70,8 +70,9 @@ fun CodeSampleSwitcher(count: Int, current: Int, onSelect: (Int) -> Unit) {
classes(SwitcherStylesheet.boxed) classes(SwitcherStylesheet.boxed)
}) { }) {
repeat(count) { ix -> repeat(count) { ix ->
Input(type = InputType.Radio, value = "snippet$ix", attrs = { Input(type = InputType.Radio, attrs = {
name("code-snippet") name("code-snippet")
value("snippet$ix")
id("snippet$ix") id("snippet$ix")
if (current == ix) checked() if (current == ix) checked()
onRadioInput { onSelect(ix) } onRadioInput { onSelect(ix) }

11
web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt

@ -5,8 +5,9 @@ import org.jetbrains.compose.web.attributes.WrappedEventListener
import org.jetbrains.compose.web.css.StyleHolder import org.jetbrains.compose.web.css.StyleHolder
import org.jetbrains.compose.web.dom.setProperty import org.jetbrains.compose.web.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable import org.jetbrains.compose.web.dom.setVariable
import kotlinx.browser.document
import kotlinx.dom.clear import kotlinx.dom.clear
import org.jetbrains.compose.web.attributes.Options
import org.jetbrains.compose.web.css.jsObject
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
import org.w3c.dom.Node import org.w3c.dom.Node
@ -38,6 +39,12 @@ class DomApplier(
} }
} }
external interface EventListenerOptions {
var once: Boolean
var passive: Boolean
var capture: Boolean
}
open class DomNodeWrapper(open val node: Node) { open class DomNodeWrapper(open val node: Node) {
private var currentListeners = emptyList<WrappedEventListener<*>>() private var currentListeners = emptyList<WrappedEventListener<*>>()
@ -114,4 +121,4 @@ class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
setVariable(node.style, name, value) setVariable(node.style, name, value)
} }
} }
} }

4
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt

@ -94,7 +94,7 @@ fun AttrsBuilder<HTMLFormElement>.target(value: FormTarget) =
/* Input attributes */ /* Input attributes */
fun AttrsBuilder<HTMLInputElement>.type(value: InputType) = fun AttrsBuilder<HTMLInputElement>.type(value: InputType<*>) =
attr("type", value.typeStr) attr("type", value.typeStr)
fun AttrsBuilder<HTMLInputElement>.accept(value: String) = fun AttrsBuilder<HTMLInputElement>.accept(value: String) =
@ -184,7 +184,7 @@ fun AttrsBuilder<HTMLInputElement>.size(value: Int) =
fun AttrsBuilder<HTMLInputElement>.src(value: String) = fun AttrsBuilder<HTMLInputElement>.src(value: String) =
attr("src", value) // image only attr("src", value) // image only
fun AttrsBuilder<HTMLInputElement>.step(value: Int) = fun AttrsBuilder<HTMLInputElement>.step(value: Number) =
attr("step", value.toString()) // numeric types only attr("step", value.toString()) // numeric types only
fun AttrsBuilder<HTMLInputElement>.valueAttr(value: String) = fun AttrsBuilder<HTMLInputElement>.valueAttr(value: String) =

14
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt

@ -7,8 +7,8 @@ import org.jetbrains.compose.web.css.StyleBuilderImpl
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.HTMLElement import org.w3c.dom.HTMLElement
class AttrsBuilder<TElement : Element> : EventsListenerBuilder() { open class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
private val attributesMap = mutableMapOf<String, String>() internal val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl() val styleBuilder = StyleBuilderImpl()
val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>() val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>()
@ -48,6 +48,16 @@ class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
return attributesMap return attributesMap
} }
internal fun copyFrom(attrsBuilder: AttrsBuilder<TElement>) {
refEffect = attrsBuilder.refEffect
styleBuilder.copyFrom(attrsBuilder.styleBuilder)
attributesMap.putAll(attrsBuilder.attributesMap)
propertyUpdates.addAll(attrsBuilder.propertyUpdates)
copyListenersFrom(attrsBuilder)
}
companion object { companion object {
const val CLASS = "class" const val CLASS = "class"
const val ID = "id" const val ID = "id"

37
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt

@ -16,7 +16,7 @@ import org.jetbrains.compose.web.events.GenericWrappedEvent
open class EventsListenerBuilder { open class EventsListenerBuilder {
private val listeners = mutableListOf<WrappedEventListener<*>>() protected val listeners = mutableListOf<WrappedEventListener<*>>()
fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) { fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(COPY, options, listener)) listeners.add(ClipboardEventListener(COPY, options, listener))
@ -42,35 +42,6 @@ open class EventsListenerBuilder {
listeners.add(MouseEventListener(DBLCLICK, options, listener)) listeners.add(MouseEventListener(DBLCLICK, options, listener))
} }
fun onInput(options: Options = Options.DEFAULT, listener: (WrappedInputEvent) -> Unit) {
listeners.add(InputEventListener(INPUT, options, listener))
}
fun onTextInput(options: Options = Options.DEFAULT, listener: (WrappedTextInputEvent) -> Unit) {
listeners.add(TextInputEventListener(options, listener))
}
fun onCheckboxInput(
options: Options = Options.DEFAULT,
listener: (WrappedCheckBoxInputEvent) -> Unit
) {
listeners.add(CheckBoxInputEventListener(options, listener))
}
fun onRadioInput(
options: Options = Options.DEFAULT,
listener: (WrappedRadioInputEvent) -> Unit
) {
listeners.add(RadioInputEventListener(options, listener))
}
fun onRangeInput(
options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit
) {
listeners.add(WrappedEventListener(INPUT, options, listener))
}
fun onGenericInput( fun onGenericInput(
options: Options = Options.DEFAULT, options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit listener: (GenericWrappedEvent<*>) -> Unit
@ -224,6 +195,10 @@ open class EventsListenerBuilder {
listeners.add(WrappedEventListener(eventName, options, listener)) listeners.add(WrappedEventListener(eventName, options, listener))
} }
internal fun copyListenersFrom(from: EventsListenerBuilder) {
listeners.addAll(from.listeners)
}
companion object { companion object {
const val COPY = "copy" const val COPY = "copy"
const val CUT = "cut" const val CUT = "cut"
@ -274,4 +249,4 @@ open class EventsListenerBuilder {
const val DRAGENTER = "dragenter" const val DRAGENTER = "dragenter"
const val DRAGLEAVE = "dragleave" const val DRAGLEAVE = "dragleave"
} }
} }

93
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt

@ -0,0 +1,93 @@
/*
* 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.*
import org.jetbrains.compose.web.events.GenericWrappedEvent
import org.jetbrains.compose.web.events.WrappedCheckBoxInputEvent
import org.jetbrains.compose.web.events.WrappedRadioInputEvent
import org.jetbrains.compose.web.events.WrappedTextInputEvent
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
class SyntheticInputEvent<ValueType, Element : HTMLElement>(
val value: ValueType,
val target: Element,
val nativeEvent: Event
) {
val bubbles: Boolean = nativeEvent.bubbles
val cancelable: Boolean = nativeEvent.cancelable
val composed: Boolean = nativeEvent.composed
val currentTarget: HTMLElement? = nativeEvent.currentTarget.unsafeCast<HTMLInputElement?>()
val eventPhase: Short = nativeEvent.eventPhase
val defaultPrevented: Boolean = nativeEvent.defaultPrevented
val timestamp: Number = nativeEvent.timeStamp
val type: String = nativeEvent.type
val isTrusted: Boolean = nativeEvent.isTrusted
fun preventDefault(): Unit = nativeEvent.preventDefault()
fun stopPropagation(): Unit = nativeEvent.stopPropagation()
fun stopImmediatePropagation(): Unit = nativeEvent.stopImmediatePropagation()
fun composedPath(): Array<EventTarget> = nativeEvent.composedPath()
}
class InputAttrsBuilder<T>(val inputType: InputType<T>) : AttrsBuilder<HTMLInputElement>() {
fun onInput(options: Options = Options.DEFAULT, listener: (SyntheticInputEvent<T, HTMLInputElement>) -> Unit) {
addEventListener(INPUT, options) {
val value = inputType.inputValue(it.nativeEvent)
listener(SyntheticInputEvent(value, it.nativeEvent.target as HTMLInputElement, it.nativeEvent))
}
}
@Deprecated(
message = "It's not reliable as it can be applied to any input type.",
replaceWith = ReplaceWith("onInput(options, listener)"),
level = DeprecationLevel.WARNING
)
fun onTextInput(options: Options = Options.DEFAULT, listener: (WrappedTextInputEvent) -> Unit) {
listeners.add(TextInputEventListener(options, listener))
}
@Deprecated(
message = "It's not reliable as it can be applied to any input type.",
replaceWith = ReplaceWith("onInput(options, listener)"),
level = DeprecationLevel.WARNING
)
fun onCheckboxInput(
options: Options = Options.DEFAULT,
listener: (WrappedCheckBoxInputEvent) -> Unit
) {
listeners.add(CheckBoxInputEventListener(options, listener))
}
@Deprecated(
message = "It's not reliable as it can be applied to any input type.",
replaceWith = ReplaceWith("onInput(options, listener)"),
level = DeprecationLevel.WARNING
)
fun onRadioInput(
options: Options = Options.DEFAULT,
listener: (WrappedRadioInputEvent) -> Unit
) {
listeners.add(RadioInputEventListener(options, listener))
}
@Deprecated(
message = "It's not reliable as it can be applied to any input type.",
replaceWith = ReplaceWith("onInput(options, listener)"),
level = DeprecationLevel.WARNING
)
fun onRangeInput(
options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit
) {
listeners.add(WrappedEventListener(INPUT, options, listener))
}
}

71
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt

@ -1,26 +1,55 @@
package org.jetbrains.compose.web.attributes package org.jetbrains.compose.web.attributes
sealed class InputType(val typeStr: String) { import org.w3c.dom.events.Event
object Button : InputType("button")
object Checkbox : InputType("checkbox") sealed class InputType<T>(val typeStr: String) {
object Color : InputType("color")
object Date : InputType("date") object Button : InputTypeWithUnitValue("button")
object DateTimeLocal : InputType("datetime-local") object Checkbox : InputTypeCheckedValue("checkbox")
object Email : InputType("email") object Color : InputTypeWithStringValue("color")
object File : InputType("file") object Date : InputTypeWithStringValue("date")
object Hidden : InputType("hidden") object DateTimeLocal : InputTypeWithStringValue("datetime-local")
object Month : InputType("month") object Email : InputTypeWithStringValue("email")
object Number : InputType("number") object File : InputTypeWithStringValue("file")
object Password : InputType("password") object Hidden : InputTypeWithStringValue("hidden")
object Radio : InputType("radio") object Month : InputTypeWithStringValue("month")
object Range : InputType("range") object Number : InputTypeNumberValue("number")
object Search : InputType("search") object Password : InputTypeWithStringValue("password")
object Submit : InputType("submit") object Radio : InputTypeCheckedValue("radio")
object Tel : InputType("tel") object Range : InputTypeNumberValue("range")
object Text : InputType("text") object Search : InputTypeWithStringValue("search")
object Time : InputType("time") object Submit : InputTypeWithUnitValue("submit")
object Url : InputType("url") object Tel : InputTypeWithStringValue("tel")
object Week : InputType("week") object Text : InputTypeWithStringValue("text")
object Time : InputTypeWithStringValue("time")
object Url : InputTypeWithStringValue("url")
object Week : InputTypeWithStringValue("week")
open class InputTypeWithStringValue(name: String) : InputType<String>(name) {
override fun inputValue(event: Event) = Week.valueAsString(event)
}
open class InputTypeWithUnitValue(name: String) : InputType<Unit>(name) {
override fun inputValue(event: Event) = Unit
}
open class InputTypeCheckedValue(name: String) : InputType<Boolean>(name) {
override fun inputValue(event: Event): Boolean {
return event.target?.asDynamic()?.checked?.unsafeCast<Boolean>() ?: false
}
}
open class InputTypeNumberValue(name: String) : InputType<kotlin.Number?>(name) {
override fun inputValue(event: Event): kotlin.Number? {
return event.target?.asDynamic()?.valueAsNumber ?: null
}
}
abstract fun inputValue(event: Event): T
protected fun valueAsString(event: Event): String {
return event.target?.asDynamic()?.value?.unsafeCast<String>() ?: ""
}
} }
sealed class DirType(val dirStr: String) { sealed class DirType(val dirStr: String) {

32
web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt

@ -0,0 +1,32 @@
/*
* 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.*
import org.jetbrains.compose.web.events.WrappedTextInputEvent
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.target as HTMLTextAreaElement, it.nativeEvent))
}
}
@Deprecated(
message = "It's not reliable as it can be applied to any input type.",
replaceWith = ReplaceWith("onInput(options, listener)"),
level = DeprecationLevel.WARNING
)
fun onTextInput(options: Options = Options.DEFAULT, listener: (WrappedTextInputEvent) -> Unit) {
listeners.add(TextInputEventListener(options, listener))
}
}

5
web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt

@ -106,6 +106,11 @@ open class StyleBuilderImpl : StyleBuilder, StyleHolder {
variables.nativeEquals(other.variables) variables.nativeEquals(other.variables)
} else false } else false
} }
internal fun copyFrom(sb: StyleBuilderImpl) {
properties.addAll(sb.properties)
variables.addAll(sb.variables)
}
} }
data class StylePropertyDeclaration( data class StylePropertyDeclaration(

2
web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt

@ -241,4 +241,4 @@ fun <TElement : Element> TagElement(
elementBuilder = ElementBuilder.createBuilder(tagName), elementBuilder = ElementBuilder.createBuilder(tagName),
applyAttrs = applyAttrs, applyAttrs = applyAttrs,
content = content content = content
) )

42
web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt

@ -2,19 +2,12 @@ package org.jetbrains.compose.web.dom
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.ComposeNode
import androidx.compose.web.attributes.InputAttrsBuilder
import androidx.compose.web.attributes.TextAreaAttrsBuilder
import org.jetbrains.compose.web.DomApplier import org.jetbrains.compose.web.DomApplier
import org.jetbrains.compose.web.DomNodeWrapper import org.jetbrains.compose.web.DomNodeWrapper
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.action
import org.jetbrains.compose.web.attributes.alt
import org.jetbrains.compose.web.attributes.forId
import org.jetbrains.compose.web.attributes.href
import org.jetbrains.compose.web.attributes.label
import org.jetbrains.compose.web.attributes.src
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.attributes.value
import kotlinx.browser.document import kotlinx.browser.document
import org.jetbrains.compose.web.attributes.*
import org.w3c.dom.HTMLAnchorElement import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLAreaElement import org.w3c.dom.HTMLAreaElement
import org.w3c.dom.HTMLAudioElement import org.w3c.dom.HTMLAudioElement
@ -30,7 +23,6 @@ import org.w3c.dom.HTMLHeadingElement
import org.w3c.dom.HTMLHRElement import org.w3c.dom.HTMLHRElement
import org.w3c.dom.HTMLIFrameElement import org.w3c.dom.HTMLIFrameElement
import org.w3c.dom.HTMLImageElement import org.w3c.dom.HTMLImageElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLLIElement import org.w3c.dom.HTMLLIElement
import org.w3c.dom.HTMLLabelElement import org.w3c.dom.HTMLLabelElement
import org.w3c.dom.HTMLLegendElement import org.w3c.dom.HTMLLegendElement
@ -55,7 +47,6 @@ import org.w3c.dom.HTMLTableColElement
import org.w3c.dom.HTMLTableElement import org.w3c.dom.HTMLTableElement
import org.w3c.dom.HTMLTableRowElement import org.w3c.dom.HTMLTableRowElement
import org.w3c.dom.HTMLTableSectionElement import org.w3c.dom.HTMLTableSectionElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.HTMLTrackElement import org.w3c.dom.HTMLTrackElement
import org.w3c.dom.HTMLUListElement import org.w3c.dom.HTMLUListElement
import org.w3c.dom.HTMLVideoElement import org.w3c.dom.HTMLVideoElement
@ -358,25 +349,6 @@ fun A(
) )
} }
@Composable
fun Input(
type: InputType = InputType.Text,
value: String = "",
attrs: AttrBuilderContext<HTMLInputElement>? = null
) {
TagElement(
elementBuilder = ElementBuilder.Input,
applyAttrs = {
type(type)
value(value)
if (attrs != null) {
attrs()
}
},
content = null
)
}
@Composable @Composable
fun Button( fun Button(
attrs: AttrBuilderContext<HTMLButtonElement>? = null, attrs: AttrBuilderContext<HTMLButtonElement>? = null,
@ -569,15 +541,17 @@ fun Section(
@Composable @Composable
fun TextArea( fun TextArea(
attrs: AttrBuilderContext<HTMLTextAreaElement>? = null, attrs: (TextAreaAttrsBuilder.() -> Unit)? = null,
value: String value: String
) = TagElement( ) = TagElement(
elementBuilder = ElementBuilder.TextArea, elementBuilder = ElementBuilder.TextArea,
applyAttrs = { applyAttrs = {
value(value) val taab = TextAreaAttrsBuilder()
if (attrs != null) { if (attrs != null) {
attrs() taab.attrs()
} }
taab.value(value)
this.copyFrom(taab)
} }
) { ) {
Text(value) Text(value)

192
web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt

@ -0,0 +1,192 @@
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.*
private fun InputAttrsBuilder<String>.applyAttrsWithStringValue(
value: String,
attrsBuilder: InputAttrsBuilder<String>.() -> Unit
) {
if (value.isNotEmpty()) value(value)
attrsBuilder()
}
@Composable
@NonRestartableComposable
fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Checkbox,
attrs = {
if (checked) checked()
this.attrsBuilder()
}
)
}
@Composable
@NonRestartableComposable
fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun HiddenInput(attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Hidden, attrs = attrsBuilder)
}
@Composable
@NonRestartableComposable
fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun NumberInput(
value: Number? = null,
min: Number? = null,
max: Number? = null,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {}
) {
Input(
type = InputType.Number,
attrs = {
if (value != null) value(value.toString())
if (min != null) min(min.toString())
if (max != null) max(max.toString())
attrsBuilder()
}
)
}
@Composable
@NonRestartableComposable
fun PasswordInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder<Boolean>.() -> Unit = {}) {
Input(
type = InputType.Radio,
attrs = {
if (checked) checked()
attrsBuilder()
}
)
}
@Composable
@NonRestartableComposable
fun RangeInput(
value: Number? = null,
min: Number? = null,
max: Number? = null,
step: Number = 1,
attrsBuilder: InputAttrsBuilder<Number?>.() -> Unit = {}
) {
Input(
type = InputType.Range,
attrs = {
if (value != null) value(value.toString())
if (min != null) min(min.toString())
if (max != null) max(max.toString())
step(step)
attrsBuilder()
}
)
}
@Composable
@NonRestartableComposable
fun SearchInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun SubmitInput(attrsBuilder: InputAttrsBuilder<Unit>.() -> Unit = {}) {
Input(type = InputType.Submit, attrs = attrsBuilder)
}
@Composable
@NonRestartableComposable
fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
@NonRestartableComposable
fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder<String>.() -> Unit = {}) {
Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) })
}
@Composable
fun <K> Input(
type: InputType<K>,
attrs: InputAttrsBuilder<K>.() -> Unit
) {
TagElement(
elementBuilder = ElementBuilder.Input,
applyAttrs = {
val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type)
inputAttrsBuilder.attrs()
this.copyFrom(inputAttrsBuilder)
},
content = null
)
}
@Composable
fun <K> Input(type: InputType<K>) {
TagElement(
elementBuilder = ElementBuilder.Input,
applyAttrs = {
val inputAttrsBuilder = InputAttrsBuilder(type)
inputAttrsBuilder.type(type)
this.copyFrom(inputAttrsBuilder)
},
content = null
)
}

6
web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt

@ -34,8 +34,8 @@ open class WrappedWheelEvent(
) : GenericWrappedEvent<WheelEvent> ) : GenericWrappedEvent<WheelEvent>
open class WrappedInputEvent( open class WrappedInputEvent(
override val nativeEvent: InputEvent override val nativeEvent: Event
) : GenericWrappedEvent<InputEvent> ) : GenericWrappedEvent<Event>
open class WrappedKeyboardEvent( open class WrappedKeyboardEvent(
override val nativeEvent: KeyboardEvent override val nativeEvent: KeyboardEvent
@ -88,7 +88,7 @@ open class WrappedClipboardEvent(
) : GenericWrappedEvent<ClipboardEvent> ) : GenericWrappedEvent<ClipboardEvent>
class WrappedTextInputEvent( class WrappedTextInputEvent(
nativeEvent: InputEvent, nativeEvent: Event,
val inputValue: String val inputValue: String
) : WrappedInputEvent(nativeEvent) ) : WrappedInputEvent(nativeEvent)

92
web/core/src/jsTest/kotlin/elements/AttributesTests.kt

@ -3,19 +3,109 @@ package org.jetbrains.compose.web.core.tests
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.AttrsBuilder
import org.jetbrains.compose.web.attributes.disabled import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId import org.jetbrains.compose.web.attributes.forId
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Label import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.dom.Text import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLDivElement import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class AttributesTests { class AttributesTests {
@Test
fun copyFromStyleBuilderCopiesCorrectly() {
val copyFromStyleBuilder = StyleBuilderImpl().apply {
property("color", "red")
property("height", 100.px)
variable("var1", 100)
variable("var2", 100.px)
}
val copyToStyleBuilder = StyleBuilderImpl().apply {
copyFrom(copyFromStyleBuilder)
}
assertEquals(copyFromStyleBuilder, copyToStyleBuilder)
}
@Test
fun copyFromAttrsBuilderCopiesCorrectly() {
val attrsBuilderCopyFrom = AttrsBuilder<HTMLElement>().apply {
id("id1")
classes("a b c")
attr("title", "customTitle")
prop<HTMLElement, String>({_, _ ->}, "Value")
ref { onDispose { } }
style {
width(500.px)
backgroundColor("red")
}
onClick { }
onFocusIn { }
onMouseEnter { }
}
val copyToAttrsBuilder = AttrsBuilder<HTMLElement>().apply {
copyFrom(attrsBuilderCopyFrom)
}
assertEquals(attrsBuilderCopyFrom.attributesMap, copyToAttrsBuilder.attributesMap)
assertEquals(attrsBuilderCopyFrom.styleBuilder, copyToAttrsBuilder.styleBuilder)
assertEquals(attrsBuilderCopyFrom.refEffect, copyToAttrsBuilder.refEffect)
assertEquals(attrsBuilderCopyFrom.propertyUpdates, copyToAttrsBuilder.propertyUpdates)
assertEquals(attrsBuilderCopyFrom.collectListeners(), copyToAttrsBuilder.collectListeners())
}
@Test
fun attrsBuilderCopyFromPreservesExistingAttrs() {
val attrsBuilderCopyFrom = AttrsBuilder<HTMLElement>().apply {
attr("title", "customTitle")
}
val copyToAttrsBuilder = AttrsBuilder<HTMLElement>().apply {
id("id1")
onClick { }
style {
width(100.px)
}
copyFrom(attrsBuilderCopyFrom)
}
assertEquals("id1", copyToAttrsBuilder.attributesMap["id"])
assertEquals(StyleBuilderImpl().apply { width(100.px) }, copyToAttrsBuilder.styleBuilder)
val listeners = copyToAttrsBuilder.collectListeners()
assertEquals(1, listeners.size)
assertEquals("click", listeners[0].event)
}
@Test
fun attrsBuilderCopyFromOverridesSameAttrs() {
val attrsBuilderCopyFrom = AttrsBuilder<HTMLElement>().apply {
attr("title", "customTitleNew")
}
val copyToAttrsBuilder = AttrsBuilder<HTMLElement>().apply {
attr("title", "customTitleOld")
}
assertEquals("customTitleOld", copyToAttrsBuilder.attributesMap["title"])
copyToAttrsBuilder.copyFrom(attrsBuilderCopyFrom)
assertEquals("customTitleNew", copyToAttrsBuilder.attributesMap["title"])
}
@Test @Test
fun labelForIdAttrAppliedProperly() = runTest { fun labelForIdAttrAppliedProperly() = runTest {
@ -151,4 +241,4 @@ class AttributesTests {
waitChanges() waitChanges()
assertEquals("<div b=\"pp\" c=\"cc\"></div>", root.innerHTML) assertEquals("<div b=\"pp\" c=\"cc\"></div>", root.innerHTML)
} }
} }

8
web/core/src/jsTest/kotlin/elements/EventTests.kt

@ -44,7 +44,7 @@ class EventTests {
Input( Input(
type = InputType.Checkbox, type = InputType.Checkbox,
attrs = { attrs = {
onCheckboxInput { handeled = true } onInput { handeled = true }
} }
) )
} }
@ -63,7 +63,7 @@ class EventTests {
Input( Input(
type = InputType.Radio, type = InputType.Radio,
attrs = { attrs = {
onRadioInput { handeled = true } onInput { handeled = true }
} }
) )
} }
@ -82,7 +82,7 @@ class EventTests {
composition { composition {
TextArea( TextArea(
{ {
onTextInput { handeled = true } onInput { handeled = true }
}, },
value = "" value = ""
) )
@ -95,4 +95,4 @@ class EventTests {
assertTrue(handeled) assertTrue(handeled)
} }
} }

388
web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt

@ -0,0 +1,388 @@
package org.jetbrains.compose.web.core.tests.elements
import org.jetbrains.compose.web.core.tests.runTest
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLInputElement
import kotlin.test.Test
import kotlin.test.assertEquals
class InputsGenerateCorrectHtmlTests {
@Test
fun checkBoxInput() = runTest {
composition {
CheckboxInput(checked = true) {
id("checkboxId")
}
}
val checkboxInput = root.firstChild as HTMLInputElement
assertEquals("checkbox", checkboxInput.getAttribute("type"))
assertEquals("checkboxId", checkboxInput.getAttribute("id"))
assertEquals(true, checkboxInput.checked)
}
@Test
fun checkBoxInputWithDefaults() = runTest {
composition { CheckboxInput() }
val checkboxInput = root.firstChild as HTMLInputElement
assertEquals("checkbox", checkboxInput.getAttribute("type"))
assertEquals(null, checkboxInput.getAttribute("id"))
assertEquals(false, checkboxInput.checked)
}
@Test
fun dateInput() = runTest {
composition {
DateInput(value = "2021-10-10") {
id("dateInputId")
}
}
val dateInput = root.firstChild as HTMLInputElement
assertEquals("date", dateInput.getAttribute("type"))
assertEquals("dateInputId", dateInput.getAttribute("id"))
assertEquals("2021-10-10", dateInput.value)
}
@Test
fun dateInputWithDefaults() = runTest {
composition { DateInput() }
val dateInput = root.firstChild as HTMLInputElement
assertEquals("date", dateInput.getAttribute("type"))
assertEquals(null, dateInput.getAttribute("id"))
assertEquals("", dateInput.value)
}
@Test
fun emailInput() = runTest {
composition {
EmailInput(value = "user@mail.com") {
id("emailInputId")
}
}
val emailInput = root.firstChild as HTMLInputElement
assertEquals("email", emailInput.getAttribute("type"))
assertEquals("emailInputId", emailInput.getAttribute("id"))
assertEquals("user@mail.com", emailInput.value)
}
@Test
fun emailInputWithDefaults() = runTest {
composition { EmailInput() }
val emailInput = root.firstChild as HTMLInputElement
assertEquals("email", emailInput.getAttribute("type"))
assertEquals(null, emailInput.getAttribute("id"))
assertEquals("", emailInput.value)
}
@Test
fun monthInput() = runTest {
composition {
MonthInput(value = "2017-06") {
id("monthInputId")
}
}
val monthInput = root.firstChild as HTMLInputElement
assertEquals("month", monthInput.getAttribute("type"))
assertEquals("monthInputId", monthInput.getAttribute("id"))
assertEquals("2017-06", monthInput.value)
}
@Test
fun monthInputWithDefaults() = runTest {
composition { MonthInput() }
val monthInput = root.firstChild as HTMLInputElement
assertEquals("month", monthInput.getAttribute("type"))
assertEquals(null, monthInput.getAttribute("id"))
assertEquals("", monthInput.value)
}
@Test
fun numberInput() = runTest {
composition {
NumberInput(value = 100, min = 10, max = 200) {
id("numberInputId")
}
}
val numberInput = root.firstChild as HTMLInputElement
assertEquals("number", numberInput.getAttribute("type"))
assertEquals("numberInputId", numberInput.getAttribute("id"))
assertEquals("200", numberInput.getAttribute("max"))
assertEquals("10", numberInput.getAttribute("min"))
assertEquals(100, numberInput.valueAsNumber.toInt())
}
@Test
fun numberInputWithDefaults() = runTest {
composition {
NumberInput {
id("numberInputId")
}
}
val numberInput = root.firstChild as HTMLInputElement
assertEquals("number", numberInput.getAttribute("type"))
assertEquals("numberInputId", numberInput.getAttribute("id"))
assertEquals(null, numberInput.getAttribute("max"))
assertEquals(null, numberInput.getAttribute("min"))
assertEquals("", numberInput.value)
}
@Test
fun passwordInput() = runTest {
composition {
PasswordInput(value = "somepassword") {
id("passwordInputId")
}
}
val passwordInput = root.firstChild as HTMLInputElement
assertEquals("password", passwordInput.getAttribute("type"))
assertEquals("passwordInputId", passwordInput.getAttribute("id"))
assertEquals("somepassword", passwordInput.value)
}
@Test
fun passwordInputWithDefaults() = runTest {
composition { PasswordInput() }
val passwordInput = root.firstChild as HTMLInputElement
assertEquals("password", passwordInput.getAttribute("type"))
assertEquals(null, passwordInput.getAttribute("id"))
assertEquals("", passwordInput.value)
}
@Test
fun radioInput() = runTest {
composition {
RadioInput(checked = true) {
id("radioInputId")
}
}
val radioInput = root.firstChild as HTMLInputElement
assertEquals("radio", radioInput.getAttribute("type"))
assertEquals("radioInputId", radioInput.getAttribute("id"))
assertEquals(true, radioInput.checked)
}
@Test
fun radioInputWithDefaults() = runTest {
composition { RadioInput() }
val radioInput = root.firstChild as HTMLInputElement
assertEquals("radio", radioInput.getAttribute("type"))
assertEquals(null, radioInput.getAttribute("id"))
assertEquals(false, radioInput.checked)
}
@Test
fun rangeInput() = runTest {
composition {
RangeInput(value = 20, min = 10, max = 30, step = 2) {
id("rangeInputId")
}
}
val rangeInput = root.firstChild as HTMLInputElement
assertEquals("range", rangeInput.getAttribute("type"))
assertEquals("rangeInputId", rangeInput.getAttribute("id"))
assertEquals("10", rangeInput.getAttribute("min"))
assertEquals("30", rangeInput.getAttribute("max"))
assertEquals("2", rangeInput.getAttribute("step"))
assertEquals(20, rangeInput.valueAsNumber.toInt())
}
@Test
fun rangeInputWithDefaults() = runTest {
composition { RangeInput() }
val rangeInput = root.firstChild as HTMLInputElement
assertEquals("range", rangeInput.getAttribute("type"))
assertEquals(null, rangeInput.getAttribute("id"))
assertEquals(null, rangeInput.getAttribute("min"))
assertEquals(null, rangeInput.getAttribute("max"))
assertEquals("1", rangeInput.getAttribute("step"))
}
@Test
fun searchInput() = runTest {
composition {
SearchInput(value = "Search Term") {
id("searchInputId")
}
}
val searchInput = root.firstChild as HTMLInputElement
assertEquals("search", searchInput.getAttribute("type"))
assertEquals("searchInputId", searchInput.getAttribute("id"))
assertEquals("Search Term", searchInput.value)
}
@Test
fun searchInputWithDefaults() = runTest {
composition { SearchInput() }
val searchInput = root.firstChild as HTMLInputElement
assertEquals("search", searchInput.getAttribute("type"))
assertEquals(null, searchInput.getAttribute("id"))
assertEquals("", searchInput.value)
}
@Test
fun telInput() = runTest {
composition {
TelInput(value = "0123456789") {
id("telInputId")
}
}
val textInput = root.firstChild as HTMLInputElement
assertEquals("tel", textInput.getAttribute("type"))
assertEquals("telInputId", textInput.getAttribute("id"))
assertEquals("0123456789", textInput.value)
}
@Test
fun telInputWithDefaults() = runTest {
composition { TelInput() }
val textInput = root.firstChild as HTMLInputElement
assertEquals("tel", textInput.getAttribute("type"))
assertEquals(null, textInput.getAttribute("id"))
assertEquals("", textInput.value)
}
@Test
fun textInput() = runTest {
composition {
TextInput(value = "Some value") {
id("textInputId")
}
}
val textInput = root.firstChild as HTMLInputElement
assertEquals("text", textInput.getAttribute("type"))
assertEquals("textInputId", textInput.getAttribute("id"))
assertEquals("Some value", textInput.value)
}
@Test
fun textInputWithDefaults() = runTest {
composition { TextInput() }
val textInput = root.firstChild as HTMLInputElement
assertEquals("text", textInput.getAttribute("type"))
assertEquals(null, textInput.getAttribute("id"))
assertEquals("", textInput.value)
}
@Test
fun timeInput() = runTest {
composition {
TimeInput(value = "12:20") {
id("timeInputId")
}
}
val textInput = root.firstChild as HTMLInputElement
assertEquals("time", textInput.getAttribute("type"))
assertEquals("timeInputId", textInput.getAttribute("id"))
assertEquals("12:20", textInput.value)
}
@Test
fun timeInputWithDefaults() = runTest {
composition { TimeInput() }
val textInput = root.firstChild as HTMLInputElement
assertEquals("time", textInput.getAttribute("type"))
assertEquals(null, textInput.getAttribute("id"))
assertEquals("", textInput.value)
}
@Test
fun urlInput() = runTest {
composition {
UrlInput(value = "http://127.0.0.1") {
id("urlInputId")
}
}
val textInput = root.firstChild as HTMLInputElement
assertEquals("url", textInput.getAttribute("type"))
assertEquals("urlInputId", textInput.getAttribute("id"))
assertEquals("http://127.0.0.1", textInput.value)
}
@Test
fun urlInputWithDefaults() = runTest {
composition { UrlInput() }
val textInput = root.firstChild as HTMLInputElement
assertEquals("url", textInput.getAttribute("type"))
assertEquals(null, textInput.getAttribute("id"))
assertEquals("", textInput.value)
}
@Test
fun weekInput() = runTest {
composition {
WeekInput(value = "2017-W01") {
id("weekInputId")
}
}
val textInput = root.firstChild as HTMLInputElement
assertEquals("week", textInput.getAttribute("type"))
assertEquals("weekInputId", textInput.getAttribute("id"))
assertEquals("2017-W01", textInput.value)
}
@Test
fun weekInputWithDefaults() = runTest {
composition { WeekInput() }
val textInput = root.firstChild as HTMLInputElement
assertEquals("week", textInput.getAttribute("type"))
assertEquals(null, textInput.getAttribute("id"))
assertEquals("", textInput.value)
}
}

15
web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt

@ -27,6 +27,7 @@ import kotlinx.browser.window
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.value
import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.css.*
import org.w3c.dom.url.URLSearchParams import org.w3c.dom.url.URLSearchParams
@ -258,7 +259,6 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
} }
onTextInput { onTextInput {
onChange(it.inputValue) onChange(it.inputValue)
println("On input = : ${it.nativeEvent.isComposing} - ${it.inputValue}")
} }
onKeyUp { onKeyUp {
println("On keyUp key = : ${it.getNormalizedKey()}") println("On keyUp key = : ${it.getNormalizedKey()}")
@ -266,15 +266,15 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
} }
) )
} }
Div( Div {
attrs = { Input(type = InputType.Checkbox, attrs = {
onCheckboxInput { onCheckboxInput {
println("From div - Checked: " + it.checked) println("From div - Checked: " + it.checked)
} }
} })
) { Input(type = InputType.Text, attrs = {
Input(type = InputType.Checkbox, attrs = {}) value(value = "Hi, ")
Input(value = "Hi, ") })
} }
Div { Div {
Input( Input(
@ -295,6 +295,7 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
name("f1") name("f1")
} }
) )
Input(type = InputType.Radio, attrs = {})
} }
} }

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

@ -32,7 +32,7 @@ internal val testCases = mutableMapOf<String, TestCase>()
fun launchTestCase(testCaseId: String) { fun launchTestCase(testCaseId: String) {
// this makes test cases get initialised: // this makes test cases get initialised:
listOf<Any>(TestCases1(), InputsTests()) listOf<Any>(TestCases1(), InputsTests(), EventsTests())
if (testCaseId !in testCases) error("Test Case '$testCaseId' not found") if (testCaseId !in testCases) error("Test Case '$testCaseId' not found")
@ -49,4 +49,4 @@ fun TestText(value: String, id: String = TEST_TEXT_DEFAULT_ID) {
) { ) {
Text(value) Text(value)
} }
} }

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

@ -0,0 +1,106 @@
package org.jetbrains.compose.web.sample.tests
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.css.selectors.attr
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.ElementBuilder.Companion.Div
class EventsTests {
val doubleClickUpdatesText by testCase {
var state by remember { mutableStateOf("") }
TestText(state)
Div(
attrs = {
id("box")
style {
width(100.px)
height(100.px)
backgroundColor("red")
}
onDoubleClick { state = "Double Click Works!" }
}
) {
Text("Clickable Box")
}
}
val focusInAndFocusOutUpdateTheText by testCase {
var state by remember { mutableStateOf("") }
TestText(state)
Input(type = InputType.Text, attrs = {
id("focusableInput")
onFocusIn {
state = "focused"
}
onFocusOut {
state = "not focused"
}
})
}
val focusAndBlurUpdateTheText by testCase {
var state by remember { mutableStateOf("") }
TestText(state)
Input(type = InputType.Text, attrs = {
id("focusableInput")
onFocus {
state = "focused"
}
onBlur {
state = "blured"
}
})
}
val scrollUpdatesText by testCase {
var state by remember { mutableStateOf("") }
TestText(state)
Div(
attrs = {
id("box")
style {
property("overflow-y", "scroll")
height(200.px)
backgroundColor(Color.RGB(220, 220, 220))
}
onScroll {
state = "Scrolled"
}
}
) {
repeat(500) {
P {
Text("Scrollable content in Div - $it")
}
}
}
}
val selectEventUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P(attrs = { style { height(50.px) } }) { TestText(state) }
Input(type = InputType.Text, attrs = {
value("This is a text to be selected")
id("selectableText")
onSelect {
state = "Text Selected"
}
})
}
}

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

@ -4,11 +4,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.attributes.checked import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.TextArea
class InputsTests { class InputsTests {
val textAreaInputGetsPrinted by testCase { val textAreaInputGetsPrinted by testCase {
@ -20,7 +17,7 @@ class InputsTests {
value = state, value = state,
attrs = { attrs = {
id("input") id("input")
onTextInput { state = it.inputValue } onInput { state = it.value }
} }
) )
} }
@ -32,10 +29,10 @@ class InputsTests {
Input( Input(
type = InputType.Text, type = InputType.Text,
value = state,
attrs = { attrs = {
value(state)
id("input") id("input")
onTextInput { state = it.inputValue } onInput { state = it.value }
} }
) )
} }
@ -52,9 +49,7 @@ class InputsTests {
if (checked) { if (checked) {
checked() checked()
} }
onCheckboxInput { onInput { checked = !checked }
checked = !checked
}
} }
) )
} }
@ -70,9 +65,7 @@ class InputsTests {
id("r1") id("r1")
name("f1") name("f1")
onRadioInput { onInput { text = "r1" }
text = "r1"
}
} }
) )
@ -82,9 +75,7 @@ class InputsTests {
id("r2") id("r2")
name("f1") name("f1")
onRadioInput { onInput { text = "r2" }
text = "r2"
}
} }
) )
} }
@ -102,9 +93,8 @@ class InputsTests {
attr("max", "100") attr("max", "100")
attr("step", "5") attr("step", "5")
onRangeInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value rangeState = it.value?.toInt() ?: 0
rangeState = value.toInt()
} }
} }
) )
@ -119,9 +109,8 @@ class InputsTests {
type = InputType.Time, type = InputType.Time,
attrs = { attrs = {
id("time") id("time")
onGenericInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value timeState = it.value
timeState = value
} }
} }
) )
@ -136,9 +125,8 @@ class InputsTests {
type = InputType.Date, type = InputType.Date,
attrs = { attrs = {
id("date") id("date")
onGenericInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value timeState = it.value
timeState = value
} }
} }
) )
@ -153,9 +141,8 @@ class InputsTests {
type = InputType.DateTimeLocal, type = InputType.DateTimeLocal,
attrs = { attrs = {
id("dateTimeLocal") id("dateTimeLocal")
onGenericInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value timeState = it.value
timeState = value
} }
} }
) )
@ -170,9 +157,8 @@ class InputsTests {
type = InputType.File, type = InputType.File,
attrs = { attrs = {
id("file") id("file")
onGenericInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value filePath = it.value
filePath = value
} }
} }
) )
@ -187,11 +173,135 @@ class InputsTests {
type = InputType.Color, type = InputType.Color,
attrs = { attrs = {
id("color") id("color")
onGenericInput { onInput {
val value: String = it.nativeEvent.target.asDynamic().value color = it.value
color = value
} }
} }
) )
} }
}
val invalidInputUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P { TestText(state) }
Form(attrs = {
action("#")
}) {
Input(type = InputType.Number, attrs = {
id("numberInput")
min("1")
max("5")
onInvalid {
state = "INVALID VALUE ENTERED"
}
onInput { state = "SOMETHING ENTERED" }
})
Input(type = InputType.Submit, attrs = {
value("submit")
id("submitBtn")
})
}
}
val changeEventUpdatesText by testCase {
var state by remember { mutableStateOf("None") }
P { TestText(state) }
Div {
Input(type = InputType.Number, attrs = {
id("numberInput")
onChange { state = "INPUT HAS CHANGED" }
})
}
}
val stopOnInputImmediatePropagationWorks by testCase {
var state by remember { mutableStateOf("None") }
var shouldStopImmediatePropagation by remember { mutableStateOf(false) }
P { TestText(state) }
Div {
Input(type = InputType.Radio, attrs = {
id("radioBtn")
onInput {
shouldStopImmediatePropagation = true
state = "None"
}
})
Input(type = InputType.Checkbox, attrs = {
id("checkbox")
onInput {
if (shouldStopImmediatePropagation) it.stopImmediatePropagation()
state = "onInput1"
}
onInput { state = "onInput2" }
})
}
}
val preventDefaultWorks by testCase {
var state by remember { mutableStateOf("None") }
var state2 by remember { mutableStateOf("None") }
P { TestText(state) }
P { TestText(state2, id = "txt2") }
Input(
type = InputType.Checkbox,
attrs = {
id("checkbox")
onClick {
state = "Clicked but check should be prevented"
it.nativeEvent.preventDefault()
}
onInput {
state2 = "This text should never be displayed as onClick calls preventDefault()"
}
}
)
}
val stopPropagationWorks by testCase {
var state by remember { mutableStateOf("None") }
var state2 by remember { mutableStateOf("None") }
var shouldStopPropagation by remember { mutableStateOf(false) }
P { TestText(state) }
P { TestText(state2, id = "txt2") }
Div {
Input(type = InputType.Radio, attrs = {
id("radioBtn")
onInput {
shouldStopPropagation = true
state = "None"
state2 = "None"
}
})
Div(attrs = {
addEventListener(EventsListenerBuilder.INPUT) {
state2 = "div caught an input"
}
}) {
Input(type = InputType.Checkbox, attrs = {
id("checkbox")
onInput {
if (shouldStopPropagation) it.stopPropagation()
state = "childInput"
}
})
}
}
}
}

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

@ -0,0 +1,174 @@
/*
* 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.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.Keys
import org.openqa.selenium.interactions.Actions
import org.openqa.selenium.WebDriver
class EventTests : BaseIntegrationTests() {
@ResolveDrivers
fun `double click updates text`(driver: WebDriver) {
driver.openTestPage("doubleClickUpdatesText")
val box = driver.findElement(By.id("box"))
val actions = Actions(driver)
actions
.doubleClick(box)
.perform()
driver.waitTextToBe(value = "Double Click Works!", textId = "txt")
}
@ResolveDrivers
fun `focusin and focusout update the text`(driver: WebDriver) {
driver.openTestPage("focusInAndFocusOutUpdateTheText")
driver.waitTextToBe(value = "", textId = "txt")
val input = driver.findElement(By.id("focusableInput"))
val actions = Actions(driver)
actions.moveToElement(input)
.click()
.perform()
driver.waitTextToBe(value = "focused", textId = "txt")
val actions2 = Actions(driver)
actions2.moveToElement(driver.findElement(By.id("txt")))
.click()
.perform()
driver.waitTextToBe(value = "not focused", textId = "txt")
}
@ResolveDrivers
fun `focus and blur update the text`(driver: WebDriver) {
driver.openTestPage("focusAndBlurUpdateTheText")
driver.waitTextToBe(value = "", textId = "txt")
val input = driver.findElement(By.id("focusableInput"))
val actions = Actions(driver)
actions.moveToElement(input)
.click()
.perform()
driver.waitTextToBe(value = "focused", textId = "txt")
val actions2 = Actions(driver)
actions2.moveToElement(driver.findElement(By.id("txt")))
.click()
.perform()
driver.waitTextToBe(value = "blured", textId = "txt")
}
@ResolveDrivers
fun `scroll updates the text`(driver: WebDriver) {
driver.openTestPage("scrollUpdatesText")
driver.waitTextToBe(value = "", textId = "txt")
val box = driver.findElement(By.id("box"))
val actions = Actions(driver)
actions.moveToElement(box)
.click()
.sendKeys(Keys.ARROW_DOWN)
.perform()
driver.waitTextToBe(value = "Scrolled", textId = "txt")
}
@ResolveDrivers
fun `select event update the txt`(driver: WebDriver) {
driver.openTestPage("selectEventUpdatesText")
driver.waitTextToBe(value = "None")
val selectableText = driver.findElement(By.id("selectableText"))
val action = Actions(driver)
action.moveToElement(selectableText,3,3)
.click().keyDown(Keys.SHIFT)
.moveToElement(selectableText,200, 0)
.click().keyUp(Keys.SHIFT)
.build()
.perform()
driver.waitTextToBe(value = "Text Selected")
}
@ResolveDrivers
fun `stopImmediatePropagation prevents consequent listeners from being called`(driver: WebDriver) {
driver.openTestPage("stopOnInputImmediatePropagationWorks")
driver.waitTextToBe(value = "None")
val checkBox = driver.findElement(By.id("checkbox"))
val radioButtonToStopImmediatePropagation = driver.findElement(By.id("radioBtn"))
checkBox.click()
driver.waitTextToBe(value = "onInput2")
radioButtonToStopImmediatePropagation.click()
driver.waitTextToBe(value = "None")
checkBox.click()
driver.waitTextToBe(value = "onInput1")
}
@ResolveDrivers
fun `preventDefault works as expected`(driver: WebDriver) {
driver.openTestPage("preventDefaultWorks")
driver.waitTextToBe(value = "None")
driver.waitTextToBe(textId = "txt2", value = "None")
val checkBox = driver.findElement(By.id("checkbox"))
checkBox.click()
driver.waitTextToBe(value = "Clicked but check should be prevented")
driver.waitTextToBe(textId = "txt2", value = "None")
}
@ResolveDrivers
fun `stopPropagation works as expected`(driver: WebDriver) {
driver.openTestPage("stopPropagationWorks")
driver.waitTextToBe(value = "None")
driver.waitTextToBe(textId = "txt2", value = "None")
val checkBox = driver.findElement(By.id("checkbox"))
val radioButtonToStopImmediatePropagation = driver.findElement(By.id("radioBtn"))
checkBox.click()
driver.waitTextToBe(value = "childInput")
driver.waitTextToBe(textId = "txt2", value = "div caught an input")
radioButtonToStopImmediatePropagation.click()
driver.waitTextToBe(value = "None")
driver.waitTextToBe(textId = "txt2", value = "None")
checkBox.click()
driver.waitTextToBe(value = "childInput")
driver.waitTextToBe(textId = "txt2", value = "None")
}
}

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

@ -134,4 +134,33 @@ class InputsTests : BaseIntegrationTests() {
driver.waitTextToBe(value = "C:\\fakepath\\index.html") driver.waitTextToBe(value = "C:\\fakepath\\index.html")
} }
}
@ResolveDrivers
fun `onInvalid updates the text`(driver: WebDriver) {
driver.openTestPage("invalidInputUpdatesText")
driver.waitTextToBe(value = "None")
val input = driver.findElement(By.id("numberInput"))
val submitBtn = driver.findElement(By.id("submitBtn"))
input.sendKeys("1000")
driver.waitTextToBe(value = "SOMETHING ENTERED")
submitBtn.click()
driver.waitTextToBe(value = "INVALID VALUE ENTERED")
}
@ResolveDrivers
fun `onChange updates the text`(driver: WebDriver) {
driver.openTestPage("changeEventUpdatesText")
driver.waitTextToBe(value = "None")
val input = driver.findElement(By.id("numberInput"))
input.sendKeys("1")
driver.waitTextToBe(value = "None")
driver.findElement(By.id("txt")).click() // to change the focus - triggers onChange
driver.waitTextToBe(value = "INPUT HAS CHANGED")
}
}

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

@ -72,4 +72,4 @@ abstract class BaseIntegrationTests() {
) )
} }
} }
} }

3
web/widgets/src/jsMain/kotlin/layouts/slider.kt

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import org.jetbrains.compose.common.ui.Modifier import org.jetbrains.compose.common.ui.Modifier
import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.value
@Composable @Composable
actual fun SliderActual( actual fun SliderActual(
@ -18,8 +19,8 @@ actual fun SliderActual(
Input( Input(
type = InputType.Range, type = InputType.Range,
value = value.toString(),
attrs = { attrs = {
value(value.toString())
attr("min", valueRange.start.toString()) attr("min", valueRange.start.toString())
attr("max", valueRange.endInclusive.toString()) attr("max", valueRange.endInclusive.toString())
attr("step", step.toString()) attr("step", step.toString())

Loading…
Cancel
Save