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)
}) {
repeat(count) { ix ->
Input(type = InputType.Radio, value = "snippet$ix", attrs = {
Input(type = InputType.Radio, attrs = {
name("code-snippet")
value("snippet$ix")
id("snippet$ix")
if (current == ix) checked()
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.dom.setProperty
import org.jetbrains.compose.web.dom.setVariable
import kotlinx.browser.document
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
@ -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) {
private var currentListeners = emptyList<WrappedEventListener<*>>()
@ -114,4 +121,4 @@ class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) {
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 */
fun AttrsBuilder<HTMLInputElement>.type(value: InputType) =
fun AttrsBuilder<HTMLInputElement>.type(value: InputType<*>) =
attr("type", value.typeStr)
fun AttrsBuilder<HTMLInputElement>.accept(value: String) =
@ -184,7 +184,7 @@ fun AttrsBuilder<HTMLInputElement>.size(value: Int) =
fun AttrsBuilder<HTMLInputElement>.src(value: String) =
attr("src", value) // image only
fun AttrsBuilder<HTMLInputElement>.step(value: Int) =
fun AttrsBuilder<HTMLInputElement>.step(value: Number) =
attr("step", value.toString()) // numeric types only
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.HTMLElement
class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
private val attributesMap = mutableMapOf<String, String>()
open class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
internal val attributesMap = mutableMapOf<String, String>()
val styleBuilder = StyleBuilderImpl()
val propertyUpdates = mutableListOf<Pair<(Element, Any) -> Unit, Any>>()
@ -48,6 +48,16 @@ class AttrsBuilder<TElement : Element> : EventsListenerBuilder() {
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 {
const val CLASS = "class"
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 {
private val listeners = mutableListOf<WrappedEventListener<*>>()
protected val listeners = mutableListOf<WrappedEventListener<*>>()
fun onCopy(options: Options = Options.DEFAULT, listener: (WrappedClipboardEvent) -> Unit) {
listeners.add(ClipboardEventListener(COPY, options, listener))
@ -42,35 +42,6 @@ open class EventsListenerBuilder {
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(
options: Options = Options.DEFAULT,
listener: (GenericWrappedEvent<*>) -> Unit
@ -224,6 +195,10 @@ open class EventsListenerBuilder {
listeners.add(WrappedEventListener(eventName, options, listener))
}
internal fun copyListenersFrom(from: EventsListenerBuilder) {
listeners.addAll(from.listeners)
}
companion object {
const val COPY = "copy"
const val CUT = "cut"
@ -274,4 +249,4 @@ open class EventsListenerBuilder {
const val DRAGENTER = "dragenter"
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
sealed class InputType(val typeStr: String) {
object Button : InputType("button")
object Checkbox : InputType("checkbox")
object Color : InputType("color")
object Date : InputType("date")
object DateTimeLocal : InputType("datetime-local")
object Email : InputType("email")
object File : InputType("file")
object Hidden : InputType("hidden")
object Month : InputType("month")
object Number : InputType("number")
object Password : InputType("password")
object Radio : InputType("radio")
object Range : InputType("range")
object Search : InputType("search")
object Submit : InputType("submit")
object Tel : InputType("tel")
object Text : InputType("text")
object Time : InputType("time")
object Url : InputType("url")
object Week : InputType("week")
import org.w3c.dom.events.Event
sealed class InputType<T>(val typeStr: String) {
object Button : InputTypeWithUnitValue("button")
object Checkbox : InputTypeCheckedValue("checkbox")
object Color : InputTypeWithStringValue("color")
object Date : InputTypeWithStringValue("date")
object DateTimeLocal : InputTypeWithStringValue("datetime-local")
object Email : InputTypeWithStringValue("email")
object File : InputTypeWithStringValue("file")
object Hidden : InputTypeWithStringValue("hidden")
object Month : InputTypeWithStringValue("month")
object Number : InputTypeNumberValue("number")
object Password : InputTypeWithStringValue("password")
object Radio : InputTypeCheckedValue("radio")
object Range : InputTypeNumberValue("range")
object Search : InputTypeWithStringValue("search")
object Submit : InputTypeWithUnitValue("submit")
object Tel : InputTypeWithStringValue("tel")
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) {

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)
} else false
}
internal fun copyFrom(sb: StyleBuilderImpl) {
properties.addAll(sb.properties)
variables.addAll(sb.variables)
}
}
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),
applyAttrs = applyAttrs,
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.ComposeNode
import androidx.compose.web.attributes.InputAttrsBuilder
import androidx.compose.web.attributes.TextAreaAttrsBuilder
import org.jetbrains.compose.web.DomApplier
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 org.jetbrains.compose.web.attributes.*
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLAreaElement
import org.w3c.dom.HTMLAudioElement
@ -30,7 +23,6 @@ import org.w3c.dom.HTMLHeadingElement
import org.w3c.dom.HTMLHRElement
import org.w3c.dom.HTMLIFrameElement
import org.w3c.dom.HTMLImageElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.HTMLLIElement
import org.w3c.dom.HTMLLabelElement
import org.w3c.dom.HTMLLegendElement
@ -55,7 +47,6 @@ import org.w3c.dom.HTMLTableColElement
import org.w3c.dom.HTMLTableElement
import org.w3c.dom.HTMLTableRowElement
import org.w3c.dom.HTMLTableSectionElement
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.HTMLTrackElement
import org.w3c.dom.HTMLUListElement
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
fun Button(
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
@ -569,15 +541,17 @@ fun Section(
@Composable
fun TextArea(
attrs: AttrBuilderContext<HTMLTextAreaElement>? = null,
attrs: (TextAreaAttrsBuilder.() -> Unit)? = null,
value: String
) = TagElement(
elementBuilder = ElementBuilder.TextArea,
applyAttrs = {
value(value)
val taab = TextAreaAttrsBuilder()
if (attrs != null) {
attrs()
taab.attrs()
}
taab.value(value)
this.copyFrom(taab)
}
) {
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>
open class WrappedInputEvent(
override val nativeEvent: InputEvent
) : GenericWrappedEvent<InputEvent>
override val nativeEvent: Event
) : GenericWrappedEvent<Event>
open class WrappedKeyboardEvent(
override val nativeEvent: KeyboardEvent
@ -88,7 +88,7 @@ open class WrappedClipboardEvent(
) : GenericWrappedEvent<ClipboardEvent>
class WrappedTextInputEvent(
nativeEvent: InputEvent,
nativeEvent: Event,
val inputValue: String
) : 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.getValue
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.forId
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import kotlin.test.Test
import kotlin.test.assertEquals
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
fun labelForIdAttrAppliedProperly() = runTest {
@ -151,4 +241,4 @@ class AttributesTests {
waitChanges()
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(
type = InputType.Checkbox,
attrs = {
onCheckboxInput { handeled = true }
onInput { handeled = true }
}
)
}
@ -63,7 +63,7 @@ class EventTests {
Input(
type = InputType.Radio,
attrs = {
onRadioInput { handeled = true }
onInput { handeled = true }
}
)
}
@ -82,7 +82,7 @@ class EventTests {
composition {
TextArea(
{
onTextInput { handeled = true }
onInput { handeled = true }
},
value = ""
)
@ -95,4 +95,4 @@ class EventTests {
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.delay
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.value
import org.jetbrains.compose.web.css.*
import org.w3c.dom.url.URLSearchParams
@ -258,7 +259,6 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
}
onTextInput {
onChange(it.inputValue)
println("On input = : ${it.nativeEvent.isComposing} - ${it.inputValue}")
}
onKeyUp {
println("On keyUp key = : ${it.getNormalizedKey()}")
@ -266,15 +266,15 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
}
)
}
Div(
attrs = {
Div {
Input(type = InputType.Checkbox, attrs = {
onCheckboxInput {
println("From div - Checked: " + it.checked)
}
}
) {
Input(type = InputType.Checkbox, attrs = {})
Input(value = "Hi, ")
})
Input(type = InputType.Text, attrs = {
value(value = "Hi, ")
})
}
Div {
Input(
@ -295,6 +295,7 @@ fun MyInputComponent(text: State<String>, onChange: (String) -> Unit) {
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) {
// 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")
@ -49,4 +49,4 @@ fun TestText(value: String, id: String = TEST_TEXT_DEFAULT_ID) {
) {
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.remember
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.checked
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.TextArea
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.dom.*
class InputsTests {
val textAreaInputGetsPrinted by testCase {
@ -20,7 +17,7 @@ class InputsTests {
value = state,
attrs = {
id("input")
onTextInput { state = it.inputValue }
onInput { state = it.value }
}
)
}
@ -32,10 +29,10 @@ class InputsTests {
Input(
type = InputType.Text,
value = state,
attrs = {
value(state)
id("input")
onTextInput { state = it.inputValue }
onInput { state = it.value }
}
)
}
@ -52,9 +49,7 @@ class InputsTests {
if (checked) {
checked()
}
onCheckboxInput {
checked = !checked
}
onInput { checked = !checked }
}
)
}
@ -70,9 +65,7 @@ class InputsTests {
id("r1")
name("f1")
onRadioInput {
text = "r1"
}
onInput { text = "r1" }
}
)
@ -82,9 +75,7 @@ class InputsTests {
id("r2")
name("f1")
onRadioInput {
text = "r2"
}
onInput { text = "r2" }
}
)
}
@ -102,9 +93,8 @@ class InputsTests {
attr("max", "100")
attr("step", "5")
onRangeInput {
val value: String = it.nativeEvent.target.asDynamic().value
rangeState = value.toInt()
onInput {
rangeState = it.value?.toInt() ?: 0
}
}
)
@ -119,9 +109,8 @@ class InputsTests {
type = InputType.Time,
attrs = {
id("time")
onGenericInput {
val value: String = it.nativeEvent.target.asDynamic().value
timeState = value
onInput {
timeState = it.value
}
}
)
@ -136,9 +125,8 @@ class InputsTests {
type = InputType.Date,
attrs = {
id("date")
onGenericInput {
val value: String = it.nativeEvent.target.asDynamic().value
timeState = value
onInput {
timeState = it.value
}
}
)
@ -153,9 +141,8 @@ class InputsTests {
type = InputType.DateTimeLocal,
attrs = {
id("dateTimeLocal")
onGenericInput {
val value: String = it.nativeEvent.target.asDynamic().value
timeState = value
onInput {
timeState = it.value
}
}
)
@ -170,9 +157,8 @@ class InputsTests {
type = InputType.File,
attrs = {
id("file")
onGenericInput {
val value: String = it.nativeEvent.target.asDynamic().value
filePath = value
onInput {
filePath = it.value
}
}
)
@ -187,11 +173,135 @@ class InputsTests {
type = InputType.Color,
attrs = {
id("color")
onGenericInput {
val value: String = it.nativeEvent.target.asDynamic().value
color = value
onInput {
color = it.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")
}
}
@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.web.dom.Input
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.value
@Composable
actual fun SliderActual(
@ -18,8 +19,8 @@ actual fun SliderActual(
Input(
type = InputType.Range,
value = value.toString(),
attrs = {
value(value.toString())
attr("min", valueRange.start.toString())
attr("max", valueRange.endInclusive.toString())
attr("step", step.toString())

Loading…
Cancel
Save