Skip to content

Commit 0ee7ce2

Browse files
committed
Add the menu composables
1 parent af94e25 commit 0ee7ce2

File tree

5 files changed

+260
-1
lines changed

5 files changed

+260
-1
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Here is a list of supported compoent APIs:
2020
- `MdIcon`
2121
- `MdIconButton`, `MdFilledIconButton`, `MdFilledTonalIconButton`, `MdOutlinedIconButton`
2222
- `MdList`, `MdListItem`
23+
- `MdMenu`, `MdMenuItem`, `MdSubMenu`
2324
- `MdSwitch`, `LabelWithMdSwitch`
2425
- `MdFilledTextField`, `MdOutlinedTextField`
2526

@@ -52,6 +53,15 @@ kotlin {
5253
}
5354
```
5455

56+
This project depends on [Kobweb](https://github.com/varabyte/kobweb) which is not published to Maven Central yet, so you have to add the following Maven repository:
57+
58+
```kotlin
59+
repositories {
60+
mavenCentral()
61+
maven("https://us-central1-maven.pkg.dev/varabyte-repos/public")
62+
}
63+
```
64+
5565
### Material Symbols & Icons
5666

5767
The Material 3 module uses [Material Symbols & Icons](https://fonts.google.com/icons), but doesn't depend on the stylesheet directly. For Material Icons to work properly, you may need to configure your project following the quick instructions below or [the developer guide](https://developers.google.com/fonts/docs/material_symbols).

buildSrc/src/main/kotlin/VersionsAndDependencies.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import com.huanshankeji.CommonDependencies
22

3-
const val projectVersion = "0.3.0-SNAPSHOT"
3+
const val projectVersion = "0.4.0-SNAPSHOT"
44

55
object DependencyVersions {
66
val kobweb = "0.17.3"

compose-html-common/src/jsMain/kotlin/com/huanshankeji/compose/web/attributes/Attrs.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ fun AttrsScope<*>.attr(attr: String, value: Boolean = true) =
1515
fun AttrsScope<*>.attr(attr: String, value: Int) =
1616
attr(attr, value.toString())
1717

18+
fun AttrsScope<*>.attr(attr: String, value: Number) =
19+
attr(attr, value.toString())
20+
1821
/**
1922
* Adds an attribute that has an explicit [Boolean] value unlike [attr].
2023
*/
@@ -34,6 +37,10 @@ fun AttrsScope<*>.attrIfNotNull(attr: String, value: Int?) {
3437
value?.let { attr(attr, it) }
3538
}
3639

40+
fun AttrsScope<*>.attrIfNotNull(attr: String, value: Number?) {
41+
value?.let { attr(attr, it) }
42+
}
43+
3744

3845
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/slot
3946
fun AttrsScope<*>.slot(value: String) =

compose-html-common/src/jsMain/kotlin/com/huanshankeji/compose/web/attributes/ext/Attrs.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,5 +140,6 @@ fun AttrsScope<*>.enterKeyHintIfValid(value: String) {
140140
}
141141

142142

143+
// https://www.w3schools.com/tags/att_selected.asp
143144
fun AttrsScope<*>.selected(value: Boolean?) =
144145
attrIfNotNull("selected", value)
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package com.huanshankeji.compose.html.material3
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.web.events.SyntheticEvent
5+
import com.huanshankeji.compose.web.attributes.Attrs
6+
import com.huanshankeji.compose.web.attributes.attrIfNotNull
7+
import com.huanshankeji.compose.web.attributes.ext.*
8+
import com.huanshankeji.compose.web.attributes.slot
9+
import org.jetbrains.compose.web.attributes.AttrsScope
10+
import org.jetbrains.compose.web.dom.ElementScope
11+
import org.jetbrains.compose.web.dom.TagElement
12+
import org.w3c.dom.HTMLElement
13+
import org.w3c.dom.events.EventTarget
14+
15+
/*
16+
https://github.com/material-components/material-web/blob/main/docs/components/menu.md
17+
https://material-web.dev/components/menu/
18+
https://material-web.dev/components/menu/stories/
19+
*/
20+
21+
22+
private fun AttrsScope<HTMLElement>.anchorCorner(anchorCorner: String?) {
23+
attrIfNotNull("anchor-corner", anchorCorner)
24+
}
25+
26+
private fun AttrsScope<HTMLElement>.menuCorner(menuCorner: String?) {
27+
attrIfNotNull("menu-corner", menuCorner)
28+
}
29+
30+
31+
// made abstract so there is no need to add the implemented methods
32+
@JsModule("@material/web/menu/menu.js")
33+
@JsName("MdMenu")
34+
abstract external class MdMenuElement : HTMLElement {
35+
var anchorElement: HTMLElement?
36+
// ...
37+
}
38+
39+
@Composable
40+
fun MdMenu(
41+
anchor: String? = null,
42+
positioning: String? = null,
43+
quick: Boolean? = null,
44+
hasOverflow: Boolean? = null,
45+
open: Boolean? = null,
46+
// see https://stackoverflow.com/questions/4308989/are-the-decimal-places-in-a-css-width-respected
47+
xOffset: Number? = null,
48+
yOffset: Number? = null,
49+
typeheadDelay: Number? = null,
50+
anchorCorner: String? = null,
51+
menuCorner: String? = null,
52+
stayOpenOnOutsideClick: Boolean? = null,
53+
stayOpenOnFocusout: Boolean? = null,
54+
skipRestoreFocus: Boolean? = null,
55+
defaultFocus: Boolean? = null,
56+
attrs: Attrs<MdMenuElement>? = null,
57+
content: @Composable ElementScope<MdMenuElement>.() -> Unit
58+
) {
59+
require("@material/web/menu/menu.js")
60+
61+
TagElement<MdMenuElement>("md-menu", {
62+
attrIfNotNull("anchor", anchor)
63+
attrIfNotNull("positioning", positioning)
64+
attrIfNotNull("quick", quick)
65+
attrIfNotNull("has-overflow", hasOverflow)
66+
attrIfNotNull("open", open)
67+
attrIfNotNull("x-offset", xOffset)
68+
attrIfNotNull("y-offset", yOffset)
69+
attrIfNotNull("typehead-delay", typeheadDelay)
70+
anchorCorner(anchorCorner)
71+
menuCorner(menuCorner)
72+
attrIfNotNull("stay-open-on-outside-click", stayOpenOnOutsideClick)
73+
attrIfNotNull("stay-open-on-focusout", stayOpenOnFocusout)
74+
attrIfNotNull("skip-restore-focus", skipRestoreFocus)
75+
attrIfNotNull("default-focus", defaultFocus)
76+
77+
attrs?.invoke(this)
78+
}) {
79+
content()
80+
}
81+
}
82+
83+
// events
84+
85+
fun <T : SyntheticEvent<out EventTarget>> AttrsScope<MdMenuElement>.onOpening(listener: (T) -> Unit) =
86+
addEventListener("opening", listener)
87+
88+
fun <T : SyntheticEvent<out EventTarget>> AttrsScope<MdMenuElement>.onOpened(listener: (T) -> Unit) =
89+
addEventListener("opened", listener)
90+
91+
fun <T : SyntheticEvent<out EventTarget>> AttrsScope<MdMenuElement>.onClosing(listener: (T) -> Unit) =
92+
addEventListener("closing", listener)
93+
94+
fun <T : SyntheticEvent<out EventTarget>> AttrsScope<MdMenuElement>.onClosed(listener: (SyntheticEvent<EventTarget>) -> Unit) =
95+
addEventListener("closed", listener)
96+
97+
class MdMenuArgs(
98+
val anchor: String? = null,
99+
val positioning: String? = null,
100+
val quick: Boolean? = null,
101+
val hasOverflow: Boolean? = null,
102+
val open: Boolean? = null,
103+
val xOffset: Int? = null,
104+
val yOffset: Int? = null,
105+
val typeheadDelay: Number? = null,
106+
val anchorCorner: String? = null,
107+
val menuCorner: String? = null,
108+
val stayOpenOnOutsideClick: Boolean? = null,
109+
val stayOpenOnFocusout: Boolean? = null,
110+
val skipRestoreFocus: Boolean? = null,
111+
val defaultFocus: Boolean? = null,
112+
val attrs: Attrs<HTMLElement>? = null,
113+
val content: @Composable ElementScope<HTMLElement>.() -> Unit
114+
)
115+
116+
117+
@Composable
118+
fun MdMenuItem(
119+
disabled: Boolean? = null,
120+
type: String? = null,
121+
href: String? = null,
122+
target: String? = null,
123+
keepOpen: Boolean? = null,
124+
selected: Boolean? = null,
125+
attrs: Attrs<HTMLElement>? = null,
126+
content: @Composable MdMenuItemScope.() -> Unit
127+
) {
128+
require("@material/web/menu/menu-item.js")
129+
130+
TagElement<HTMLElement>("md-menu-item", {
131+
disabled(disabled)
132+
type(type)
133+
href(href)
134+
target(target)
135+
attrIfNotNull("keep-open", keepOpen)
136+
selected(selected)
137+
138+
attrs?.invoke(this)
139+
}) {
140+
MdMenuItemScope(this).content()
141+
}
142+
}
143+
144+
class MdMenuItemScope(val elementScope: ElementScope<HTMLElement>) {
145+
enum class Slot(val stringValue: String) {
146+
Headline("headline"), Start("start"), End("end")
147+
}
148+
149+
fun AttrsScope<*>.slot(value: Slot) =
150+
slot(value.stringValue)
151+
}
152+
153+
class MdMenuItemArgs(
154+
val disabled: Boolean? = null,
155+
val type: String? = null,
156+
val href: String? = null,
157+
val target: String? = null,
158+
val keepOpen: Boolean? = null,
159+
val selected: Boolean? = null,
160+
val attrs: Attrs<HTMLElement>? = null,
161+
val content: @Composable MdMenuItemScope.() -> Unit
162+
)
163+
164+
165+
@Composable
166+
fun MdSubMenu(
167+
anchorCorner: String? = null,
168+
menuCorner: String? = null,
169+
hoverOpenDelay: Number? = null,
170+
hoverCloseDelay: Number? = null,
171+
//isSubMenu : Boolean? = null, // `md-sub-menu`, read-only
172+
attrs: Attrs<HTMLElement>? = null,
173+
content: @Composable MdSubMenuScope.() -> Unit
174+
) {
175+
require("@material/web/menu/sub-menu.js")
176+
177+
TagElement<HTMLElement>("md-sub-menu", {
178+
anchorCorner(anchorCorner)
179+
menuCorner(menuCorner)
180+
attrIfNotNull("hover-open-delay", hoverOpenDelay)
181+
attrIfNotNull("hover-close-delay", hoverCloseDelay)
182+
183+
attrs?.invoke(this)
184+
}) {
185+
MdSubMenuScope(this).content()
186+
}
187+
}
188+
189+
class MdSubMenuScope(val elementScope: ElementScope<HTMLElement>) {
190+
enum class Slot(val stringValue: String) {
191+
Item("item"), Menu("menu")
192+
}
193+
194+
fun AttrsScope<*>.slot(value: Slot) =
195+
slot(value.stringValue)
196+
}
197+
198+
@Composable
199+
fun MdSubMenu(
200+
anchorCorner: String? = null,
201+
menuCorner: String? = null,
202+
hoverOpenDelay: Number? = null,
203+
hoverCloseDelay: Number? = null,
204+
//isSubMenu : Boolean? = null, // `md-sub-menu`
205+
attrs: Attrs<HTMLElement>? = null,
206+
mdMenuItemArgs: MdMenuItemArgs,
207+
mdMenuArgs: MdMenuArgs
208+
) =
209+
MdSubMenu(anchorCorner, menuCorner, hoverOpenDelay, hoverCloseDelay, attrs) {
210+
with(mdMenuItemArgs) {
211+
MdMenuItem(disabled, type, href, target, keepOpen, selected, {
212+
slot(MdSubMenuScope.Slot.Item)
213+
214+
attrs?.invoke(this)
215+
}, content)
216+
}
217+
with(mdMenuArgs) {
218+
MdMenu(
219+
anchor,
220+
positioning,
221+
quick,
222+
hasOverflow,
223+
open,
224+
xOffset,
225+
yOffset,
226+
typeheadDelay,
227+
this.anchorCorner,
228+
this.menuCorner,
229+
stayOpenOnOutsideClick,
230+
stayOpenOnFocusout,
231+
skipRestoreFocus,
232+
defaultFocus,
233+
{
234+
slot(MdSubMenuScope.Slot.Menu)
235+
236+
attrs?.invoke(this)
237+
},
238+
content
239+
)
240+
}
241+
}

0 commit comments

Comments
 (0)