Skip to content

Commit

Permalink
avoid events in flatMapLatest (#830)
Browse files Browse the repository at this point in the history
* avoid events in flatMapLatest

* fix switch label

* use preventDefault and stopPropagation convenience functions

---------

Co-authored-by: Metin Kale <metin.kale@oeffentliche.de>
  • Loading branch information
metin-kale and metin-kale authored Dec 22, 2023
1 parent 8cab86d commit d7c3d02
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dev.fritz2.headless.components
import dev.fritz2.core.*
import dev.fritz2.headless.foundation.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.w3c.dom.*

Expand Down Expand Up @@ -160,18 +160,17 @@ class CheckboxGroup<C : HTMLElement, T>(tag: Tag<C>, private val explicitId: Str
withKeyboardNavigation = false
toggleEvent = changes
}
value.handler?.invoke(this, value.data.flatMapLatest { value ->
toggleEvent.map { if (value.contains(option)) value - option else value + option }
})
value.handler?.invoke(this, toggleEvent
.map { value.data.first() }
.map { value -> if (value.contains(option)) value - option else value + option })
if (withKeyboardNavigation) {
value.handler?.invoke(
this,
value.data.flatMapLatest { value ->
keydowns.filter { shortcutOf(it) == Keys.Space }.map {
it.stopImmediatePropagation()
it.preventDefault()
keydowns.filter { shortcutOf(it) == Keys.Space }
.stopImmediatePropagation().preventDefault()
.map {
val value = value.data.first()
if (value.contains(option)) value - option else value + option
}
})
}
}.also { toggle = it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.fritz2.headless.foundation.utils.scrollintoview.*
import kotlinx.browser.document
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable.isActive
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import org.w3c.dom.HTMLButtonElement
Expand Down Expand Up @@ -422,11 +423,8 @@ class DataCollection<T, C : HTMLElement>(tag: Tag<C>) : Tag<C> by tag {
data.value?.let { selection.selectItem(clicks.map { item }, it) }
}

active.flatMapLatest { isActive ->
mousemoves.mapNotNull {
if (!isActive) (item to false)
else null
}
mousemoves.mapNotNull {
if (!active.first()) (item to false) else null
} handledBy activeItem.update

if (scrollIntoView.isSet) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ class Listbox<T, C : HTMLElement>(tag: Tag<C>, id: String?) : Tag<C> by tag, Ope
}
}

private val state by lazy { activeIndex.data.combine(entries.data, ::Pair) }

fun render() {
attr("id", componentId)
opened.drop(1).filter { !it } handledBy {
Expand Down Expand Up @@ -207,9 +205,11 @@ class Listbox<T, C : HTMLElement>(tag: Tag<C>, id: String?) : Tag<C> by tag, Ope
label?.let { attr(Aria.labelledby, it.id) }
attr(Aria.activedescendant, activeIndex.data.map { if (it == -1) null else "$componentId-item-$it" })

state.flatMapLatest { (currentIndex, entries) ->
keydowns.mapNotNull { event ->
when (shortcutOf(event)) {

keydowns.mapNotNull { event ->
val currentIndex = activeIndex.current
val entries = entries.current
when (shortcutOf(event)) {
Keys.ArrowUp -> nextItem(currentIndex, Direction.Previous, entries)
Keys.ArrowDown -> nextItem(currentIndex, Direction.Next, entries)
Keys.Home -> firstItem(entries)
Expand All @@ -220,37 +220,33 @@ class Listbox<T, C : HTMLElement>(tag: Tag<C>, id: String?) : Tag<C> by tag, Ope
event.preventDefault()
event.stopImmediatePropagation()
}
}
}
} handledBy activeIndex.update

entries.data.flatMapLatest { entries ->
keydowns.mapNotNull { event ->
if (!Keys.NamedKeys.contains(event.key)) {
event.preventDefault()
event.stopImmediatePropagation()
event.key.first().lowercaseChar()
} else null
}
.mapNotNull { c ->
if (c.isLetterOrDigit()) itemByCharacter(entries, c)
else null
}
keydowns.mapNotNull { event ->
if (!Keys.NamedKeys.contains(event.key)) {
event.preventDefault()
event.stopImmediatePropagation()
event.key.first().lowercaseChar()
} else null
}.mapNotNull { c ->
if (c.isLetterOrDigit()) itemByCharacter(entries.current, c)
else null
} handledBy activeIndex.update

value.handler?.invoke(
this,
state.flatMapLatest { (currentIndex, entries) ->
keydowns.filter {
setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it))
}.mapNotNull {
if (currentIndex == -1 || entries[currentIndex].disabled) {
null
} else {
it.preventDefault()
it.stopImmediatePropagation()
entries[currentIndex].value
}
keydowns.filter {
setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it))
}.mapNotNull {
val currentIndex = activeIndex.current
val entries = entries.current
if (currentIndex == -1 || entries[currentIndex].disabled) {
null
} else {
it.preventDefault()
it.stopImmediatePropagation()
entries[currentIndex].value
}
}
)
Expand Down
60 changes: 29 additions & 31 deletions headless/src/jsMain/kotlin/dev/fritz2/headless/components/menu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -132,41 +132,39 @@ class Menu<C : HTMLElement>(tag: Tag<C>, id: String?) : Tag<C> by tag, OpenClose
attrIfNotSet("tabindex", "0")
attr("role", Aria.Role.menu)

state.flatMapLatest { (currentIndex, items) ->
keydowns.mapNotNull { event ->
when (shortcutOf(event)) {
Keys.ArrowUp -> nextItem(currentIndex, Direction.Previous, items)
Keys.ArrowDown -> nextItem(currentIndex, Direction.Next, items)
Keys.Home -> firstItem(items)
Keys.End -> lastItem(items)
else -> null
}.also {
if (it != null) {
event.stopImmediatePropagation()
event.preventDefault()
}
keydowns.mapNotNull { event ->
val currentIndex = activeIndex.current
val items = items.current
when (shortcutOf(event)) {
Keys.ArrowUp -> nextItem(currentIndex, Direction.Previous, items)
Keys.ArrowDown -> nextItem(currentIndex, Direction.Next, items)
Keys.Home -> firstItem(items)
Keys.End -> lastItem(items)
else -> null
}.also {
if (it != null) {
event.stopImmediatePropagation()
event.preventDefault()
}
}
} handledBy activeIndex.update

items.data.flatMapLatest { items ->
keydowns
.mapNotNull { e -> if (e.key.length == 1) e.key.first().lowercaseChar() else null }
.mapNotNull { c ->
if (c.isLetterOrDigit()) itemByCharacter(items, c)
else null
}
} handledBy activeIndex.update

state.flatMapLatest { (currentIndex, disabled) ->
keydowns.filter { setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it)) }.mapNotNull {
if (currentIndex == -1 || disabled[currentIndex].disabled) {
null
} else {
it.preventDefault()
it.stopImmediatePropagation()
currentIndex
}
keydowns
.mapNotNull { e -> if (e.key.length == 1) e.key.first().lowercaseChar() else null }
.mapNotNull { c ->
if (c.isLetterOrDigit()) itemByCharacter(items.current, c)
else null
} handledBy activeIndex.update

keydowns.filter { setOf(Keys.Enter, Keys.Space).contains(shortcutOf(it)) }.mapNotNull {
val currentIndex = activeIndex.current
val disabled = items.current
if (currentIndex == -1 || disabled[currentIndex].disabled) {
null
} else {
it.preventDefault()
it.stopImmediatePropagation()
currentIndex
}
} handledBy selections.update

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package dev.fritz2.headless.components
import dev.fritz2.core.*
import dev.fritz2.headless.foundation.*
import kotlinx.browser.document
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.*
import org.w3c.dom.*

/**
Expand Down Expand Up @@ -41,18 +38,17 @@ class RadioGroup<C : HTMLElement, T>(tag: Tag<C>, private val explicitId: String
if (withKeyboardNavigation) {
value.handler?.invoke(
this,
value.data.flatMapLatest { option ->
keydowns.mapNotNull { event ->
when (shortcutOf(event)) {
Keys.ArrowDown -> options.rotateNext(option)
Keys.ArrowUp -> options.rotatePrevious(option)
else -> null
}.also {
if (it != null) {
event.stopImmediatePropagation()
event.preventDefault()
isActive.update(it)
}
keydowns.mapNotNull { event ->
val option = value.data.first()
when (shortcutOf(event)) {
Keys.ArrowDown -> options.rotateNext(option)
Keys.ArrowUp -> options.rotatePrevious(option)
else -> null
}.also {
if (it != null) {
event.stopImmediatePropagation()
event.preventDefault()
isActive.update(it)
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ package dev.fritz2.headless.components

import dev.fritz2.core.*
import dev.fritz2.headless.foundation.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import org.w3c.dom.*

/**
Expand Down Expand Up @@ -38,16 +35,14 @@ abstract class AbstractSwitch<C : HTMLElement>(
attr(Aria.checked, enabled.asString())
attr(Aria.invalid, "true".whenever(value.hasError))
attr("tabindex", "0")
value.handler?.invoke(this, value.data.flatMapLatest { state -> clicks.map { !state } })
value.handler?.invoke(this, clicks.map { !value.data.first() })
value.handler?.invoke(
this,
value.data.flatMapLatest { state ->
keydowns.filter { shortcutOf(it) == Keys.Space }.map {
it.stopImmediatePropagation()
it.preventDefault()
!state
}
})
keydowns.filter { shortcutOf(it) == Keys.Space }
.stopImmediatePropagation()
.preventDefault()
.map { !value.data.first() }
)
}

/**
Expand Down Expand Up @@ -175,7 +170,7 @@ class SwitchWithLabel<C : HTMLElement>(tag: Tag<C>, id: String?) :
): Tag<CL> {
addComponentStructureInfo("switchLabel", this@switchLabel.scope, this)
return tag(this, classes, "$componentId-label", scope, content).apply {
value.handler?.invoke(this, value.data.flatMapLatest { state -> clicks.map { !state } })
value.handler?.invoke(this, clicks.preventDefault().map { !value.data.first() })
}.also { label = it }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,17 @@ class TabGroup<C : HTMLElement>(tag: Tag<C>, id: String?) : Tag<C> by tag {
}
}.handledBy(this, withActiveUpdates(::nextByKeys))

keydowns.filter { setOf(Keys.Home, Keys.PageUp).contains(shortcutOf(it)) }.map {
it.stopImmediatePropagation()
it.preventDefault()
}.handledBy(this, withActiveUpdates(::firstByKey))

keydowns.filter { setOf(Keys.End, Keys.PageDown).contains(shortcutOf(it)) }.map {
it.stopImmediatePropagation()
it.preventDefault()
}.handledBy(this, withActiveUpdates(::lastByKey))
keydowns.filter { setOf(Keys.Home, Keys.PageUp).contains(shortcutOf(it)) }
.stopImmediatePropagation()
.preventDefault()
.map { }
.handledBy(this, withActiveUpdates(::firstByKey))

keydowns.filter { setOf(Keys.End, Keys.PageDown).contains(shortcutOf(it)) }
.stopImmediatePropagation()
.preventDefault()
.map { }
.handledBy(this, withActiveUpdates(::lastByKey))
}

inner class Tab<CT : HTMLElement>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package dev.fritz2.headless.foundation

import dev.fritz2.core.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.*

/**
* Base class that provides all functionality needed for components, that have some "open" and "close" state of
Expand Down Expand Up @@ -43,11 +39,7 @@ abstract class OpenClose: WithJob {

val toggle by lazy {
SimpleHandler<Unit> { data, _ ->
openState.handler?.invoke(this, openState.data.flatMapLatest { state ->
data.map {
!state
}
})
openState.handler?.invoke(this, data.map { !opened.first() })
}
}

Expand All @@ -57,37 +49,24 @@ abstract class OpenClose: WithJob {
* `button` element behave natively.
*/
protected fun Tag<*>.toggleOnClicksEnterAndSpace() {
openState.handler?.invoke(this, openState.data.flatMapLatest { state ->
merge(
clicks,
keydowns.filter { setOf(Keys.Space, Keys.Enter).contains(shortcutOf(it)) }
).map {
it.preventDefault()
!state
}
})
merge(clicks, keydowns.filter { setOf(Keys.Space, Keys.Enter).contains(shortcutOf(it)) })
.preventDefault().stopPropagation() handledBy toggle
}

/**
* Apply this function on the panel representing [Tag] of the [OpenClose] implementing component, if the panel
* should be closed by pressing the *Escape* key.
*/
protected fun Tag<*>.closeOnEscape() {
openState.data.flatMapLatest { isOpen ->
Window.keydowns.filter { isOpen && shortcutOf(it) == Keys.Escape }
} handledBy close
Window.keydowns.filter { opened.first() && shortcutOf(it) == Keys.Escape } handledBy close
}

/**
* Apply this function on the panel representing [Tag] of the [OpenClose] implementing component, if the panel
* should be closed on clicking to somewhere outside the panel.
*/
protected fun Tag<*>.closeOnBlur() {
openState.data.flatMapLatest { isOpen ->
Window.clicks.filter { event ->
isOpen && event.composedPath().none { it == this }
}
} handledBy close
Window.clicks.filter { event -> opened.first() && event.composedPath().none { it == this } } handledBy close
}

}
Loading

0 comments on commit d7c3d02

Please sign in to comment.