From 1b0e85f10507c66cd9ea1d5ff2ee8d6a05030eef Mon Sep 17 00:00:00 2001 From: Andrey Kiryushin Date: Sat, 29 Jan 2022 16:17:48 +0300 Subject: [PATCH 1/5] kmdc-select (#22) --- .../src/jsMain/kotlin/MDCMenuSurface.kt | 2 + kmdc/kmdc-select/build.gradle.kts | 23 ++ .../src/jsMain/kotlin/MDCSelect.kt | 351 ++++++++++++++++++ kmdc/kmdc-select/src/jsMain/kotlin/events.kt | 11 + kotlin-js-store/yarn.lock | 23 ++ sandbox/kotlin-js-store/yarn.lock | 23 ++ sandbox/src/jsMain/kotlin/samples/Select.kt | 156 ++++++++ 7 files changed, 589 insertions(+) create mode 100644 kmdc/kmdc-select/build.gradle.kts create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/events.kt create mode 100644 sandbox/src/jsMain/kotlin/samples/Select.kt diff --git a/kmdc/kmdc-menu-surface/src/jsMain/kotlin/MDCMenuSurface.kt b/kmdc/kmdc-menu-surface/src/jsMain/kotlin/MDCMenuSurface.kt index 0cc32fe1..600006cf 100644 --- a/kmdc/kmdc-menu-surface/src/jsMain/kotlin/MDCMenuSurface.kt +++ b/kmdc/kmdc-menu-surface/src/jsMain/kotlin/MDCMenuSurface.kt @@ -14,6 +14,7 @@ private external val MDCMenuSurfaceStyle: dynamic public data class MDCMenuSurfaceOpts( public var fixed: Boolean = false, + public var fullwidth: Boolean = false ) public open class MDCMenuSurfaceAttrsScope : AttrsBuilder() @@ -34,6 +35,7 @@ public fun MDCMenuSurface( attrs = { classes("mdc-menu-surface") if (options.fixed) classes("mdc-menu-surface--fixed") + if (options.fullwidth) classes("mdc-menu-surface--fullwidth") initialiseMDC(MDCMenuSurfaceModule.MDCMenuSurface::attachTo) attrs?.invoke(this.unsafeCast()) }, diff --git a/kmdc/kmdc-select/build.gradle.kts b/kmdc/kmdc-select/build.gradle.kts new file mode 100644 index 00000000..d041cf7a --- /dev/null +++ b/kmdc/kmdc-select/build.gradle.kts @@ -0,0 +1,23 @@ +import util.mdcVersion + +plugins { + id("plugin.library-compose") + id("plugin.publishing-mpp") +} + +description = "Compose Multiplatform Kotlin/JS wrappers for @material/select" + +kotlin { + sourceSets { + commonMain + jsMain { + dependencies { + api(project(":kmdc:kmdc-core")) + api(project(":kmdc:kmdc-list")) + api(project(":kmdc:kmdc-menu-surface")) + api(npm("@material/select", mdcVersion)) + api(compose.web.svg) + } + } + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt new file mode 100644 index 00000000..3090f131 --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt @@ -0,0 +1,351 @@ +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import dev.petuska.kmdc.core.Builder +import dev.petuska.kmdc.core.ComposableBuilder +import dev.petuska.kmdc.core.MDCAttrsDsl +import dev.petuska.kmdc.core.MDCDsl +import dev.petuska.kmdc.core.MDCEvent +import dev.petuska.kmdc.core.aria +import dev.petuska.kmdc.core.mdc +import dev.petuska.kmdc.core.role +import dev.petuska.kmdc.core.uniqueDomElementId +import dev.petuska.kmdc.list.MDCList +import dev.petuska.kmdc.list.MDCListItem +import dev.petuska.kmdc.list.MDCListItemGraphic +import dev.petuska.kmdc.list.MDCListItemText +import dev.petuska.kmdc.menu.surface.MDCMenuSurface +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.attributes.AttrsBuilder +import org.jetbrains.compose.web.attributes.name +import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.dom.ContentBuilder +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.HiddenInput +import org.jetbrains.compose.web.dom.I +import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text +import org.jetbrains.compose.web.svg.Polygon +import org.jetbrains.compose.web.svg.Svg +import org.w3c.dom.Element +import org.w3c.dom.HTMLDivElement +import org.w3c.dom.HTMLElement + +@JsModule("@material/select/dist/mdc.select.css") +private external val MDCSelectCSS: dynamic + +@JsModule("@material/select") +public external object MDCSelectModule { + internal class MDCSelect(element: Element) { + companion object { + fun attachTo(element: Element): MDCSelect + } + + fun destroy() + + var value: String? + + fun setValue(value: String?, skipNotify: Boolean) + + var disabled: Boolean + + var required: Boolean + } + + public class MDCSelectChangeEventDetail { + public val value: String + public val index: Int + } + + public class MDCSelectChangeEvent : MDCEvent +} + +@JsModule("@material/select/constants") +internal external object MDCSelectConstants { + + @Suppress("ClassName") + object strings { + val CHANGE_EVENT: String + } +} + +@JsModule("@material/select/helper-text") +internal external object MDCSelectHelperTextModule { + class MDCSelectHelperText(element: Element) { + companion object { + fun attachTo(element: Element): MDCSelectHelperText + } + + fun destroy() + } +} + +public class MDCSelectAttrsScope private constructor() : AttrsBuilder() + +public data class MDCSelectOpts( + var type: Type = Type.Filled, + var label: String? = null, + var required: Boolean = false, + var disabled: Boolean = false, + var hiddenInputName: String? = null, + var helperText: MDCSelectHelperText? = null +) { + public enum class Type(public vararg val classes: String) { + Outlined("mdc-select--outlined"), + Filled("mdc-select--filled") + } +} + +public data class MDCSelectHelperText( + val text: String, + val type: Type +) { + public enum class Type(public vararg val classes: String) { + Default("mdc-select-helper-text"), + Validation(*(Default.classes + "mdc-select-helper-text--validation-msg")), + PersistentValidation(*(Validation.classes + "mdc-select-helper-text--validation-msg-persistent")) + } +} + +public class MDCSelectLeadingIconScope + +public data class MDCSelectItem(val value: String, val text: String, val disabled: Boolean = false) + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@MDCDsl +@Composable +public fun MDCSelect( + items: List, + selectedValue: String = "", + opts: Builder? = null, + attrs: Builder? = null, + leadingIcon: ComposableBuilder? = null, +) { + MDCSelectCSS + val labelId = remember { uniqueDomElementId() } + val selectedTextId = remember { uniqueDomElementId() } + val helperTextId = remember { uniqueDomElementId() } + val options = MDCSelectOpts().apply { opts?.invoke(this) } + Div( + attrs = { + classes("mdc-select", *options.type.classes) + if (options.label == null) classes("mdc-select--no-label") + if (options.required) classes("mdc-select--required") + if (options.disabled) classes("mdc-select--disabled") + if (options.required && selectedValue.isBlank()) classes("mdc-select--invalid") + if (leadingIcon != null) classes("mdc-select--with-leading-icon") + if (options.helperText != null) { + aria("controls", helperTextId) + aria("describedby", helperTextId) + } + ref { + it.mdc = MDCSelectModule.MDCSelect.attachTo(it) + onDispose { + it.mdc { destroy() } + } + } + attrs?.invoke(this.unsafeCast()) + } + ) { + + DomSideEffect(options.required) { + it.mdc { required = options.required } + } + DomSideEffect(options.disabled) { + it.mdc { disabled = options.disabled } + } + DomSideEffect(selectedValue) { + it.mdc { value = selectedValue } + } + + options.hiddenInputName?.let { + HiddenInput { name(it) } + } + + Div( + attrs = { + classes("mdc-select__anchor") + role("button") + aria("haspopup", "listbox") + aria("expanded", "false") + aria("labelledby", labelId) + } + ) { + + // floating label + when (options.type) { + MDCSelectOpts.Type.Filled -> { + Span(attrs = { classes("mdc-select__ripple") }) + options.label?.let { + mdcSelectLabel(labelId, it) + } + } + MDCSelectOpts.Type.Outlined -> { + Span( + attrs = { classes("mdc-notched-outline") } + ) { + Span(attrs = { classes("mdc-notched-outline__leading") }) + options.label?.let { + Span(attrs = { classes("mdc-notched-outline__notch") }) { + mdcSelectLabel(labelId, it) + } + } + Span(attrs = { classes("mdc-notched-outline__trailing") }) + } + } + } + + // leading icon + leadingIcon?.invoke(MDCSelectLeadingIconScope()) + + // selected text + Span( + attrs = { + classes("mdc-select__selected-text-container") + } + ) { + Span( + attrs = { + classes("mdc-select__selected-text") + id(selectedTextId) + } + ) { + items.firstOrNull { it.value == selectedValue }?.let { + Text(it.text) + } + } + } + + // down arrow icon + mdcSelectDropdownIcon() + + Span(attrs = { classes("mdc-line-ripple") }) + } + + MDCMenuSurface( + opts = { fullwidth = true }, + attrs = { + classes("mdc-select__menu", "mdc-menu") + } + ) { + MDCList( + opts = { singleSelection = true }, + attrs = { + options.label?.let { aria("label", it) } + } + ) { + items.forEach { item -> + MDCListItem( + opts = { + this.selected = item.value == selectedValue + this.disabled = item.disabled + }, + attrs = { + attr("data-value", item.value) + aria("selected", (item.value == selectedValue).toString()) + if (item.disabled) aria("disabled", "true") + role("option") + } + ) { + if (leadingIcon != null) { + MDCListItemGraphic() + } + MDCListItemText(item.text) + } + } + } + } + } + options.helperText?.let { helperText -> + P( + attrs = { + id(helperTextId) + classes(*helperText.type.classes) + ref { + it.mdc = MDCSelectHelperTextModule.MDCSelectHelperText.attachTo(it) + onDispose { + it.mdc { destroy() } + } + } + } + ) { + Text(helperText.text) + } + } +} + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +private fun mdcSelectDropdownIcon() { + Span( + attrs = { classes("mdc-select__dropdown-icon") } + ) { + Svg( + viewBox = "7 10 10 5", + attrs = { + attr("focusable", "false") + classes("mdc-select__dropdown-icon-graphic") + } + ) { + Polygon( + 7, 10, 12, 15, 17, 10, + attrs = { + classes("mdc-select__dropdown-icon-inactive") + attr("stroke", "none") + attr("fill-rule", "evenodd") + } + ) + Polygon( + 7, 15, 12, 10, 17, 15, + attrs = { + classes("mdc-select__dropdown-icon-active") + attr("stroke", "none") + attr("fill-rule", "evenodd") + } + ) + } + } +} + +@Composable +private fun mdcSelectLabel(id: String, label: String) { + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + id(id) + } + ) { + Text(label) + } +} + +@MDCAttrsDsl +public fun AttrsBuilder.mdcSelectIcon(clickable: Boolean) { + classes("mdc-select__icon") + if (clickable) { + tabIndex(0) + role("button") + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@Suppress("unused") +@MDCDsl +@Composable +public fun MDCSelectLeadingIconScope.MDCSelectLeadingIcon( + clickable: Boolean = true, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null, +) { + I( + attrs = { + mdcSelectIcon(clickable) + attrs?.invoke(this) + }, + content = content + ) +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt new file mode 100644 index 00000000..893a6e15 --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt @@ -0,0 +1,11 @@ +import dev.petuska.kmdc.core.MDCAttrsDsl + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-dialog) + */ +@MDCAttrsDsl +public fun MDCSelectAttrsScope.onChange(listener: (event: MDCSelectModule.MDCSelectChangeEvent) -> Unit) { + addEventListener(MDCSelectConstants.strings.CHANGE_EVENT) { + listener(it.nativeEvent.unsafeCast()) + } +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index a774e0af..24cc4202 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -330,6 +330,29 @@ "@material/typography" "^13.0.0" tslib "^2.1.0" +"@material/select@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@material/select/-/select-13.0.0.tgz#0bd1933c983ec86440bd5a5540e25aeba3918868" + integrity sha512-wVprsSMicU/l+LAqXdOU+qdzzdHupLXpWWQo2Rsk8G6AxL1Zna+/+ETnRlDdr2wHHK/KNDXSBdmuCcoEIRshcA== + dependencies: + "@material/animation" "^13.0.0" + "@material/base" "^13.0.0" + "@material/density" "^13.0.0" + "@material/dom" "^13.0.0" + "@material/feature-targeting" "^13.0.0" + "@material/floating-label" "^13.0.0" + "@material/line-ripple" "^13.0.0" + "@material/list" "^13.0.0" + "@material/menu" "^13.0.0" + "@material/menu-surface" "^13.0.0" + "@material/notched-outline" "^13.0.0" + "@material/ripple" "^13.0.0" + "@material/rtl" "^13.0.0" + "@material/shape" "^13.0.0" + "@material/theme" "^13.0.0" + "@material/typography" "^13.0.0" + tslib "^2.1.0" + "@material/shape@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@material/shape/-/shape-13.0.0.tgz#42751ceb4c709c87026f14340aed1831106fdfda" diff --git a/sandbox/kotlin-js-store/yarn.lock b/sandbox/kotlin-js-store/yarn.lock index 28c321aa..2893eb05 100644 --- a/sandbox/kotlin-js-store/yarn.lock +++ b/sandbox/kotlin-js-store/yarn.lock @@ -330,6 +330,29 @@ "@material/typography" "^13.0.0" tslib "^2.1.0" +"@material/select@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@material/select/-/select-13.0.0.tgz#0bd1933c983ec86440bd5a5540e25aeba3918868" + integrity sha512-wVprsSMicU/l+LAqXdOU+qdzzdHupLXpWWQo2Rsk8G6AxL1Zna+/+ETnRlDdr2wHHK/KNDXSBdmuCcoEIRshcA== + dependencies: + "@material/animation" "^13.0.0" + "@material/base" "^13.0.0" + "@material/density" "^13.0.0" + "@material/dom" "^13.0.0" + "@material/feature-targeting" "^13.0.0" + "@material/floating-label" "^13.0.0" + "@material/line-ripple" "^13.0.0" + "@material/list" "^13.0.0" + "@material/menu" "^13.0.0" + "@material/menu-surface" "^13.0.0" + "@material/notched-outline" "^13.0.0" + "@material/ripple" "^13.0.0" + "@material/rtl" "^13.0.0" + "@material/shape" "^13.0.0" + "@material/theme" "^13.0.0" + "@material/typography" "^13.0.0" + tslib "^2.1.0" + "@material/shape@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@material/shape/-/shape-13.0.0.tgz#42751ceb4c709c87026f14340aed1831106fdfda" diff --git a/sandbox/src/jsMain/kotlin/samples/Select.kt b/sandbox/src/jsMain/kotlin/samples/Select.kt new file mode 100644 index 00000000..b030c16f --- /dev/null +++ b/sandbox/src/jsMain/kotlin/samples/Select.kt @@ -0,0 +1,156 @@ +package local.sandbox.samples + +import MDCSelect +import MDCSelectHelperText +import MDCSelectItem +import MDCSelectLeadingIcon +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.petuska.kmdc.checkbox.MDCCheckbox +import dev.petuska.kmdc.form.field.MDCFormField +import dev.petuska.kmdcx.icons.MDCIconOpts +import local.sandbox.engine.Sample +import local.sandbox.engine.Samples +import onChange +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.Br +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text + +@Suppress("unused") +private val SelectSamples = Samples("MDCSelect") { + var value by remember { mutableStateOf("apple") } + var required by remember { mutableStateOf(false) } + var disabled by remember { mutableStateOf(false) } + + Sample("Filled") { + Div { + MDCSelect( + fruits, + value, + opts = { + type = MDCSelectOpts.Type.Filled + this.required = required + this.disabled = disabled + hiddenInputName = "filledSelect" + label = "Fruit" + }, + attrs = { + onChange { value = it.detail.value } + } + ) + } + Div { + MDCFormField { + MDCCheckbox(required, opts = { label = "Required" }, attrs = { onChange { required = it.value } }) + } + } + Div { + MDCFormField { + MDCCheckbox(disabled, opts = { label = "Disabled" }, attrs = { onChange { disabled = it.value } }) + } + } + } + + Sample("Outlined") { + MDCSelect( + fruits, + value, + opts = { + type = MDCSelectOpts.Type.Outlined + label = "Fruit" + }, + attrs = { + onChange { value = it.detail.value } + } + ) + } + + Sample("Helper text") { + var helperTextType by remember { mutableStateOf(MDCSelectHelperText.Type.Default) } + Div { + MDCSelect( + MDCSelectHelperText.Type.values().map { + MDCSelectItem(it.name, it.name) + }, + helperTextType.name, + opts = { + type = MDCSelectOpts.Type.Filled + }, + attrs = { + onChange { helperTextType = MDCSelectHelperText.Type.valueOf(it.detail.value) } + } + ) + } + Br() + Div { + MDCSelect( + fruits, + value, + opts = { + type = MDCSelectOpts.Type.Outlined + label = "Fruit" + this.required = true + helperText = MDCSelectHelperText("Please pick up your favorite fruit", helperTextType) + }, + attrs = { + onChange { value = it.detail.value } + } + ) + } + } + + Sample("With leading icon") { + MDCSelect( + fruits, + value, + opts = { + type = MDCSelectOpts.Type.Filled + }, + attrs = { + onChange { value = it.detail.value } + }, + leadingIcon = { + MDCSelectLeadingIcon( + clickable = true, + attrs = { classes("material-icons") } + ) { + Text(MDCIconOpts.MDCIconType.FoodBank.iconType) + } + } + ) + + Span(attrs = { style { width(10.px) } }) + + MDCSelect( + fruits, + value, + opts = { + type = MDCSelectOpts.Type.Outlined + }, + attrs = { + onChange { value = it.detail.value } + }, + leadingIcon = { + MDCSelectLeadingIcon( + clickable = false, + attrs = { classes("material-icons") } + ) { + Text(MDCIconOpts.MDCIconType.FoodBank.iconType) + } + } + ) + } +} + +private val fruits = listOf( + MDCSelectItem("", ""), + MDCSelectItem("apple", "Apple"), + MDCSelectItem("orange", "Orange"), + MDCSelectItem("banana", "Banana"), + MDCSelectItem("potato", "Potato", disabled = true) +) From f528419fc952aee39c2bf786ecfbd86f90eb8fca Mon Sep 17 00:00:00 2001 From: Andrey Kiryushin Date: Sun, 30 Jan 2022 17:14:30 +0300 Subject: [PATCH 2/5] Address review comments --- .../src/jsMain/kotlin/MDCSelect.kt | 303 +++++------------- .../src/jsMain/kotlin/MDCSelectAnchor.kt | 58 ++++ .../jsMain/kotlin/MDCSelectDropdownIcon.kt | 43 +++ .../src/jsMain/kotlin/MDCSelectHelperText.kt | 19 ++ .../src/jsMain/kotlin/MDCSelectLabel.kt | 51 +++ .../src/jsMain/kotlin/MDCSelectLeadingIcon.kt | 43 +++ .../src/jsMain/kotlin/MDCSelectModule.kt | 40 +++ kmdc/kmdc-select/src/jsMain/kotlin/events.kt | 11 - sandbox/src/jsMain/kotlin/samples/Select.kt | 106 +++--- 9 files changed, 389 insertions(+), 285 deletions(-) create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectDropdownIcon.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt delete mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/events.kt diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt index 3090f131..c2074631 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt @@ -1,11 +1,12 @@ +package dev.petuska.kmdc.select + import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import dev.petuska.kmdc.core.Builder import dev.petuska.kmdc.core.ComposableBuilder -import dev.petuska.kmdc.core.MDCAttrsDsl import dev.petuska.kmdc.core.MDCDsl -import dev.petuska.kmdc.core.MDCEvent import dev.petuska.kmdc.core.aria +import dev.petuska.kmdc.core.initialiseMDC import dev.petuska.kmdc.core.mdc import dev.petuska.kmdc.core.role import dev.petuska.kmdc.core.uniqueDomElementId @@ -14,86 +15,43 @@ import dev.petuska.kmdc.list.MDCListItem import dev.petuska.kmdc.list.MDCListItemGraphic import dev.petuska.kmdc.list.MDCListItemText import dev.petuska.kmdc.menu.surface.MDCMenuSurface -import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi import org.jetbrains.compose.web.attributes.AttrsBuilder import org.jetbrains.compose.web.attributes.name -import org.jetbrains.compose.web.dom.AttrBuilderContext -import org.jetbrains.compose.web.dom.ContentBuilder import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.ElementScope import org.jetbrains.compose.web.dom.HiddenInput -import org.jetbrains.compose.web.dom.I -import org.jetbrains.compose.web.dom.P -import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text -import org.jetbrains.compose.web.svg.Polygon -import org.jetbrains.compose.web.svg.Svg -import org.w3c.dom.Element import org.w3c.dom.HTMLDivElement -import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement @JsModule("@material/select/dist/mdc.select.css") private external val MDCSelectCSS: dynamic -@JsModule("@material/select") -public external object MDCSelectModule { - internal class MDCSelect(element: Element) { - companion object { - fun attachTo(element: Element): MDCSelect - } - - fun destroy() - - var value: String? - - fun setValue(value: String?, skipNotify: Boolean) - - var disabled: Boolean - - var required: Boolean - } - - public class MDCSelectChangeEventDetail { - public val value: String - public val index: Int - } - - public class MDCSelectChangeEvent : MDCEvent -} - -@JsModule("@material/select/constants") -internal external object MDCSelectConstants { - - @Suppress("ClassName") - object strings { - val CHANGE_EVENT: String - } -} - -@JsModule("@material/select/helper-text") -internal external object MDCSelectHelperTextModule { - class MDCSelectHelperText(element: Element) { - companion object { - fun attachTo(element: Element): MDCSelectHelperText - } - - fun destroy() - } -} - -public class MDCSelectAttrsScope private constructor() : AttrsBuilder() - -public data class MDCSelectOpts( +public data class MDCSelectOpts( var type: Type = Type.Filled, + var value: T? = null, + var onChange: ((value: T) -> Unit)? = null, var label: String? = null, var required: Boolean = false, var disabled: Boolean = false, var hiddenInputName: String? = null, var helperText: MDCSelectHelperText? = null ) { - public enum class Type(public vararg val classes: String) { + public enum class Type(public val klass: String) { Outlined("mdc-select--outlined"), Filled("mdc-select--filled") } + + public fun copyFrom(source: MDCSelectOpts, map: S.() -> T, unmap: T.() -> S) { + type = source.type + value = source.value?.map() + onChange = source.onChange?.let { listener -> { listener(it.unmap()) } } + label = source.label + required = source.required + disabled = source.disabled + hiddenInputName = source.hiddenInputName + helperText = source.helperText + } } public data class MDCSelectHelperText( @@ -109,44 +67,51 @@ public data class MDCSelectHelperText( public class MDCSelectLeadingIconScope -public data class MDCSelectItem(val value: String, val text: String, val disabled: Boolean = false) +public interface MDCSelectItem { + public val value: String + public val disabled: Boolean get() = false +} /** * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) */ @MDCDsl @Composable -public fun MDCSelect( - items: List, - selectedValue: String = "", - opts: Builder? = null, - attrs: Builder? = null, +public fun MDCSelect( + items: List, + opts: Builder>? = null, + attrs: Builder>? = null, leadingIcon: ComposableBuilder? = null, + renderItem: @Composable ElementScope.(T) -> Unit ) { MDCSelectCSS val labelId = remember { uniqueDomElementId() } val selectedTextId = remember { uniqueDomElementId() } val helperTextId = remember { uniqueDomElementId() } - val options = MDCSelectOpts().apply { opts?.invoke(this) } + val options = MDCSelectOpts().apply { opts?.invoke(this) } + Div( attrs = { - classes("mdc-select", *options.type.classes) - if (options.label == null) classes("mdc-select--no-label") - if (options.required) classes("mdc-select--required") - if (options.disabled) classes("mdc-select--disabled") - if (options.required && selectedValue.isBlank()) classes("mdc-select--invalid") - if (leadingIcon != null) classes("mdc-select--with-leading-icon") - if (options.helperText != null) { - aria("controls", helperTextId) - aria("describedby", helperTextId) + with(options) { + classes("mdc-select", type.klass) + if (label == null) classes("mdc-select--no-label") + if (required) classes("mdc-select--required") + if (disabled) classes("mdc-select--disabled") + if (required && value?.value.isNullOrBlank()) classes("mdc-select--invalid") + if (leadingIcon != null) classes("mdc-select--with-leading-icon") + if (helperText != null) { + aria("controls", helperTextId) + aria("describedby", helperTextId) + } } - ref { - it.mdc = MDCSelectModule.MDCSelect.attachTo(it) - onDispose { - it.mdc { destroy() } + initialiseMDC(MDCSelectModule.MDCSelect::attachTo) + options.onChange?.let { listener -> + addEventListener(MDCSelectModule.strings.CHANGE_EVENT) { + val event = it.nativeEvent.unsafeCast() + listener(items[event.detail.index]) } } - attrs?.invoke(this.unsafeCast()) + attrs?.invoke(this) } ) { @@ -156,73 +121,15 @@ public fun MDCSelect( DomSideEffect(options.disabled) { it.mdc { disabled = options.disabled } } - DomSideEffect(selectedValue) { - it.mdc { value = selectedValue } + DomSideEffect(options.value) { + it.mdc { value = options.value?.value } } options.hiddenInputName?.let { HiddenInput { name(it) } } - Div( - attrs = { - classes("mdc-select__anchor") - role("button") - aria("haspopup", "listbox") - aria("expanded", "false") - aria("labelledby", labelId) - } - ) { - - // floating label - when (options.type) { - MDCSelectOpts.Type.Filled -> { - Span(attrs = { classes("mdc-select__ripple") }) - options.label?.let { - mdcSelectLabel(labelId, it) - } - } - MDCSelectOpts.Type.Outlined -> { - Span( - attrs = { classes("mdc-notched-outline") } - ) { - Span(attrs = { classes("mdc-notched-outline__leading") }) - options.label?.let { - Span(attrs = { classes("mdc-notched-outline__notch") }) { - mdcSelectLabel(labelId, it) - } - } - Span(attrs = { classes("mdc-notched-outline__trailing") }) - } - } - } - - // leading icon - leadingIcon?.invoke(MDCSelectLeadingIconScope()) - - // selected text - Span( - attrs = { - classes("mdc-select__selected-text-container") - } - ) { - Span( - attrs = { - classes("mdc-select__selected-text") - id(selectedTextId) - } - ) { - items.firstOrNull { it.value == selectedValue }?.let { - Text(it.text) - } - } - } - - // down arrow icon - mdcSelectDropdownIcon() - - Span(attrs = { classes("mdc-line-ripple") }) - } + MDCSelectAnchor(labelId, options, leadingIcon, selectedTextId, items, renderItem) MDCMenuSurface( opts = { fullwidth = true }, @@ -239,12 +146,12 @@ public fun MDCSelect( items.forEach { item -> MDCListItem( opts = { - this.selected = item.value == selectedValue + this.selected = item == options.value this.disabled = item.disabled }, attrs = { attr("data-value", item.value) - aria("selected", (item.value == selectedValue).toString()) + aria("selected", (item == options.value).toString()) if (item.disabled) aria("disabled", "true") role("option") } @@ -252,100 +159,60 @@ public fun MDCSelect( if (leadingIcon != null) { MDCListItemGraphic() } - MDCListItemText(item.text) + MDCListItemText { + renderItem(item) + } } } } } } options.helperText?.let { helperText -> - P( - attrs = { - id(helperTextId) - classes(*helperText.type.classes) - ref { - it.mdc = MDCSelectHelperTextModule.MDCSelectHelperText.attachTo(it) - onDispose { - it.mdc { destroy() } - } - } - } - ) { - Text(helperText.text) - } + MDCSelectHelperText(helperTextId, helperText) } } -@OptIn(ExperimentalComposeWebSvgApi::class) +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@MDCDsl @Composable -private fun mdcSelectDropdownIcon() { - Span( - attrs = { classes("mdc-select__dropdown-icon") } - ) { - Svg( - viewBox = "7 10 10 5", - attrs = { - attr("focusable", "false") - classes("mdc-select__dropdown-icon-graphic") +public fun MDCSelect( + items: List, + opts: Builder>? = null, + attrs: Builder>? = null, + leadingIcon: ComposableBuilder? = null, + renderItem: @Composable ElementScope.(T) -> Unit +) { + val selectItems = items.map { Item(it) } + val selectOpts: Builder>>? = opts?.let { + MDCSelectOpts().apply(it).let { options -> + { + this.copyFrom(options, map = { Item(this) }, unmap = { original }) } - ) { - Polygon( - 7, 10, 12, 15, 17, 10, - attrs = { - classes("mdc-select__dropdown-icon-inactive") - attr("stroke", "none") - attr("fill-rule", "evenodd") - } - ) - Polygon( - 7, 15, 12, 10, 17, 15, - attrs = { - classes("mdc-select__dropdown-icon-active") - attr("stroke", "none") - attr("fill-rule", "evenodd") - } - ) } } -} - -@Composable -private fun mdcSelectLabel(id: String, label: String) { - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - id(id) - } - ) { - Text(label) + MDCSelect(selectItems, selectOpts, attrs, leadingIcon) { + renderItem(it.original) } } -@MDCAttrsDsl -public fun AttrsBuilder.mdcSelectIcon(clickable: Boolean) { - classes("mdc-select__icon") - if (clickable) { - tabIndex(0) - role("button") - } +private class Item(val original: T) : MDCSelectItem { + override val value = original.toString() } /** * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) */ -@Suppress("unused") @MDCDsl @Composable -public fun MDCSelectLeadingIconScope.MDCSelectLeadingIcon( - clickable: Boolean = true, - attrs: AttrBuilderContext? = null, - content: ContentBuilder? = null, +public fun MDCSelect( + items: List, + opts: Builder>? = null, + attrs: Builder>? = null, + leadingIcon: ComposableBuilder? = null ) { - I( - attrs = { - mdcSelectIcon(clickable) - attrs?.invoke(this) - }, - content = content - ) + MDCSelect(items, opts, attrs, leadingIcon) { + Text(it) + } } diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt new file mode 100644 index 00000000..c3ed9208 --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt @@ -0,0 +1,58 @@ +package dev.petuska.kmdc.select + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.ComposableBuilder +import dev.petuska.kmdc.core.aria +import dev.petuska.kmdc.core.role +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.ElementScope +import org.jetbrains.compose.web.dom.Span +import org.w3c.dom.HTMLSpanElement + +@Composable +internal fun MDCSelectAnchor( + labelId: String, + options: MDCSelectOpts, + leadingIcon: ComposableBuilder?, + selectedTextId: String, + items: List, + renderItem: @Composable ElementScope.(T) -> Unit +) { + Div( + attrs = { + classes("mdc-select__anchor") + role("button") + aria("haspopup", "listbox") + aria("expanded", "false") + aria("labelledby", labelId) + } + ) { + + MDCSelectLabel(options, labelId) + + leadingIcon?.invoke(MDCSelectLeadingIconScope()) + + Span( + attrs = { + classes("mdc-select__selected-text-container") + } + ) { + Span( + attrs = { + classes("mdc-select__selected-text") + id(selectedTextId) + } + ) { + options.value?.let { selectedItem -> + items.firstOrNull { it.value == selectedItem.value }?.let { + renderItem(it) + } + } + } + } + + MDCSelectDownArrowIcon() + + Span(attrs = { classes("mdc-line-ripple") }) + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectDropdownIcon.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectDropdownIcon.kt new file mode 100644 index 00000000..e4396d2a --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectDropdownIcon.kt @@ -0,0 +1,43 @@ +package dev.petuska.kmdc.select + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.svg.Polygon +import org.jetbrains.compose.web.svg.Svg + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +internal fun MDCSelectDownArrowIcon() { + Span( + attrs = { classes("mdc-select__dropdown-icon") } + ) { + Svg( + viewBox = "7 10 10 5", + attrs = { + attr("focusable", "false") + classes("mdc-select__dropdown-icon-graphic") + } + ) { + Polygon( + 7, 10, 12, 15, 17, 10, + attrs = { + classes("mdc-select__dropdown-icon-inactive") + attr("stroke", "none") + attr("fill-rule", "evenodd") + } + ) + Polygon( + 7, 15, 12, 10, 17, 15, + attrs = { + classes("mdc-select__dropdown-icon-active") + attr("stroke", "none") + attr("fill-rule", "evenodd") + } + ) + } + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt new file mode 100644 index 00000000..bde04f4a --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt @@ -0,0 +1,19 @@ +package dev.petuska.kmdc.select + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.initialiseMDC +import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.dom.Text + +@Composable +internal fun MDCSelectHelperText(helperTextId: String, helperText: MDCSelectHelperText) { + P( + attrs = { + id(helperTextId) + classes(*helperText.type.classes) + initialiseMDC(MDCSelectModule.MDCSelectHelperText.Companion::attachTo) + } + ) { + Text(helperText.text) + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt new file mode 100644 index 00000000..f0379d99 --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt @@ -0,0 +1,51 @@ +package dev.petuska.kmdc.select + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@Composable +internal fun MDCSelectLabel( + options: MDCSelectOpts, + labelId: String +) { + when (options.type) { + MDCSelectOpts.Type.Filled -> { + Span(attrs = { classes("mdc-select__ripple") }) + options.label?.let { + MDCSelectFloatingLabel(labelId, it) + } + } + MDCSelectOpts.Type.Outlined -> { + Span( + attrs = { classes("mdc-notched-outline") } + ) { + Span(attrs = { classes("mdc-notched-outline__leading") }) + options.label?.let { + Span(attrs = { classes("mdc-notched-outline__notch") }) { + MDCSelectFloatingLabel(labelId, it) + } + } + Span(attrs = { classes("mdc-notched-outline__trailing") }) + } + } + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@Composable +private fun MDCSelectFloatingLabel(id: String, label: String) { + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + id(id) + } + ) { + Text(label) + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt new file mode 100644 index 00000000..2d2f2256 --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt @@ -0,0 +1,43 @@ +package dev.petuska.kmdc.select + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.MDCAttrsDsl +import dev.petuska.kmdc.core.MDCDsl +import dev.petuska.kmdc.core.role +import org.jetbrains.compose.web.attributes.AttrsBuilder +import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.dom.ContentBuilder +import org.jetbrains.compose.web.dom.I +import org.w3c.dom.HTMLElement + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@MDCAttrsDsl +public fun AttrsBuilder.mdcSelectIcon(clickable: Boolean) { + classes("mdc-select__icon") + if (clickable) { + tabIndex(0) + role("button") + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@Suppress("unused") +@MDCDsl +@Composable +public fun MDCSelectLeadingIconScope.MDCSelectLeadingIcon( + clickable: Boolean = true, + attrs: AttrBuilderContext? = null, + content: ContentBuilder? = null, +) { + I( + attrs = { + mdcSelectIcon(clickable) + attrs?.invoke(this) + }, + content = content + ) +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt new file mode 100644 index 00000000..13e72b8f --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt @@ -0,0 +1,40 @@ +package dev.petuska.kmdc.select + +import dev.petuska.kmdc.core.MDCBaseModule +import dev.petuska.kmdc.core.MDCEvent +import org.w3c.dom.Element + +@JsModule("@material/select") +public external object MDCSelectModule { + public class MDCSelect(element: Element) : MDCBaseModule.MDCComponent { + public companion object { + public fun attachTo(element: Element): MDCSelect + } + + public var value: String? + + public fun setValue(value: String?, skipNotify: Boolean) + + public var disabled: Boolean + + public var required: Boolean + } + + public class MDCSelectHelperText(element: Element) : MDCBaseModule.MDCComponent { + public companion object { + public fun attachTo(element: Element): MDCSelectHelperText + } + } + + @Suppress("ClassName") + public object strings { + public val CHANGE_EVENT: String + } + + public class MDCSelectChangeEventDetail { + public val value: String + public val index: Int + } + + public class MDCSelectChangeEvent : MDCEvent +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt deleted file mode 100644 index 893a6e15..00000000 --- a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt +++ /dev/null @@ -1,11 +0,0 @@ -import dev.petuska.kmdc.core.MDCAttrsDsl - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-dialog) - */ -@MDCAttrsDsl -public fun MDCSelectAttrsScope.onChange(listener: (event: MDCSelectModule.MDCSelectChangeEvent) -> Unit) { - addEventListener(MDCSelectConstants.strings.CHANGE_EVENT) { - listener(it.nativeEvent.unsafeCast()) - } -} diff --git a/sandbox/src/jsMain/kotlin/samples/Select.kt b/sandbox/src/jsMain/kotlin/samples/Select.kt index b030c16f..ab7d101d 100644 --- a/sandbox/src/jsMain/kotlin/samples/Select.kt +++ b/sandbox/src/jsMain/kotlin/samples/Select.kt @@ -1,19 +1,19 @@ package local.sandbox.samples -import MDCSelect -import MDCSelectHelperText -import MDCSelectItem -import MDCSelectLeadingIcon import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.petuska.kmdc.checkbox.MDCCheckbox import dev.petuska.kmdc.form.field.MDCFormField +import dev.petuska.kmdc.select.MDCSelect +import dev.petuska.kmdc.select.MDCSelectHelperText +import dev.petuska.kmdc.select.MDCSelectOpts +import dev.petuska.kmdc.select.MDCSelectItem +import dev.petuska.kmdc.select.MDCSelectLeadingIcon import dev.petuska.kmdcx.icons.MDCIconOpts import local.sandbox.engine.Sample import local.sandbox.engine.Samples -import onChange import org.jetbrains.compose.web.css.px import org.jetbrains.compose.web.css.width import org.jetbrains.compose.web.dom.Br @@ -23,96 +23,92 @@ import org.jetbrains.compose.web.dom.Text @Suppress("unused") private val SelectSamples = Samples("MDCSelect") { - var value by remember { mutableStateOf("apple") } - var required by remember { mutableStateOf(false) } - var disabled by remember { mutableStateOf(false) } + var selectedValue by remember { mutableStateOf(fruitItems[1]) } + var isRequired by remember { mutableStateOf(false) } + var isDisabled by remember { mutableStateOf(false) } Sample("Filled") { Div { MDCSelect( - fruits, - value, + fruitItems, opts = { type = MDCSelectOpts.Type.Filled - this.required = required - this.disabled = disabled + value = selectedValue + onChange = { selectedValue = it } + required = isRequired + disabled = isDisabled hiddenInputName = "filledSelect" label = "Fruit" - }, - attrs = { - onChange { value = it.detail.value } } - ) + ) { + Text(it.value) + } } Div { MDCFormField { - MDCCheckbox(required, opts = { label = "Required" }, attrs = { onChange { required = it.value } }) + MDCCheckbox(isRequired, opts = { label = "Required" }, attrs = { onChange { isRequired = it.value } }) } } Div { MDCFormField { - MDCCheckbox(disabled, opts = { label = "Disabled" }, attrs = { onChange { disabled = it.value } }) + MDCCheckbox(isDisabled, opts = { label = "Disabled" }, attrs = { onChange { isDisabled = it.value } }) } } } Sample("Outlined") { MDCSelect( - fruits, - value, + fruitItems, opts = { type = MDCSelectOpts.Type.Outlined + value = selectedValue + onChange = { selectedValue = it } label = "Fruit" - }, - attrs = { - onChange { value = it.detail.value } } - ) + ) { + Text(it.value) + } } Sample("Helper text") { var helperTextType by remember { mutableStateOf(MDCSelectHelperText.Type.Default) } Div { MDCSelect( - MDCSelectHelperText.Type.values().map { - MDCSelectItem(it.name, it.name) - }, - helperTextType.name, + MDCSelectHelperText.Type.values().toList(), opts = { type = MDCSelectOpts.Type.Filled - }, - attrs = { - onChange { helperTextType = MDCSelectHelperText.Type.valueOf(it.detail.value) } + value = helperTextType + onChange = { helperTextType = it } } - ) + ) { + Text(it.name) + } } Br() Div { MDCSelect( - fruits, - value, + fruitItems, opts = { type = MDCSelectOpts.Type.Outlined + value = selectedValue + onChange = { selectedValue = it } label = "Fruit" - this.required = true + required = true helperText = MDCSelectHelperText("Please pick up your favorite fruit", helperTextType) - }, - attrs = { - onChange { value = it.detail.value } } - ) + ) { + Text(it.value) + } } } Sample("With leading icon") { MDCSelect( - fruits, - value, + fruitItems, opts = { type = MDCSelectOpts.Type.Filled - }, - attrs = { - onChange { value = it.detail.value } + value = selectedValue + onChange = { selectedValue = it } }, leadingIcon = { MDCSelectLeadingIcon( @@ -122,18 +118,18 @@ private val SelectSamples = Samples("MDCSelect") { Text(MDCIconOpts.MDCIconType.FoodBank.iconType) } } - ) + ) { + Text(it.value) + } Span(attrs = { style { width(10.px) } }) MDCSelect( fruits, - value, opts = { type = MDCSelectOpts.Type.Outlined - }, - attrs = { - onChange { value = it.detail.value } + value = selectedValue.value + onChange = { selectedValue = FruitItem(it, false) } }, leadingIcon = { MDCSelectLeadingIcon( @@ -147,10 +143,8 @@ private val SelectSamples = Samples("MDCSelect") { } } -private val fruits = listOf( - MDCSelectItem("", ""), - MDCSelectItem("apple", "Apple"), - MDCSelectItem("orange", "Orange"), - MDCSelectItem("banana", "Banana"), - MDCSelectItem("potato", "Potato", disabled = true) -) +private val fruits = listOf("", "Apple", "Orange", "Banana") + +private val fruitItems = (fruits + "Potato").map { FruitItem(it, it == "Potato") } + +private data class FruitItem(override val value: String, override val disabled: Boolean) : MDCSelectItem \ No newline at end of file From d95d6a9cc24a9fadd7f1855de7068d317d6648d1 Mon Sep 17 00:00:00 2001 From: Andrey Kiryushin Date: Sun, 30 Jan 2022 17:20:24 +0300 Subject: [PATCH 3/5] fixup! Address review comments --- kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt index 2d2f2256..1c9a9d0f 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt @@ -14,7 +14,7 @@ import org.w3c.dom.HTMLElement * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) */ @MDCAttrsDsl -public fun AttrsBuilder.mdcSelectIcon(clickable: Boolean) { +public fun AttrsBuilder.MDCSelectIcon(clickable: Boolean) { classes("mdc-select__icon") if (clickable) { tabIndex(0) @@ -35,7 +35,7 @@ public fun MDCSelectLeadingIconScope.MDCSelectLeadingIcon( ) { I( attrs = { - mdcSelectIcon(clickable) + MDCSelectIcon(clickable) attrs?.invoke(this) }, content = content From 54d46a6e9dacf9bf29a4afad8ace52c05f21b90d Mon Sep 17 00:00:00 2001 From: Andrey Kiryushin Date: Tue, 1 Feb 2022 22:22:03 +0300 Subject: [PATCH 4/5] Address review comments - part 2 --- .../src/jsMain/kotlin/MDCSelect.kt | 137 +++++------------- .../src/jsMain/kotlin/MDCSelectAnchor.kt | 34 ++++- .../src/jsMain/kotlin/MDCSelectHelperText.kt | 9 +- .../src/jsMain/kotlin/MDCSelectLabel.kt | 2 +- .../src/jsMain/kotlin/MDCSelectLeadingIcon.kt | 43 ------ .../src/jsMain/kotlin/MDCSelectModule.kt | 6 +- kmdc/kmdc-select/src/jsMain/kotlin/events.kt | 20 +++ sandbox/src/jsMain/kotlin/samples/Select.kt | 85 +++++------ 8 files changed, 131 insertions(+), 205 deletions(-) delete mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt create mode 100644 kmdc/kmdc-select/src/jsMain/kotlin/events.kt diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt index c2074631..ef95542d 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt @@ -1,15 +1,13 @@ package dev.petuska.kmdc.select import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import dev.petuska.kmdc.core.Builder -import dev.petuska.kmdc.core.ComposableBuilder import dev.petuska.kmdc.core.MDCDsl import dev.petuska.kmdc.core.aria import dev.petuska.kmdc.core.initialiseMDC import dev.petuska.kmdc.core.mdc +import dev.petuska.kmdc.core.rememberUniqueDomElementId import dev.petuska.kmdc.core.role -import dev.petuska.kmdc.core.uniqueDomElementId import dev.petuska.kmdc.list.MDCList import dev.petuska.kmdc.list.MDCListItem import dev.petuska.kmdc.list.MDCListItemGraphic @@ -30,66 +28,52 @@ private external val MDCSelectCSS: dynamic public data class MDCSelectOpts( var type: Type = Type.Filled, var value: T? = null, - var onChange: ((value: T) -> Unit)? = null, + var itemValue: T.() -> String = { toString() }, + var itemDisabled: T.() -> Boolean = { false }, var label: String? = null, var required: Boolean = false, var disabled: Boolean = false, var hiddenInputName: String? = null, - var helperText: MDCSelectHelperText? = null + var helperText: String? = null, + var helperTextType: HelperTextType = HelperTextType.Default, + var leadingIcon: String? = null, + var leadingIconClickable: Boolean = false, + var leadingIconClasses: List = listOf("material-icons") ) { public enum class Type(public val klass: String) { Outlined("mdc-select--outlined"), Filled("mdc-select--filled") } - public fun copyFrom(source: MDCSelectOpts, map: S.() -> T, unmap: T.() -> S) { - type = source.type - value = source.value?.map() - onChange = source.onChange?.let { listener -> { listener(it.unmap()) } } - label = source.label - required = source.required - disabled = source.disabled - hiddenInputName = source.hiddenInputName - helperText = source.helperText + public enum class HelperTextType(public vararg val classes: String) { + Default, + Validation("mdc-select-helper-text--validation-msg"), + PersistentValidation("mdc-select-helper-text--validation-msg", "mdc-select-helper-text--validation-msg-persistent") } } -public data class MDCSelectHelperText( - val text: String, - val type: Type -) { - public enum class Type(public vararg val classes: String) { - Default("mdc-select-helper-text"), - Validation(*(Default.classes + "mdc-select-helper-text--validation-msg")), - PersistentValidation(*(Validation.classes + "mdc-select-helper-text--validation-msg-persistent")) - } -} - -public class MDCSelectLeadingIconScope - -public interface MDCSelectItem { - public val value: String - public val disabled: Boolean get() = false -} +public class MDCSelectAttrsScope private constructor() : AttrsBuilder() /** * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) */ @MDCDsl @Composable -public fun MDCSelect( +public fun MDCSelect( items: List, opts: Builder>? = null, - attrs: Builder>? = null, - leadingIcon: ComposableBuilder? = null, - renderItem: @Composable ElementScope.(T) -> Unit + attrs: Builder>? = null, + renderItem: @Composable ElementScope.(T) -> Unit = { Text(it.toString()) } ) { MDCSelectCSS - val labelId = remember { uniqueDomElementId() } - val selectedTextId = remember { uniqueDomElementId() } - val helperTextId = remember { uniqueDomElementId() } + val labelId = rememberUniqueDomElementId() + val selectedTextId = rememberUniqueDomElementId() + val helperTextId = rememberUniqueDomElementId() val options = MDCSelectOpts().apply { opts?.invoke(this) } + fun T.itemValue() = with(options) { itemValue() } + fun T.itemDisabled() = with(options) { itemDisabled() } + Div( attrs = { with(options) { @@ -97,39 +81,35 @@ public fun MDCSelect( if (label == null) classes("mdc-select--no-label") if (required) classes("mdc-select--required") if (disabled) classes("mdc-select--disabled") - if (required && value?.value.isNullOrBlank()) classes("mdc-select--invalid") + if (required && value?.itemValue().isNullOrBlank()) classes("mdc-select--invalid") if (leadingIcon != null) classes("mdc-select--with-leading-icon") if (helperText != null) { aria("controls", helperTextId) aria("describedby", helperTextId) } } - initialiseMDC(MDCSelectModule.MDCSelect::attachTo) - options.onChange?.let { listener -> - addEventListener(MDCSelectModule.strings.CHANGE_EVENT) { - val event = it.nativeEvent.unsafeCast() - listener(items[event.detail.index]) - } - } - attrs?.invoke(this) + initialiseMDC(mdcInit = { + MDCSelectModule.MDCSelect.attachTo(it).also { it.items = items } + }) + attrs?.invoke(this.unsafeCast>()) } ) { DomSideEffect(options.required) { - it.mdc { required = options.required } + it.mdc> { required = options.required } } DomSideEffect(options.disabled) { - it.mdc { disabled = options.disabled } + it.mdc> { disabled = options.disabled } } DomSideEffect(options.value) { - it.mdc { value = options.value?.value } + it.mdc> { value = options.value?.itemValue() } } options.hiddenInputName?.let { HiddenInput { name(it) } } - MDCSelectAnchor(labelId, options, leadingIcon, selectedTextId, items, renderItem) + MDCSelectAnchor(labelId, options, selectedTextId, items, renderItem) MDCMenuSurface( opts = { fullwidth = true }, @@ -147,16 +127,16 @@ public fun MDCSelect( MDCListItem( opts = { this.selected = item == options.value - this.disabled = item.disabled + this.disabled = item.itemDisabled() }, attrs = { - attr("data-value", item.value) + attr("data-value", item.itemValue()) aria("selected", (item == options.value).toString()) - if (item.disabled) aria("disabled", "true") + if (item.itemDisabled()) aria("disabled", "true") role("option") } ) { - if (leadingIcon != null) { + if (options.leadingIcon != null) { MDCListItemGraphic() } MDCListItemText { @@ -168,51 +148,6 @@ public fun MDCSelect( } } options.helperText?.let { helperText -> - MDCSelectHelperText(helperTextId, helperText) - } -} - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) - */ -@MDCDsl -@Composable -public fun MDCSelect( - items: List, - opts: Builder>? = null, - attrs: Builder>? = null, - leadingIcon: ComposableBuilder? = null, - renderItem: @Composable ElementScope.(T) -> Unit -) { - val selectItems = items.map { Item(it) } - val selectOpts: Builder>>? = opts?.let { - MDCSelectOpts().apply(it).let { options -> - { - this.copyFrom(options, map = { Item(this) }, unmap = { original }) - } - } - } - MDCSelect(selectItems, selectOpts, attrs, leadingIcon) { - renderItem(it.original) - } -} - -private class Item(val original: T) : MDCSelectItem { - override val value = original.toString() -} - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) - */ -@MDCDsl -@Composable -public fun MDCSelect( - items: List, - opts: Builder>? = null, - attrs: Builder>? = null, - leadingIcon: ComposableBuilder? = null -) { - MDCSelect(items, opts, attrs, leadingIcon) { - Text(it) + MDCSelectHelperText(helperTextId, helperText, options.helperTextType) } } diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt index c3ed9208..77e1ed7d 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt @@ -1,23 +1,25 @@ package dev.petuska.kmdc.select import androidx.compose.runtime.Composable -import dev.petuska.kmdc.core.ComposableBuilder import dev.petuska.kmdc.core.aria import dev.petuska.kmdc.core.role import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.ElementScope +import org.jetbrains.compose.web.dom.I import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text import org.w3c.dom.HTMLSpanElement @Composable -internal fun MDCSelectAnchor( +internal fun MDCSelectAnchor( labelId: String, options: MDCSelectOpts, - leadingIcon: ComposableBuilder?, selectedTextId: String, items: List, renderItem: @Composable ElementScope.(T) -> Unit ) { + fun T.itemValue() = with(options) { itemValue() } + Div( attrs = { classes("mdc-select__anchor") @@ -30,7 +32,9 @@ internal fun MDCSelectAnchor( MDCSelectLabel(options, labelId) - leadingIcon?.invoke(MDCSelectLeadingIconScope()) + options.leadingIcon?.let { icon -> + MDCSelectLeadingIcon(options, icon) + } Span( attrs = { @@ -43,8 +47,8 @@ internal fun MDCSelectAnchor( id(selectedTextId) } ) { - options.value?.let { selectedItem -> - items.firstOrNull { it.value == selectedItem.value }?.let { + options.value?.itemValue()?.let { selectedValue -> + items.firstOrNull { it.itemValue() == selectedValue }?.let { renderItem(it) } } @@ -56,3 +60,21 @@ internal fun MDCSelectAnchor( Span(attrs = { classes("mdc-line-ripple") }) } } + +@Composable +private fun MDCSelectLeadingIcon(options: MDCSelectOpts, icon: String) { + I( + attrs = { + classes("mdc-select__icon") + if (options.leadingIconClickable) { + tabIndex(0) + role("button") + } + options.leadingIconClasses.takeUnless { it.isEmpty() }?.let { + classes(*it.toTypedArray()) + } + } + ) { + Text(icon) + } +} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt index bde04f4a..c7b27f10 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectHelperText.kt @@ -6,14 +6,15 @@ import org.jetbrains.compose.web.dom.P import org.jetbrains.compose.web.dom.Text @Composable -internal fun MDCSelectHelperText(helperTextId: String, helperText: MDCSelectHelperText) { +internal fun MDCSelectHelperText(id: String, text: String, type: MDCSelectOpts.HelperTextType) { P( attrs = { - id(helperTextId) - classes(*helperText.type.classes) + id(id) + classes("mdc-select-helper-text") + classes(*type.classes) initialiseMDC(MDCSelectModule.MDCSelectHelperText.Companion::attachTo) } ) { - Text(helperText.text) + Text(text) } } diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt index f0379d99..d99ea210 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLabel.kt @@ -8,7 +8,7 @@ import org.jetbrains.compose.web.dom.Text * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) */ @Composable -internal fun MDCSelectLabel( +internal fun MDCSelectLabel( options: MDCSelectOpts, labelId: String ) { diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt deleted file mode 100644 index 1c9a9d0f..00000000 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectLeadingIcon.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.petuska.kmdc.select - -import androidx.compose.runtime.Composable -import dev.petuska.kmdc.core.MDCAttrsDsl -import dev.petuska.kmdc.core.MDCDsl -import dev.petuska.kmdc.core.role -import org.jetbrains.compose.web.attributes.AttrsBuilder -import org.jetbrains.compose.web.dom.AttrBuilderContext -import org.jetbrains.compose.web.dom.ContentBuilder -import org.jetbrains.compose.web.dom.I -import org.w3c.dom.HTMLElement - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) - */ -@MDCAttrsDsl -public fun AttrsBuilder.MDCSelectIcon(clickable: Boolean) { - classes("mdc-select__icon") - if (clickable) { - tabIndex(0) - role("button") - } -} - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) - */ -@Suppress("unused") -@MDCDsl -@Composable -public fun MDCSelectLeadingIconScope.MDCSelectLeadingIcon( - clickable: Boolean = true, - attrs: AttrBuilderContext? = null, - content: ContentBuilder? = null, -) { - I( - attrs = { - MDCSelectIcon(clickable) - attrs?.invoke(this) - }, - content = content - ) -} diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt index 13e72b8f..f012d682 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectModule.kt @@ -6,9 +6,9 @@ import org.w3c.dom.Element @JsModule("@material/select") public external object MDCSelectModule { - public class MDCSelect(element: Element) : MDCBaseModule.MDCComponent { + public class MDCSelect(element: Element) : MDCBaseModule.MDCComponent { public companion object { - public fun attachTo(element: Element): MDCSelect + public fun attachTo(element: Element): MDCSelect } public var value: String? @@ -18,6 +18,8 @@ public external object MDCSelectModule { public var disabled: Boolean public var required: Boolean + + public var items: List } public class MDCSelectHelperText(element: Element) : MDCBaseModule.MDCComponent { diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt new file mode 100644 index 00000000..879a1d7b --- /dev/null +++ b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt @@ -0,0 +1,20 @@ +package dev.petuska.kmdc.select + +import dev.petuska.kmdc.core.MDCAttrsDsl +import dev.petuska.kmdc.core.mdc +import org.w3c.dom.Element + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-select) + */ +@MDCAttrsDsl +public fun MDCSelectAttrsScope.onChange(listener: (value: T) -> Unit) { + addEventListener(MDCSelectModule.strings.CHANGE_EVENT) { + val event = it.nativeEvent.unsafeCast() + (it.nativeEvent.currentTarget as? Element) + ?.mdc> { + listener(items[event.detail.index]) + } + ?: console.warn("MDCSelect component - current target not found for change event") + } +} diff --git a/sandbox/src/jsMain/kotlin/samples/Select.kt b/sandbox/src/jsMain/kotlin/samples/Select.kt index ab7d101d..8be3156e 100644 --- a/sandbox/src/jsMain/kotlin/samples/Select.kt +++ b/sandbox/src/jsMain/kotlin/samples/Select.kt @@ -7,10 +7,8 @@ import androidx.compose.runtime.setValue import dev.petuska.kmdc.checkbox.MDCCheckbox import dev.petuska.kmdc.form.field.MDCFormField import dev.petuska.kmdc.select.MDCSelect -import dev.petuska.kmdc.select.MDCSelectHelperText import dev.petuska.kmdc.select.MDCSelectOpts -import dev.petuska.kmdc.select.MDCSelectItem -import dev.petuska.kmdc.select.MDCSelectLeadingIcon +import dev.petuska.kmdc.select.onChange import dev.petuska.kmdcx.icons.MDCIconOpts import local.sandbox.engine.Sample import local.sandbox.engine.Samples @@ -23,26 +21,27 @@ import org.jetbrains.compose.web.dom.Text @Suppress("unused") private val SelectSamples = Samples("MDCSelect") { - var selectedValue by remember { mutableStateOf(fruitItems[1]) } + var selectedValue by remember { mutableStateOf(fruits[1]) } var isRequired by remember { mutableStateOf(false) } var isDisabled by remember { mutableStateOf(false) } Sample("Filled") { Div { MDCSelect( - fruitItems, + fruits + "Potato", opts = { type = MDCSelectOpts.Type.Filled value = selectedValue - onChange = { selectedValue = it } required = isRequired disabled = isDisabled + itemDisabled = { this == "Potato" } hiddenInputName = "filledSelect" label = "Fruit" + }, + attrs = { + onChange { selectedValue = it } } - ) { - Text(it.value) - } + ) } Div { MDCFormField { @@ -58,27 +57,30 @@ private val SelectSamples = Samples("MDCSelect") { Sample("Outlined") { MDCSelect( - fruitItems, + fruits + "Potato", opts = { type = MDCSelectOpts.Type.Outlined value = selectedValue - onChange = { selectedValue = it } label = "Fruit" + itemDisabled = { this == "Potato" } + }, + attrs = { + onChange { selectedValue = it } } - ) { - Text(it.value) - } + ) } Sample("Helper text") { - var helperTextType by remember { mutableStateOf(MDCSelectHelperText.Type.Default) } + var helperTextType by remember { mutableStateOf(MDCSelectOpts.HelperTextType.Default) } Div { MDCSelect( - MDCSelectHelperText.Type.values().toList(), + MDCSelectOpts.HelperTextType.values().toList(), opts = { type = MDCSelectOpts.Type.Filled value = helperTextType - onChange = { helperTextType = it } + }, + attrs = { + onChange { helperTextType = it } } ) { Text(it.name) @@ -87,40 +89,35 @@ private val SelectSamples = Samples("MDCSelect") { Br() Div { MDCSelect( - fruitItems, + fruits, opts = { type = MDCSelectOpts.Type.Outlined value = selectedValue - onChange = { selectedValue = it } label = "Fruit" required = true - helperText = MDCSelectHelperText("Please pick up your favorite fruit", helperTextType) + helperText = "Please pick up your favorite fruit" + this.helperTextType = helperTextType + }, + attrs = { + onChange { selectedValue = it } } - ) { - Text(it.value) - } + ) } } Sample("With leading icon") { MDCSelect( - fruitItems, + fruits, opts = { type = MDCSelectOpts.Type.Filled value = selectedValue - onChange = { selectedValue = it } + leadingIcon = MDCIconOpts.MDCIconType.FoodBank.iconType + leadingIconClickable = true }, - leadingIcon = { - MDCSelectLeadingIcon( - clickable = true, - attrs = { classes("material-icons") } - ) { - Text(MDCIconOpts.MDCIconType.FoodBank.iconType) - } + attrs = { + onChange { selectedValue = it } } - ) { - Text(it.value) - } + ) Span(attrs = { style { width(10.px) } }) @@ -128,23 +125,15 @@ private val SelectSamples = Samples("MDCSelect") { fruits, opts = { type = MDCSelectOpts.Type.Outlined - value = selectedValue.value - onChange = { selectedValue = FruitItem(it, false) } + value = selectedValue + leadingIcon = MDCIconOpts.MDCIconType.FoodBank.iconType + leadingIconClickable = false }, - leadingIcon = { - MDCSelectLeadingIcon( - clickable = false, - attrs = { classes("material-icons") } - ) { - Text(MDCIconOpts.MDCIconType.FoodBank.iconType) - } + attrs = { + onChange { selectedValue = it } } ) } } private val fruits = listOf("", "Apple", "Orange", "Banana") - -private val fruitItems = (fruits + "Potato").map { FruitItem(it, it == "Potato") } - -private data class FruitItem(override val value: String, override val disabled: Boolean) : MDCSelectItem \ No newline at end of file From 049c188e350b768fa912f7ab57a73f8afa0ba611 Mon Sep 17 00:00:00 2001 From: Andrey Kiryushin Date: Wed, 2 Feb 2022 17:53:02 +0300 Subject: [PATCH 5/5] Some minor changes --- kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt | 12 +++++++----- .../kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt | 4 +--- kmdc/kmdc-select/src/jsMain/kotlin/events.kt | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt index ef95542d..f0245629 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelect.kt @@ -70,6 +70,7 @@ public fun MDCSelect( val selectedTextId = rememberUniqueDomElementId() val helperTextId = rememberUniqueDomElementId() val options = MDCSelectOpts().apply { opts?.invoke(this) } + val hasLeadingIcon = options.leadingIcon != null fun T.itemValue() = with(options) { itemValue() } fun T.itemDisabled() = with(options) { itemDisabled() } @@ -82,15 +83,16 @@ public fun MDCSelect( if (required) classes("mdc-select--required") if (disabled) classes("mdc-select--disabled") if (required && value?.itemValue().isNullOrBlank()) classes("mdc-select--invalid") - if (leadingIcon != null) classes("mdc-select--with-leading-icon") + if (hasLeadingIcon) classes("mdc-select--with-leading-icon") if (helperText != null) { aria("controls", helperTextId) aria("describedby", helperTextId) } } - initialiseMDC(mdcInit = { - MDCSelectModule.MDCSelect.attachTo(it).also { it.items = items } - }) + initialiseMDC( + mdcInit = { MDCSelectModule.MDCSelect.attachTo(it) }, + postInit = { _, mdc -> mdc.items = items } + ) attrs?.invoke(this.unsafeCast>()) } ) { @@ -136,7 +138,7 @@ public fun MDCSelect( role("option") } ) { - if (options.leadingIcon != null) { + if (hasLeadingIcon) { MDCListItemGraphic() } MDCListItemText { diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt index 77e1ed7d..4102320e 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/MDCSelectAnchor.kt @@ -70,9 +70,7 @@ private fun MDCSelectLeadingIcon(options: MDCSelectOpts, icon: String) { tabIndex(0) role("button") } - options.leadingIconClasses.takeUnless { it.isEmpty() }?.let { - classes(*it.toTypedArray()) - } + classes(*options.leadingIconClasses.toTypedArray()) } ) { Text(icon) diff --git a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt index 879a1d7b..e32ffd3b 100644 --- a/kmdc/kmdc-select/src/jsMain/kotlin/events.kt +++ b/kmdc/kmdc-select/src/jsMain/kotlin/events.kt @@ -11,7 +11,7 @@ import org.w3c.dom.Element public fun MDCSelectAttrsScope.onChange(listener: (value: T) -> Unit) { addEventListener(MDCSelectModule.strings.CHANGE_EVENT) { val event = it.nativeEvent.unsafeCast() - (it.nativeEvent.currentTarget as? Element) + (event.currentTarget as? Element) ?.mdc> { listener(items[event.detail.index]) }