diff --git a/CHANGELOG.md b/CHANGELOG.md index 34203b8d..64d91199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * N/A ### New modules wrapped - [x] mdc-switch +- [x] mdc-slider # 0.0.2 ## Versions diff --git a/README.md b/README.md index f09777c3..18ed8cca 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ fun Sample() { ### Progress -Here's a tracker list of currently completed [material-components-web] modules (25/49): +Here's a tracker list of currently completed [material-components-web] modules (26/49): - [ ] mdc-animation (SASS) - [x] mdc-auto-init (won't wrap) @@ -110,7 +110,7 @@ Here's a tracker list of currently completed [material-components-web] modules ( - [x] mdc-segmented-button - [ ] mdc-select - [ ] mdc-shape (SASS) -- [ ] mdc-slider +- [x] mdc-slider - [x] mdc-snackbar - [x] mdc-switch - [ ] mdc-tab-bar diff --git a/kmdc/kmdc-chips/src/jsMain/kotlin/MDCChipSet.kt b/kmdc/kmdc-chips/src/jsMain/kotlin/MDCChipSet.kt index 45aa21b3..4140bac6 100644 --- a/kmdc/kmdc-chips/src/jsMain/kotlin/MDCChipSet.kt +++ b/kmdc/kmdc-chips/src/jsMain/kotlin/MDCChipSet.kt @@ -9,10 +9,10 @@ import org.w3c.dom.Element private external val MDCChipsCSS: dynamic @JsModule("@material/chips") -private external object MDCChipsModule { - class MDCChipSet(element: Element) { - companion object { - fun attachTo(element: Element) +public external object MDCChipsModule { + public class MDCChipSet(element: Element) { + public companion object { + public fun attachTo(element: Element): MDCChipSet } } } diff --git a/kmdc/kmdc-core/src/jsMain/kotlin/MDCDsl.kt b/kmdc/kmdc-core/src/jsMain/kotlin/MDCDsl.kt index 66b6a01a..da46817f 100644 --- a/kmdc/kmdc-core/src/jsMain/kotlin/MDCDsl.kt +++ b/kmdc/kmdc-core/src/jsMain/kotlin/MDCDsl.kt @@ -1,9 +1,11 @@ package dev.petuska.kmdc.core @DslMarker +@MDCInternalDsl public annotation class MDCDsl @DslMarker +@MDCInternalDsl public annotation class MDCAttrsDsl @DslMarker diff --git a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButton.kt b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButton.kt index 1445a7fa..76b858ad 100644 --- a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButton.kt +++ b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButton.kt @@ -3,100 +3,27 @@ package dev.petuska.kmdc.segmented.button import androidx.compose.runtime.Composable 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.mdc -import dev.petuska.kmdc.ripple.MDCRippleModule import org.jetbrains.compose.web.attributes.AttrsBuilder -import org.jetbrains.compose.web.dom.AttrBuilderContext import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.ElementScope -import org.w3c.dom.Element import org.w3c.dom.HTMLButtonElement import org.w3c.dom.HTMLDivElement -import org.w3c.dom.events.Event @JsModule("@material/segmented-button/dist/mdc.segmented-button.css") private external val MDCSegmentedButtonStyle: dynamic -@JsModule("@material/segmented-button") -public external object MDCSegmentedButtonModule { - public class MDCSegmentedButton(element: Element) { - public companion object { - public fun attachTo(element: Element): MDCSegmentedButton - } - - public val segments: Array - public fun initialize( - segmentFactory: ( - el: Element, - foundation: dynamic - ) -> MDCSegmentedButtonSegment = definedExternally - ) - - public fun initialSyncWithDOM() - public fun destroy() - public fun getDefaultFoundation(): dynamic - public fun getSelectedSegments(): Array - public fun selectSegment(indexOrSegmentId: dynamic) - public fun unselectSegment(indexOrSegmentId: dynamic) - public fun isSegmentSelected(indexOrSegmentId: dynamic): Boolean - } - - public class MDCSegmentedButtonSegment(element: Element) { - public companion object { - public fun attachTo(element: Element): MDCSegmentedButtonSegment - } - - public var ripple: MDCRippleModule.MDCRipple - public fun initialize(rippleFactory: (el: Element, foundation: dynamic) -> MDCRippleModule.MDCRipple) - public fun initialSyncWithDOM() - public fun destroy() - public fun getDefaultFoundation(): dynamic - public fun setIndex(index: Number) - public fun setIsSingleSelect(isSingleSelect: Boolean) - public fun isSelected(): Boolean - public fun setSelected() - public fun setUnselected() - public fun getSegmentId(): dynamic - } - - public interface SegmentDetail { - public val index: Number - public val selected: Boolean - public val segmentId: String? - } - - public class MDCSegmentedButtonChangeEvent : Event { - public val detail: SegmentDetail - } - - public class MDCSegmentedButtonSegmentSelectedEvent : Event { - public val detail: SegmentDetail - } -} - public data class MDCSegmentedButtonOpts( var singleSelect: Boolean = false, ) -public class MDCSegmentedButtonScope(scope: ElementScope, internal val opts: MDCSegmentedButtonOpts) : - ElementScope by scope { - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) - */ - @MDCAttrsDsl - public fun AttrsBuilder.onSegmentSelected( - listener: (MDCSegmentedButtonModule.MDCSegmentedButtonSegmentSelectedEvent) -> Unit - ) { -// TODO Uncomment after https://github.com/material-components/material-components-web/issues/7127 is fixed -// addEventListener("MDCSegmentedButtonSegment:selected") { - addEventListener("selected") { - listener(it.nativeEvent.unsafeCast()) - } - } -} +public class MDCSegmentedButtonAttrsScope private constructor() : AttrsBuilder() +public class MDCSegmentedButtonScope( + scope: ElementScope, + internal val options: MDCSegmentedButtonOpts +) : + ElementScope by scope /** * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) @@ -105,7 +32,7 @@ public class MDCSegmentedButtonScope(scope: ElementScope, intern @Composable public fun MDCSegmentedButton( opts: Builder? = null, - attrs: AttrBuilderContext? = null, + attrs: Builder? = null, content: ComposableBuilder? = null ) { MDCSegmentedButtonStyle @@ -126,22 +53,8 @@ public fun MDCSegmentedButton( it.mdc { destroy() } } } - attrs?.invoke(this) + attrs?.invoke(this.unsafeCast()) }, content = content?.let { { MDCSegmentedButtonScope(this, options).it() } } ) } - -/** - * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) - */ -@MDCAttrsDsl -public fun AttrsBuilder.onSegmentChange( - listener: (MDCSegmentedButtonModule.MDCSegmentedButtonChangeEvent) -> Unit -) { -// TODO Uncomment after https://github.com/material-components/material-components-web/issues/7127 is fixed -// addEventListener("MDCSegmentedButton:change") { - addEventListener("change") { - listener(it.nativeEvent.unsafeCast()) - } -} diff --git a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonModule.kt b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonModule.kt new file mode 100644 index 00000000..c7928e8f --- /dev/null +++ b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonModule.kt @@ -0,0 +1,59 @@ +package dev.petuska.kmdc.segmented.button + +import dev.petuska.kmdc.core.MDCEvent +import dev.petuska.kmdc.ripple.MDCRippleModule +import org.w3c.dom.Element + +@JsModule("@material/segmented-button") +public external object MDCSegmentedButtonModule { + public class MDCSegmentedButton(element: Element) { + public companion object { + public fun attachTo(element: Element): MDCSegmentedButton + } + + public val segments: Array + public fun initialize( + segmentFactory: ( + el: Element, + foundation: dynamic + ) -> MDCSegmentedButtonSegment = definedExternally + ) + + public fun initialSyncWithDOM() + public fun destroy() + public fun getDefaultFoundation(): dynamic + public fun getSelectedSegments(): Array + public fun selectSegment(indexOrSegmentId: dynamic) + public fun unselectSegment(indexOrSegmentId: dynamic) + public fun isSegmentSelected(indexOrSegmentId: dynamic): Boolean + } + + public class MDCSegmentedButtonSegment(element: Element) { + public companion object { + public fun attachTo(element: Element): MDCSegmentedButtonSegment + } + + public var ripple: MDCRippleModule.MDCRipple + public fun initialize(rippleFactory: (el: Element, foundation: dynamic) -> MDCRippleModule.MDCRipple) + public fun initialSyncWithDOM() + public fun destroy() + public fun getDefaultFoundation(): dynamic + public fun setIndex(index: Number) + public fun setIsSingleSelect(isSingleSelect: Boolean) + public fun isSelected(): Boolean + public fun setSelected() + public fun setUnselected() + public fun getSegmentId(): dynamic + } + + public interface SegmentDetail { + public val index: Number + public val selected: Boolean + public val segmentId: String? + } + + public class MDCSegmentedButtonEvent : MDCEvent + + public class MDCSegmentedButtonSegmentEvent : MDCEvent + public class MDCSegmentedButtonSegmentClickEvent : MDCEvent +} diff --git a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonSegment.kt b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonSegment.kt index 898a4f99..fe3d73cd 100644 --- a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonSegment.kt +++ b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/MDCSegmentedButtonSegment.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable import dev.petuska.kmdc.core.Builder import dev.petuska.kmdc.core.ComposableBuilder import dev.petuska.kmdc.core.MDCDsl -import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.attributes.AttrsBuilder import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.ElementScope @@ -15,6 +15,7 @@ public data class MDCSegmentedButtonSegmentOpts( var touch: Boolean = false ) +public class MDCSegmentedButtonSegmentAttrsScope private constructor() : AttrsBuilder() public class MDCSegmentedButtonSegmentScope(scope: ElementScope) : ElementScope by scope @@ -25,7 +26,7 @@ public class MDCSegmentedButtonSegmentScope(scope: ElementScope? = null, - attrs: AttrBuilderContext? = null, + attrs: Builder? = null, content: ComposableBuilder? = null, ) { val options = MDCSegmentedButtonSegmentOpts().apply { opts?.invoke(this) } @@ -38,13 +39,13 @@ public fun MDCSegmentedButtonScope.MDCSegmentedButtonSegment( if (options.selected) { classes("mdc-segmented-button__segment--selected") } - if (this@MDCSegmentedButtonSegment.opts.singleSelect) { + if (this@MDCSegmentedButtonSegment.options.singleSelect) { attr("aria-checked", "${options.selected}") attr("role", "radio") } else { attr("aria-pressed", "${options.selected}") } - attrs?.invoke(this) + attrs?.invoke(this.unsafeCast()) } ) { if (options.touch) { @@ -65,7 +66,7 @@ public fun MDCSegmentedButtonScope.MDCSegmentedButtonSegment( public fun MDCSegmentedButtonScope.MDCSegmentedButtonSegment( text: String, opts: Builder? = null, - attrs: AttrBuilderContext? = null, + attrs: Builder? = null, ) { MDCSegmentedButtonSegment(opts, attrs) { MDCSegmentedButtonLabel(text) diff --git a/kmdc/kmdc-segmented-button/src/jsMain/kotlin/events.kt b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/events.kt new file mode 100644 index 00000000..8be5a61d --- /dev/null +++ b/kmdc/kmdc-segmented-button/src/jsMain/kotlin/events.kt @@ -0,0 +1,69 @@ +package dev.petuska.kmdc.segmented.button + +import dev.petuska.kmdc.core.MDCAttrsDsl + +@JsModule("@material/segmented-button/segmented-button/constants") +private external object SegmentedButtonModuleConstants { + @Suppress("ClassName") + object events { + val CHANGE: String + val SELECTED: String + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) + */ +@MDCAttrsDsl +public fun MDCSegmentedButtonAttrsScope.onSegmentSelected( + listener: (event: MDCSegmentedButtonModule.MDCSegmentedButtonEvent) -> Unit +) { + addEventListener(SegmentedButtonModuleConstants.events.SELECTED) { + listener(it.nativeEvent.unsafeCast()) + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) + */ +@MDCAttrsDsl +public fun MDCSegmentedButtonAttrsScope.onSegmentChange( + listener: (event: MDCSegmentedButtonModule.MDCSegmentedButtonEvent) -> Unit +) { + addEventListener(SegmentedButtonModuleConstants.events.CHANGE) { + listener(it.nativeEvent.unsafeCast()) + } +} + +@JsModule("@material/segmented-button/segment/constants") +private external object SegmentModuleConstants { + @Suppress("ClassName") + object events { + val CLICK: String + val SELECTED: String + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) + */ +@MDCAttrsDsl +public fun MDCSegmentedButtonSegmentAttrsScope.onSegmentSelected( + listener: (event: MDCSegmentedButtonModule.MDCSegmentedButtonSegmentEvent) -> Unit +) { + addEventListener(SegmentModuleConstants.events.SELECTED) { + listener(it.nativeEvent.unsafeCast()) + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-segmented-button) + */ +@MDCAttrsDsl +public fun MDCSegmentedButtonSegmentAttrsScope.onSegmentClick( + listener: (event: MDCSegmentedButtonModule.MDCSegmentedButtonSegmentClickEvent) -> Unit +) { + addEventListener(SegmentModuleConstants.events.CLICK) { + listener(it.nativeEvent.unsafeCast()) + } +} diff --git a/kmdc/kmdc-slider/build.gradle.kts b/kmdc/kmdc-slider/build.gradle.kts new file mode 100644 index 00000000..80a92712 --- /dev/null +++ b/kmdc/kmdc-slider/build.gradle.kts @@ -0,0 +1,19 @@ +import util.mdcVersion + +plugins { + id("plugin.library-compose") + id("plugin.publishing-mpp") +} + +description = "Compose Multiplatform Kotlin/JS wrappers for @material/slider" + +kotlin { + sourceSets { + jsMain { + dependencies { + api(project(":kmdc:kmdc-core")) + api(npm("@material/slider", mdcVersion)) + } + } + } +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSlider.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSlider.kt new file mode 100644 index 00000000..9ff8cd03 --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSlider.kt @@ -0,0 +1,73 @@ +package dev.petuska.kmdc.slider + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.Builder +import dev.petuska.kmdc.core.MDCDsl +import dev.petuska.kmdc.core.initialiseMDC +import org.jetbrains.compose.web.attributes.AttrsBuilder +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.ElementScope +import org.w3c.dom.HTMLDivElement + +@JsModule("@material/slider/dist/mdc.slider.css") +private external val MDCSliderCSS: dynamic + +public class MDCSliderAttrsScope private constructor() : AttrsBuilder() + +public class MDCSliderScope(scope: ElementScope, public val options: MDCSliderOpts) : + ElementScope by scope + +public data class MDCSliderOpts( + var disabled: Boolean = false, + var discrete: Boolean = false, + var tickMarks: Boolean = false, + var value: Number = 0, + var label: String? = null, + var value2: Number? = null, + var label2: String? = null, + var min: Number = 0, + var max: Number = 100, + var step: Number = 1, +) { + val range: Boolean get() = value2 != null +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCDsl +@Composable +public fun MDCSlider( + opts: Builder? = null, + attrs: Builder? = null, +) { + MDCSliderCSS + val options = MDCSliderOpts().apply { opts?.invoke(this) } + Div( + attrs = { + classes("mdc-slider") + if (options.range) classes("mdc-slider--range") + if (options.discrete) classes("mdc-slider--discrete") + if (options.tickMarks) classes("mdc-slider--tick-marks") + if (options.disabled) classes("mdc-slider--disabled") + initialiseMDC(MDCSliderModule.MDCSlider::attachTo) + attrs?.invoke(this.unsafeCast()) + } + ) { + with(options) { + if (range) { + MDCSliderInput(value = value, min = min, max = value2!!, label = label, rangeStart = true) + MDCSliderInput(value = value2!!, min = value, max = max, label = label2, rangeStart = false) + } else { + MDCSliderInput(value = value, min = min, max = max, label = label, rangeStart = null) + } + MDCSliderTrack() + if (range) { + MDCSliderThumb(value) + MDCSliderThumb(value2) + } else { + MDCSliderThumb(value) + } + } + } +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderInput.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderInput.kt new file mode 100644 index 00000000..c9f94cfe --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderInput.kt @@ -0,0 +1,47 @@ +package dev.petuska.kmdc.slider + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.Builder +import dev.petuska.kmdc.core.MDCDsl +import dev.petuska.kmdc.core.aria +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.builders.InputAttrsBuilder +import org.jetbrains.compose.web.attributes.disabled +import org.jetbrains.compose.web.attributes.max +import org.jetbrains.compose.web.attributes.min +import org.jetbrains.compose.web.attributes.name +import org.jetbrains.compose.web.attributes.step +import org.jetbrains.compose.web.dom.Input + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCDsl +@Composable +internal fun MDCSliderOpts.MDCSliderInput( + value: Number, + min: Number, + max: Number, + label: String?, + rangeStart: Boolean?, + attrs: Builder>? = null, +) { + Input( + type = InputType.Range, + attrs = { + classes("mdc-slider__input") + when (rangeStart) { + null -> name("volume") + true -> name("rangeStart") + false -> name("rangeEnd") + } + if (label != null) aria("label", label) + min("$min") + max("$max") + step(step) + attr("value", "$value") + if (disabled) disabled() + attrs?.invoke(this) + } + ) +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderModule.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderModule.kt new file mode 100644 index 00000000..1b5d521a --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderModule.kt @@ -0,0 +1,44 @@ +package dev.petuska.kmdc.slider + +import dev.petuska.kmdc.core.MDCEvent +import org.w3c.dom.Element +import org.w3c.dom.HTMLElement + +@JsModule("@material/slider") +public external object MDCSliderModule { + public class MDCSlider(element: Element) { + public interface MDCSliderOptions { + public var skipInitialUIUpdate: Boolean? + } + + public companion object { + public fun attachTo(element: Element, options: MDCSliderOptions = definedExternally): MDCSlider + } + + public var root: HTMLElement + public fun getDefaultFoundation(): dynamic + public fun initialize(options: MDCSliderOptions = definedExternally): dynamic + public fun initialSyncWithDOM() + public fun layout() + public fun getValueStart(): Number + public fun setValueStart(valueStart: Number) + public fun getValue(): Number + public fun setValue(value: Number) + public fun getDisabled(): Boolean + public fun setDisabled(disabled: Boolean) + public fun setValueToAriaValueTextFn(mapFn: ((value: Number) -> String)?) + } + + @Suppress("ClassName") + public object events { + public val CHANGE: String + public val INPUT: String + } + + public class MDCSliderChangeEventDetail { + public val value: Number + public val thumb: Int + } + + public class MDCSliderChangeEvent : MDCEvent +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderThumb.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderThumb.kt new file mode 100644 index 00000000..46ea69ed --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderThumb.kt @@ -0,0 +1,38 @@ +package dev.petuska.kmdc.slider + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.MDCDsl +import dev.petuska.kmdc.core.aria +import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Text +import org.w3c.dom.HTMLDivElement + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCDsl +@Composable +internal fun MDCSliderOpts.MDCSliderThumb( + value: Number? = null, + attrs: AttrBuilderContext? = null, +) { + Div(attrs = { + classes("mdc-slider__thumb") + attrs?.invoke(this) + }) { + if (discrete && value != null) { + Div(attrs = { + classes("mdc-slider__value-indicator-container") + aria("hidden", "true") + }) { + Div(attrs = { classes("mdc-slider__value-indicator") }) { + Div(attrs = { classes("mdc-slider__value-indicator-text") }) { + Text("$value") + } + } + } + } + Div(attrs = { classes("mdc-slider__thumb-knob") }) + } +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderTrack.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderTrack.kt new file mode 100644 index 00000000..33770fa2 --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/MDCSliderTrack.kt @@ -0,0 +1,49 @@ +package dev.petuska.kmdc.slider + +import androidx.compose.runtime.Composable +import dev.petuska.kmdc.core.MDCDsl +import org.jetbrains.compose.web.dom.AttrBuilderContext +import org.jetbrains.compose.web.dom.Div +import org.w3c.dom.HTMLDivElement + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCDsl +@Composable +internal fun MDCSliderOpts.MDCSliderTrack( + attrs: AttrBuilderContext? = null, +) { + Div(attrs = { + classes("mdc-slider__track") + attrs?.invoke(this) + }) { + Div(attrs = { + classes("mdc-slider__track--inactive") + }) + Div(attrs = { + classes("mdc-slider--active") + }) { + Div(attrs = { + classes("mdc-slider__track--active_fill") + }) + } + if (tickMarks) MDCSliderTickMarks() + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCDsl +@Composable +private fun MDCSliderTickMarks( + attrs: AttrBuilderContext? = null, +) { + Div( + attrs = { + classes("mdc-slider__tick-marks") + attrs?.invoke(this) + }, + ) +} diff --git a/kmdc/kmdc-slider/src/jsMain/kotlin/events.kt b/kmdc/kmdc-slider/src/jsMain/kotlin/events.kt new file mode 100644 index 00000000..65e9ab5f --- /dev/null +++ b/kmdc/kmdc-slider/src/jsMain/kotlin/events.kt @@ -0,0 +1,23 @@ +package dev.petuska.kmdc.slider + +import dev.petuska.kmdc.core.MDCAttrsDsl + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCAttrsDsl +public fun MDCSliderAttrsScope.onSliderChange(listener: (event: MDCSliderModule.MDCSliderChangeEvent) -> Unit) { + addEventListener(MDCSliderModule.events.CHANGE) { + listener(it.nativeEvent.unsafeCast()) + } +} + +/** + * [JS API](https://github.com/material-components/material-components-web/tree/v13.0.0/packages/mdc-slider) + */ +@MDCAttrsDsl +public fun MDCSliderAttrsScope.onSliderInput(listener: (event: MDCSliderModule.MDCSliderChangeEvent) -> Unit) { + addEventListener(MDCSliderModule.events.INPUT) { + listener(it.nativeEvent.unsafeCast()) + } +} diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 5aed2f0c..ff56ed66 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -310,6 +310,22 @@ "@material/theme" "^13.0.0" tslib "^2.1.0" +"@material/slider@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@material/slider/-/slider-13.0.0.tgz#21575e99bc9700b56d44bb5ee1b63b4a0545e412" + integrity sha512-PW+3X9MiOoWmXhirYo/Mk2UYW00Tnsihrx5YJQ4+IxwbrUI75/8yUsO8kVr7YC+Eqhldz8oXzhIXglQFtbpolQ== + dependencies: + "@material/animation" "^13.0.0" + "@material/base" "^13.0.0" + "@material/dom" "^13.0.0" + "@material/elevation" "^13.0.0" + "@material/feature-targeting" "^13.0.0" + "@material/ripple" "^13.0.0" + "@material/rtl" "^13.0.0" + "@material/theme" "^13.0.0" + "@material/typography" "^13.0.0" + tslib "^2.1.0" + "@material/snackbar@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@material/snackbar/-/snackbar-13.0.0.tgz#63798b832c6931ecb99350c9ef99ac23a7e47f18" diff --git a/sandbox/kotlin-js-store/yarn.lock b/sandbox/kotlin-js-store/yarn.lock index f9885387..b9d891b2 100644 --- a/sandbox/kotlin-js-store/yarn.lock +++ b/sandbox/kotlin-js-store/yarn.lock @@ -310,6 +310,22 @@ "@material/theme" "^13.0.0" tslib "^2.1.0" +"@material/slider@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@material/slider/-/slider-13.0.0.tgz#21575e99bc9700b56d44bb5ee1b63b4a0545e412" + integrity sha512-PW+3X9MiOoWmXhirYo/Mk2UYW00Tnsihrx5YJQ4+IxwbrUI75/8yUsO8kVr7YC+Eqhldz8oXzhIXglQFtbpolQ== + dependencies: + "@material/animation" "^13.0.0" + "@material/base" "^13.0.0" + "@material/dom" "^13.0.0" + "@material/elevation" "^13.0.0" + "@material/feature-targeting" "^13.0.0" + "@material/ripple" "^13.0.0" + "@material/rtl" "^13.0.0" + "@material/theme" "^13.0.0" + "@material/typography" "^13.0.0" + tslib "^2.1.0" + "@material/snackbar@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@material/snackbar/-/snackbar-13.0.0.tgz#63798b832c6931ecb99350c9ef99ac23a7e47f18" diff --git a/sandbox/src/jsMain/kotlin/engine/Samples.kt b/sandbox/src/jsMain/kotlin/engine/Samples.kt index 129ecbab..608667f4 100644 --- a/sandbox/src/jsMain/kotlin/engine/Samples.kt +++ b/sandbox/src/jsMain/kotlin/engine/Samples.kt @@ -28,7 +28,7 @@ data class Samples( @SampleDsl @Composable operator fun MDCLayoutGridCellsScope.invoke() { - NamedCell(name, description, titleRender = { t, a -> MDCH5(t, a) }) { + NamedCell(name, description, span = 12u, titleRender = { t, a -> MDCH5(t, a) }) { content() } } @@ -42,6 +42,7 @@ annotation class SampleDsl class Sample @SampleDsl constructor( private val description: String? = null, private val span: UInt = 6u, + private val name: String? = null, private val content: @Composable MDCLayoutGridScope.(name: String) -> Unit ) : ReadOnlyProperty Unit> { @@ -49,7 +50,8 @@ class Sample @SampleDsl constructor( override fun getValue(thisRef: Nothing?, property: KProperty<*>): @Composable MDCLayoutGridCellsScope.() -> Unit { return sample ?: run { - val s: @Composable MDCLayoutGridCellsScope.() -> Unit = { Sample(property.name, description, span, content) } + val s: @Composable MDCLayoutGridCellsScope.() -> Unit = + { Sample(name ?: property.name, description, span, content) } s.also { sample = it } } } diff --git a/sandbox/src/jsMain/kotlin/engine/Sandbox.kt b/sandbox/src/jsMain/kotlin/engine/Sandbox.kt index 66d5877c..95021361 100644 --- a/sandbox/src/jsMain/kotlin/engine/Sandbox.kt +++ b/sandbox/src/jsMain/kotlin/engine/Sandbox.kt @@ -12,7 +12,11 @@ import dev.petuska.kmdc.layout.grid.MDCLayoutGrid import dev.petuska.kmdc.layout.grid.MDCLayoutGridCell import dev.petuska.kmdc.layout.grid.MDCLayoutGridCells import dev.petuska.kmdc.layout.grid.MDCLayoutGridCellsScope +import dev.petuska.kmdc.slider.MDCSlider +import dev.petuska.kmdc.slider.onSliderInput +import dev.petuska.kmdc.typography.MDCBody1 import dev.petuska.kmdc.typography.MDCH1 +import dev.petuska.kmdc.typography.mdcTypography import org.jetbrains.compose.web.css.AlignItems import org.jetbrains.compose.web.css.Color import org.jetbrains.compose.web.css.DisplayStyle @@ -45,7 +49,7 @@ fun Sandbox() { .toSet() .let { enabledSamples = it } noMatch { - MDCLayoutGrid { + MDCLayoutGrid(attrs = { mdcTypography() }) { MDCLayoutGridCells { SamplesList(parameters?.map, enabledSamples) SamplesView(enabledSamples) diff --git a/sandbox/src/jsMain/kotlin/samples/SegmentedButton.kt b/sandbox/src/jsMain/kotlin/samples/SegmentedButton.kt index 6db8f991..353caa77 100644 --- a/sandbox/src/jsMain/kotlin/samples/SegmentedButton.kt +++ b/sandbox/src/jsMain/kotlin/samples/SegmentedButton.kt @@ -1,10 +1,14 @@ package local.sandbox.samples import dev.petuska.kmdc.segmented.button.MDCSegmentedButton +import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonAttrsScope import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonIcon import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonLabel import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonSegment +import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonSegmentAttrsScope import dev.petuska.kmdc.segmented.button.onSegmentChange +import dev.petuska.kmdc.segmented.button.onSegmentClick +import dev.petuska.kmdc.segmented.button.onSegmentSelected import local.sandbox.engine.Sample import local.sandbox.engine.Samples import org.jetbrains.compose.web.dom.Text @@ -18,14 +22,24 @@ private val SegmentedButtonSamples = Samples( SingleSelect() } -private val MultiSelect by Sample { +private fun MDCSegmentedButtonAttrsScope.registerEvents(name: String) { + onSegmentChange { console.log("$name#onSegmentChange", it.detail) } + onSegmentSelected { console.log("$name#onSegmentSelected", it.detail) } +} + +private fun MDCSegmentedButtonSegmentAttrsScope.registerEvents(name: String) { + onSegmentSelected { console.log("$name#onSegmentSelected", it.detail) } + onSegmentClick { console.log("$name#onSegmentClick", it.detail) } +} + +private val MultiSelect by Sample { name -> MDCSegmentedButton(attrs = { - onSegmentChange { console.log("onSegmentChange", it.detail) } + registerEvents(name) }) { MDCSegmentedButtonSegment( attrs = { id("mdc-segmented-button-ms-segment-0") - onSegmentSelected { console.log("onSegmentSelected", it.detail) } + registerEvents(name) } ) { MDCSegmentedButtonIcon(attrs = { @@ -36,13 +50,13 @@ private val MultiSelect by Sample { text = "One", attrs = { id("mdc-segmented-button-ms-segment-1") - onSegmentSelected { console.log("onSegmentSelected", it.detail) } + registerEvents(name) } ) MDCSegmentedButtonSegment( attrs = { id("mdc-segmented-button-ms-segment-2") - onSegmentSelected { console.log("onSegmentSelected", it.detail) } + registerEvents(name) } ) { MDCSegmentedButtonIcon(attrs = { @@ -53,16 +67,18 @@ private val MultiSelect by Sample { } } -private val SingleSelect by Sample { +private val SingleSelect by Sample { name -> MDCSegmentedButton( opts = { singleSelect = true }, attrs = { - onSegmentChange { console.log("onSegmentChange", it.detail) } + registerEvents(name) } ) { MDCSegmentedButtonSegment( opts = { selected = true }, - attrs = { onSegmentSelected { console.log("onSegmentSelected", it.detail) } } + attrs = { + registerEvents(name) + } ) { MDCSegmentedButtonIcon(attrs = { classes("material-icons") @@ -70,10 +86,14 @@ private val SingleSelect by Sample { } MDCSegmentedButtonSegment( text = "A", - attrs = { onSegmentSelected { console.log("onSegmentSelected", it.detail) } } + attrs = { + registerEvents(name) + } ) MDCSegmentedButtonSegment( - attrs = { onSegmentSelected { console.log("onSegmentSelected", it.detail) } } + attrs = { + registerEvents(name) + } ) { MDCSegmentedButtonIcon(attrs = { classes("material-icons") diff --git a/sandbox/src/jsMain/kotlin/samples/Slider.kt b/sandbox/src/jsMain/kotlin/samples/Slider.kt new file mode 100644 index 00000000..4fb9096d --- /dev/null +++ b/sandbox/src/jsMain/kotlin/samples/Slider.kt @@ -0,0 +1,150 @@ +package local.sandbox.samples + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import dev.petuska.kmdc.slider.MDCSlider +import dev.petuska.kmdc.slider.MDCSliderAttrsScope +import dev.petuska.kmdc.slider.onSliderChange +import dev.petuska.kmdc.slider.onSliderInput +import dev.petuska.kmdc.typography.MDCBody1 +import local.sandbox.engine.Sample +import local.sandbox.engine.Samples + +@Suppress("unused") +private val SliderSamples = Samples("MDCSlider") { + Sample("Continuous") { name -> + var v1 by remember { mutableStateOf(50) } + MDCBody1("Value: $v1") + MDCSlider( + opts = { + value = v1 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + v1 = detail.value.toInt() + } + } + } + ) + } + Sample("Continuous Range") { name -> + var v1 by remember { mutableStateOf(25) } + var v2 by remember { mutableStateOf(75) } + MDCBody1("Values: $v1:$v2") + MDCSlider( + opts = { + value = v1 + value2 = v2 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + if (detail.thumb == 1) { + v1 = detail.value.toInt() + } else { + v2 = detail.value.toInt() + } + } + } + } + ) + } + Sample("Discrete") { name -> + var v1 by remember { mutableStateOf(50) } + MDCBody1("Value: $v1") + MDCSlider( + opts = { + discrete = true + value = v1 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + v1 = detail.value.toInt() + } + } + } + ) + } + Sample("Discrete Range") { name -> + var v1 by remember { mutableStateOf(25) } + var v2 by remember { mutableStateOf(75) } + MDCBody1("Values: $v1:$v2") + MDCSlider( + opts = { + discrete = true + value = v1 + value2 = v2 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + if (detail.thumb == 1) { + v1 = detail.value.toInt() + } else { + v2 = detail.value.toInt() + } + } + } + } + ) + } + Sample("Discrete with Ticks") { name -> + var v1 by remember { mutableStateOf(50) } + MDCBody1("Value: $v1") + MDCSlider( + opts = { + discrete = true + tickMarks = true + value = v1 + step = 10 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + v1 = detail.value.toInt() + } + } + } + ) + } + Sample("Discrete Range with Ticks") { name -> + var v1 by remember { mutableStateOf(20) } + var v2 by remember { mutableStateOf(80) } + MDCBody1("Values: $v1:$v2") + MDCSlider( + opts = { + discrete = true + tickMarks = true + value = v1 + value2 = v2 + step = 10 + }, + attrs = { + registerEvents(name) + onSliderInput { + it.detail.let { detail -> + if (detail.thumb == 1) { + v1 = detail.value.toInt() + } else { + v2 = detail.value.toInt() + } + } + } + } + ) + } +} + +private fun MDCSliderAttrsScope.registerEvents(name: String) { + onSliderInput { console.log("$name#onSliderInput", it.detail) } + onSliderChange { console.log("$name#onSliderChange", it.detail) } +} diff --git a/sandbox/src/jsMain/kotlin/samples/Switch.kt b/sandbox/src/jsMain/kotlin/samples/Switch.kt index ace09023..8d9f9aa9 100644 --- a/sandbox/src/jsMain/kotlin/samples/Switch.kt +++ b/sandbox/src/jsMain/kotlin/samples/Switch.kt @@ -11,6 +11,7 @@ import dev.petuska.kmdc.layout.grid.MDCLayoutGridCells import dev.petuska.kmdc.layout.grid.MDCLayoutGridCellsScope import dev.petuska.kmdc.segmented.button.MDCSegmentedButton import dev.petuska.kmdc.segmented.button.MDCSegmentedButtonSegment +import dev.petuska.kmdc.segmented.button.onSegmentSelected import dev.petuska.kmdc.switch.MDCSwitch import local.sandbox.engine.Sample import local.sandbox.engine.Samples