Skip to content

Commit a1c3624

Browse files
committed
✨ Add SpeedDial component
1 parent 37fb50b commit a1c3624

File tree

12 files changed

+685
-0
lines changed

12 files changed

+685
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ html body {
170170
--w-slider-color: var(--w-color-primary);
171171
--w-slider-thumb: var(--w-color-primary-50);
172172

173+
// SpeedDial component
174+
--w-speed-dial-size: 50px;
175+
173176
// Spinner component
174177
--w-spinner-color: var(--w-color-primary);
175178
--w-spinner-width: 2px;
@@ -269,6 +272,7 @@ import { Accordion } from 'webcoreui/react'
269272
- [Sidebar](https://github.com/Frontendland/webcoreui/tree/main/src/components/Sidebar)
270273
- [Skeleton](https://github.com/Frontendland/webcoreui/tree/main/src/components/Skeleton)
271274
- [Slider](https://github.com/Frontendland/webcoreui/tree/main/src/components/Slider)
275+
- [SpeedDial](https://github.com/Frontendland/webcoreui/tree/main/src/components/SpeedDial)
272276
- [Spinner](https://github.com/Frontendland/webcoreui/tree/main/src/components/Spinner)
273277
- [Spoiler](https://github.com/Frontendland/webcoreui/tree/main/src/components/Spoiler)
274278
- [Stepper](https://github.com/Frontendland/webcoreui/tree/main/src/components/Stepper)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
import type { SpeedDialProps } from './speeddial'
3+
4+
import Button from '../Button/Button.astro'
5+
import Icon from '../Icon/Icon.astro'
6+
7+
import styles from './speeddial.module.scss'
8+
9+
interface Props extends SpeedDialProps {}
10+
11+
const {
12+
items,
13+
position,
14+
horizontal,
15+
circular,
16+
theme,
17+
icon,
18+
triggerOnClick,
19+
className
20+
} = Astro.props
21+
22+
const classes = [
23+
styles.dial,
24+
position && styles[position],
25+
horizontal && styles.horizontal,
26+
circular && styles.circular,
27+
className
28+
]
29+
30+
const getTooltipPosition = () => {
31+
const positionMap = {
32+
'top-left': 'right',
33+
'bottom-left': 'right',
34+
'horizontal': {
35+
'top-left': 'bottom',
36+
'top-right': 'bottom'
37+
}
38+
}
39+
40+
return horizontal
41+
? positionMap.horizontal[position as keyof typeof positionMap.horizontal]
42+
: positionMap[position as keyof typeof positionMap] || 'left'
43+
}
44+
---
45+
46+
{!!items?.length && (
47+
<div class:list={classes} data-id={triggerOnClick ? 'w-speed-dial' : null}>
48+
<Button className={styles.trigger} {...(theme && { theme })}>
49+
{icon && (
50+
<Fragment>
51+
{icon.startsWith('<svg')
52+
? <Fragment set:html={icon} />
53+
: <Icon type={icon} size={18} />
54+
}
55+
</Fragment>
56+
)}
57+
{!icon && <span>+</span>}
58+
</Button>
59+
60+
<ul class={styles.list}>
61+
{items.map(item => (
62+
<li>
63+
<Button
64+
data-tooltip={item.tooltip}
65+
data-position={getTooltipPosition()}
66+
href={item.href}
67+
target={item.target}
68+
className={styles.button}
69+
{...(theme && { theme })}
70+
>
71+
{item.icon.startsWith('<svg')
72+
? <Fragment set:html={item.icon} />
73+
: <Icon type={item.icon} size={16} />
74+
}
75+
</Button>
76+
</li>
77+
))}
78+
</ul>
79+
</div>
80+
)}
81+
82+
<script>
83+
import { get, on } from '../../utils/DOMUtils'
84+
85+
const addEventListeners = () => {
86+
const speedDial = get('[data-id="w-speed-dial"] button') as HTMLButtonElement
87+
const speedDialContainer = speedDial?.parentElement as HTMLDivElement
88+
89+
if (speedDial) {
90+
on(speedDial, 'click', () => {
91+
speedDialContainer.dataset.show = speedDialContainer.dataset.show === 'true'
92+
? 'false'
93+
: 'true'
94+
})
95+
96+
on(document, 'click', (event: Event) => {
97+
if (!speedDial.contains((event.target || event.currentTarget) as HTMLElement)) {
98+
speedDialContainer.dataset.show = 'false'
99+
}
100+
})
101+
}
102+
}
103+
104+
on(document, 'astro:after-swap', addEventListeners)
105+
addEventListeners()
106+
</script>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte'
3+
import type { SpeedDialProps } from './speeddial'
4+
5+
import Button from '../Button/Button.svelte'
6+
7+
import { classNames } from '../../utils/classNames'
8+
import { get, on } from '../../utils/DOMUtils'
9+
10+
import styles from './speeddial.module.scss'
11+
12+
const {
13+
items,
14+
position,
15+
horizontal,
16+
circular,
17+
theme,
18+
icon,
19+
triggerOnClick,
20+
className
21+
}: SpeedDialProps = $props()
22+
23+
let show = $state(false)
24+
25+
const classes = classNames([
26+
styles.dial,
27+
position && styles[position],
28+
horizontal && styles.horizontal,
29+
circular && styles.circular,
30+
className
31+
])
32+
33+
const getTooltipPosition = () => {
34+
const positionMap = {
35+
'top-left': 'right',
36+
'bottom-left': 'right',
37+
'horizontal': {
38+
'top-left': 'bottom',
39+
'top-right': 'bottom'
40+
}
41+
}
42+
43+
return horizontal
44+
? positionMap.horizontal[position as keyof typeof positionMap.horizontal]
45+
: positionMap[position as keyof typeof positionMap] || 'left'
46+
}
47+
48+
const toggle = () => show = !show
49+
50+
onMount(() => {
51+
const speedDial = get('[data-id="w-speed-dial"] button') as HTMLButtonElement
52+
53+
const eventListener = (event: Event) => {
54+
if (!speedDial.contains((event.target || event.currentTarget) as HTMLElement)) {
55+
show = false
56+
}
57+
}
58+
59+
on(document, 'click', eventListener)
60+
61+
return () => {
62+
document.removeEventListener('click', eventListener)
63+
}
64+
})
65+
</script>
66+
67+
{#if items?.length}
68+
<div
69+
class={classes}
70+
data-id={triggerOnClick ? 'w-speed-dial' : null}
71+
data-show={triggerOnClick ? show : null}
72+
>
73+
<Button
74+
className={styles.trigger}
75+
onClick={triggerOnClick ? toggle : undefined}
76+
{...(theme && { theme })}
77+
>
78+
{#if icon}
79+
{@html icon}
80+
{:else}
81+
<span>+</span>
82+
{/if}
83+
</Button>
84+
85+
<ul class={styles.list}>
86+
{#each items as item}
87+
<li>
88+
<Button
89+
data-tooltip={item.tooltip}
90+
data-position={getTooltipPosition()}
91+
{...(theme && { theme })}
92+
href={item.href}
93+
target={item.target}
94+
className={styles.button}
95+
>
96+
{@html item.icon}
97+
</Button>
98+
</li>
99+
{/each}
100+
</ul>
101+
</div>
102+
{/if}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useEffect, useState } from 'react'
2+
import type { SpeedDialProps } from './speeddial'
3+
4+
import Button from '../Button/Button.tsx'
5+
6+
import { classNames } from '../../utils/classNames'
7+
import { get, on } from '../../utils/DOMUtils'
8+
9+
import styles from './speeddial.module.scss'
10+
11+
const SpeedDial = ({
12+
items,
13+
position,
14+
horizontal,
15+
circular,
16+
theme,
17+
icon,
18+
triggerOnClick,
19+
className
20+
}: SpeedDialProps) => {
21+
const [show, setShow] = useState(false)
22+
23+
const classes = classNames([
24+
styles.dial,
25+
position && styles[position],
26+
horizontal && styles.horizontal,
27+
circular && styles.circular,
28+
className
29+
])
30+
31+
const getTooltipPosition = () => {
32+
const positionMap = {
33+
'top-left': 'right',
34+
'bottom-left': 'right',
35+
'horizontal': {
36+
'top-left': 'bottom',
37+
'top-right': 'bottom'
38+
}
39+
}
40+
41+
return horizontal
42+
? positionMap.horizontal[position as keyof typeof positionMap.horizontal]
43+
: positionMap[position as keyof typeof positionMap] || 'left'
44+
}
45+
46+
const toggle = () => {
47+
setShow(!show)
48+
}
49+
50+
useEffect(() => {
51+
const speedDial = get('[data-id="w-speed-dial"] button') as HTMLButtonElement
52+
53+
const eventListener = (event: Event) => {
54+
if (!speedDial.contains((event.target || event.currentTarget) as HTMLElement)) {
55+
setShow(false)
56+
}
57+
}
58+
59+
on(document, 'click', eventListener)
60+
61+
return () => {
62+
document.removeEventListener('click', eventListener)
63+
}
64+
}, [])
65+
66+
if (!items?.length) {
67+
return null
68+
}
69+
70+
return (
71+
<div
72+
className={classes}
73+
data-id={triggerOnClick ? 'w-speed-dial' : null}
74+
data-show={triggerOnClick ? show : null}
75+
>
76+
<Button
77+
className={styles.trigger}
78+
onClick={triggerOnClick ? toggle : undefined}
79+
{...(theme && { theme })}
80+
>
81+
{icon && (
82+
<span dangerouslySetInnerHTML={{ __html: icon }} />
83+
)}
84+
{!icon && <span>+</span>}
85+
</Button>
86+
87+
<ul className={styles.list}>
88+
{items.map((item, index) => (
89+
<li key={index}>
90+
<Button
91+
data-tooltip={item.tooltip}
92+
data-position={getTooltipPosition()}
93+
href={item.href}
94+
target={item.target}
95+
className={styles.button}
96+
dangerouslySetInnerHTML={{ __html: item.icon }}
97+
{...(theme && { theme })}
98+
/>
99+
</li>
100+
))}
101+
</ul>
102+
</div>
103+
)
104+
}
105+
106+
export default SpeedDial

0 commit comments

Comments
 (0)