diff --git a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt b/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt index 267ee2b4bb..30fe58fd9a 100644 --- a/web/benchmark-core/src/jsMain/kotlin/com/sample/content/CodeSamplesSwitcher.kt +++ b/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) } diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt index 4be7a3d7d9..fc81be01b7 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/DomApplier.kt +++ b/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>() @@ -114,4 +121,4 @@ class DomElementWrapper(override val node: HTMLElement): DomNodeWrapper(node) { setVariable(node.style, name, value) } } -} \ No newline at end of file +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt index a0b982dfaa..a8fdc106ab 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/Attrs.kt @@ -94,7 +94,7 @@ fun AttrsBuilder.target(value: FormTarget) = /* Input attributes */ -fun AttrsBuilder.type(value: InputType) = +fun AttrsBuilder.type(value: InputType<*>) = attr("type", value.typeStr) fun AttrsBuilder.accept(value: String) = @@ -184,7 +184,7 @@ fun AttrsBuilder.size(value: Int) = fun AttrsBuilder.src(value: String) = attr("src", value) // image only -fun AttrsBuilder.step(value: Int) = +fun AttrsBuilder.step(value: Number) = attr("step", value.toString()) // numeric types only fun AttrsBuilder.valueAttr(value: String) = diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt index 9df4554d9b..dfd9f4a02e 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/AttrsBuilder.kt +++ b/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 : EventsListenerBuilder() { - private val attributesMap = mutableMapOf() +open class AttrsBuilder : EventsListenerBuilder() { + internal val attributesMap = mutableMapOf() val styleBuilder = StyleBuilderImpl() val propertyUpdates = mutableListOf Unit, Any>>() @@ -48,6 +48,16 @@ class AttrsBuilder : EventsListenerBuilder() { return attributesMap } + internal fun copyFrom(attrsBuilder: AttrsBuilder) { + 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" diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt index edf8e00bc5..31ac0119d2 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/EventsListenerBuilder.kt +++ b/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>() + protected val listeners = mutableListOf>() 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" } -} \ No newline at end of file +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/InputAttrsBuilder.kt new file mode 100644 index 0000000000..4d96b08a30 --- /dev/null +++ b/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( + 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() + 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 = nativeEvent.composedPath() +} + +class InputAttrsBuilder(val inputType: InputType) : AttrsBuilder() { + + fun onInput(options: Options = Options.DEFAULT, listener: (SyntheticInputEvent) -> Unit) { + addEventListener(INPUT, options) { + val value = inputType.inputValue(it.nativeEvent) + listener(SyntheticInputEvent(value, it.nativeEvent.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)) + } +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt index 13fe8fb78d..1d2b604d5e 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/PredefinedAttrValues.kt +++ b/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(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(name) { + override fun inputValue(event: Event) = Week.valueAsString(event) + } + + open class InputTypeWithUnitValue(name: String) : InputType(name) { + override fun inputValue(event: Event) = Unit + } + + open class InputTypeCheckedValue(name: String) : InputType(name) { + override fun inputValue(event: Event): Boolean { + return event.target?.asDynamic()?.checked?.unsafeCast() ?: false + } + } + + open class InputTypeNumberValue(name: String) : InputType(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() ?: "" + } } sealed class DirType(val dirStr: String) { diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/attributes/TextAreaAttrsBuilder.kt new file mode 100644 index 0000000000..9231233a18 --- /dev/null +++ b/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() { + + fun onInput( + options: Options = Options.DEFAULT, + listener: (SyntheticInputEvent) -> Unit + ) { + addEventListener(INPUT, options) { + val text = it.nativeEvent.target.asDynamic().value.unsafeCast() + listener(SyntheticInputEvent(text, it.nativeEvent.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)) + } +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt index 048ca27a70..b781fdd4ec 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/css/StyleBuilder.kt +++ b/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( diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt index fac3bc1d94..784c5dec87 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Base.kt @@ -241,4 +241,4 @@ fun TagElement( elementBuilder = ElementBuilder.createBuilder(tagName), applyAttrs = applyAttrs, content = content -) \ No newline at end of file +) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt index 96f40112f0..2ba3370443 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/Elements.kt +++ b/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? = null -) { - TagElement( - elementBuilder = ElementBuilder.Input, - applyAttrs = { - type(type) - value(value) - if (attrs != null) { - attrs() - } - }, - content = null - ) -} - @Composable fun Button( attrs: AttrBuilderContext? = null, @@ -569,15 +541,17 @@ fun Section( @Composable fun TextArea( - attrs: AttrBuilderContext? = 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) diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/elements/InputElements.kt new file mode 100644 index 0000000000..ce0c7695e0 --- /dev/null +++ b/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.applyAttrsWithStringValue( + value: String, + attrsBuilder: InputAttrsBuilder.() -> Unit +) { + if (value.isNotEmpty()) value(value) + attrsBuilder() +} + +@Composable +@NonRestartableComposable +fun CheckboxInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input( + type = InputType.Checkbox, + attrs = { + if (checked) checked() + this.attrsBuilder() + } + ) +} + +@Composable +@NonRestartableComposable +fun DateInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Date, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun DateTimeLocalInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.DateTimeLocal, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun EmailInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Email, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun FileInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.File, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun HiddenInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Hidden, attrs = attrsBuilder) +} + +@Composable +@NonRestartableComposable +fun MonthInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Month, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun NumberInput( + value: Number? = null, + min: Number? = null, + max: Number? = null, + attrsBuilder: InputAttrsBuilder.() -> 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.() -> Unit = {}) { + Input(type = InputType.Password, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun RadioInput(checked: Boolean = false, attrsBuilder: InputAttrsBuilder.() -> 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.() -> 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.() -> Unit = {}) { + Input(type = InputType.Search, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun SubmitInput(attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Submit, attrs = attrsBuilder) +} + +@Composable +@NonRestartableComposable +fun TelInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Tel, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun TextInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Text, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun TimeInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Time, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun UrlInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Url, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +@NonRestartableComposable +fun WeekInput(value: String = "", attrsBuilder: InputAttrsBuilder.() -> Unit = {}) { + Input(type = InputType.Week, attrs = { applyAttrsWithStringValue(value, attrsBuilder) }) +} + +@Composable +fun Input( + type: InputType, + attrs: InputAttrsBuilder.() -> Unit +) { + TagElement( + elementBuilder = ElementBuilder.Input, + applyAttrs = { + val inputAttrsBuilder = InputAttrsBuilder(type) + inputAttrsBuilder.type(type) + inputAttrsBuilder.attrs() + this.copyFrom(inputAttrsBuilder) + }, + content = null + ) +} + +@Composable +fun Input(type: InputType) { + TagElement( + elementBuilder = ElementBuilder.Input, + applyAttrs = { + val inputAttrsBuilder = InputAttrsBuilder(type) + inputAttrsBuilder.type(type) + this.copyFrom(inputAttrsBuilder) + }, + content = null + ) +} diff --git a/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt b/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt index 9eda388e13..4c967234ce 100644 --- a/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt +++ b/web/core/src/jsMain/kotlin/androidx/compose/web/events/WrappedEvent.kt @@ -34,8 +34,8 @@ open class WrappedWheelEvent( ) : GenericWrappedEvent open class WrappedInputEvent( - override val nativeEvent: InputEvent -) : GenericWrappedEvent + override val nativeEvent: Event +) : GenericWrappedEvent open class WrappedKeyboardEvent( override val nativeEvent: KeyboardEvent @@ -88,7 +88,7 @@ open class WrappedClipboardEvent( ) : GenericWrappedEvent class WrappedTextInputEvent( - nativeEvent: InputEvent, + nativeEvent: Event, val inputValue: String ) : WrappedInputEvent(nativeEvent) diff --git a/web/core/src/jsTest/kotlin/elements/AttributesTests.kt b/web/core/src/jsTest/kotlin/elements/AttributesTests.kt index c8a6d9aae8..9815743a3d 100644 --- a/web/core/src/jsTest/kotlin/elements/AttributesTests.kt +++ b/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().apply { + id("id1") + classes("a b c") + attr("title", "customTitle") + + prop({_, _ ->}, "Value") + + ref { onDispose { } } + style { + width(500.px) + backgroundColor("red") + } + + onClick { } + onFocusIn { } + onMouseEnter { } + } + + val copyToAttrsBuilder = AttrsBuilder().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().apply { + attr("title", "customTitle") + } + + val copyToAttrsBuilder = AttrsBuilder().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().apply { + attr("title", "customTitleNew") + } + + val copyToAttrsBuilder = AttrsBuilder().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("
", root.innerHTML) } -} \ No newline at end of file +} diff --git a/web/core/src/jsTest/kotlin/elements/EventTests.kt b/web/core/src/jsTest/kotlin/elements/EventTests.kt index 348c9beb27..2f0307fcb9 100644 --- a/web/core/src/jsTest/kotlin/elements/EventTests.kt +++ b/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) } -} \ No newline at end of file +} diff --git a/web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt b/web/core/src/jsTest/kotlin/elements/InputsGenerateCorrectHtmlTests.kt new file mode 100644 index 0000000000..eee6de4284 --- /dev/null +++ b/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) + } +} diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt index 52dd0aa20c..e6609db73e 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/Sample.kt +++ b/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, 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, 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, onChange: (String) -> Unit) { name("f1") } ) + Input(type = InputType.Radio, attrs = {}) } } diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt index 89ae79daa1..1a941f28e9 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/Common.kt @@ -32,7 +32,7 @@ internal val testCases = mutableMapOf() fun launchTestCase(testCaseId: String) { // this makes test cases get initialised: - listOf(TestCases1(), InputsTests()) + listOf(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) } -} \ No newline at end of file +} diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/EventsTests.kt new file mode 100644 index 0000000000..1442064a34 --- /dev/null +++ b/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" + } + }) + } +} diff --git a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt index 53a88f11c4..8adab2dbdd 100644 --- a/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt +++ b/web/integration-core/src/jsMain/kotlin/androidx/compose/web/sample/tests/InputsTests.kt @@ -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 } } ) } -} \ No newline at end of file + + 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" + } + }) + } + } + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/EventTests.kt new file mode 100644 index 0000000000..1212e19302 --- /dev/null +++ b/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") + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt index 3cc00ddcbb..3fbd441902 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt +++ b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/InputsTests.kt @@ -134,4 +134,33 @@ class InputsTests : BaseIntegrationTests() { driver.waitTextToBe(value = "C:\\fakepath\\index.html") } -} \ No newline at end of file + + @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") + } +} diff --git a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt index bfd6a828ec..7fb441d7ec 100644 --- a/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt +++ b/web/integration-core/src/jvmTest/kotlin/org/jetbrains/compose/web/tests/integration/common/BaseIntegrationTests.kt @@ -72,4 +72,4 @@ abstract class BaseIntegrationTests() { ) } } -} \ No newline at end of file +} diff --git a/web/widgets/src/jsMain/kotlin/layouts/slider.kt b/web/widgets/src/jsMain/kotlin/layouts/slider.kt index c5d88ea5f6..ed0f194469 100644 --- a/web/widgets/src/jsMain/kotlin/layouts/slider.kt +++ b/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())