Skip to content

Commit 1a65cd3

Browse files
committed
✨ Add RangeSlider component
1 parent beebe51 commit 1a65cd3

File tree

13 files changed

+1045
-14
lines changed

13 files changed

+1045
-14
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ html body {
179179
// Radio component
180180
--w-radio-color: var(--w-color-primary);
181181

182+
// RangeSlider component
183+
--w-range-slider-background: var(--w-color-primary-50);
184+
--w-range-slider-color: var(--w-color-primary);
185+
--w-range-slider-thumb: var(--w-color-primary-50);
186+
182187
// Rating component
183188
--w-rating-color: var(--w-color-primary);
184189
--w-rating-empty-color: var(--w-color-primary);
@@ -299,6 +304,7 @@ import { Accordion } from 'webcoreui/react'
299304
- [Popover](https://github.com/Frontendland/webcoreui/tree/main/src/components/Popover)
300305
- [Progress](https://github.com/Frontendland/webcoreui/tree/main/src/components/Progress)
301306
- [Radio](https://github.com/Frontendland/webcoreui/tree/main/src/components/Radio)
307+
- [RangeSlider](https://github.com/Frontendland/webcoreui/tree/main/src/components/RangeSlider)
302308
- [Rating](https://github.com/Frontendland/webcoreui/tree/main/src/components/Rating)
303309
- [Ribbon](https://github.com/Frontendland/webcoreui/tree/main/src/components/Ribbon)
304310
- [Select](https://github.com/Frontendland/webcoreui/tree/main/src/components/Select)

scripts/additionalTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const additionalTypes = {
55
],
66
List: ['ListEventType'],
77
Pagination: ['PaginationEventType'],
8+
RangeSlider: ['RangeSliderEventType'],
89
Select: ['SelectEventType']
910
}
1011

scripts/utilityTypes.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ declare module 'webcoreui' {
152152
callback: any,
153153
all?: boolean
154154
) => void
155+
export const off = (
156+
selector: string | HTMLElement | Document,
157+
event: string,
158+
fn: any,
159+
all?: boolean
160+
) => void
155161
156162
export const dispatch: (event: string, detail: unknown) => void
157163
export const listen: (event: string, callback: (e: any) => unknown) => {
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
---
2+
import type { RangeSliderProps } from './rangeslider'
3+
4+
import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.astro'
5+
import Icon from '../Icon/Icon.astro'
6+
7+
import { classNames } from '../../utils/classNames'
8+
import { interpolate } from '../../utils/interpolate'
9+
10+
import styles from './rangeslider.module.scss'
11+
12+
interface Props extends RangeSliderProps {}
13+
14+
const {
15+
min = 0,
16+
max = 100,
17+
selectedMin,
18+
selectedMax,
19+
step = 1,
20+
minGap = 5,
21+
disabled,
22+
color,
23+
background,
24+
thumb,
25+
label,
26+
subText,
27+
minLabel,
28+
maxLabel,
29+
minIcon,
30+
maxIcon,
31+
interactiveLabels,
32+
updateLabels,
33+
className
34+
} = Astro.props
35+
36+
const styleVariables = classNames([
37+
color && `--w-range-slider-color: ${color};`,
38+
background && `--w-range-slider-background: ${background};`,
39+
thumb && `--w-range-slider-thumb: ${thumb};`
40+
])
41+
42+
const minLabelWidth = `${String(max).length}ch`
43+
const labelStyle = updateLabels ? `min-width:${minLabelWidth};` : null
44+
---
45+
46+
<ConditionalWrapper condition={!!(label || subText)}>
47+
<label slot="wrapper" class:list={[styles.label, className]}>children</label>
48+
49+
{label && <span>{label}</span>}
50+
51+
<div
52+
class:list={[styles.container, !(label && subText) && className]}
53+
data-id="w-range-slider"
54+
data-gap={minGap}
55+
data-interactive={interactiveLabels}
56+
data-update-labels={updateLabels}
57+
style={styleVariables}
58+
>
59+
<ConditionalWrapper condition={!!interactiveLabels}>
60+
<button slot="wrapper" data-dir="left">children</button>
61+
62+
{minIcon && (
63+
<Fragment>
64+
{minIcon.startsWith('<svg')
65+
? <Fragment set:html={minIcon} />
66+
: <Icon type={minIcon} size={18} />
67+
}
68+
</Fragment>
69+
)}
70+
{minLabel && (
71+
<span data-id="w-min-label" style={labelStyle}>{minLabel}</span>
72+
)}
73+
</ConditionalWrapper>
74+
75+
<div class={styles.slider}>
76+
<div
77+
data-id="w-range"
78+
data-disabled={disabled ? 'true' : undefined}
79+
class={styles.range}
80+
style={`
81+
left: ${interpolate(selectedMin || min, [min, max], [0, 100])}%;
82+
right: ${interpolate(selectedMax || max, [min, max], [100, 0])}%;
83+
`}
84+
/>
85+
<input
86+
type="range"
87+
class:list={[styles.input, styles.min]}
88+
min={min}
89+
max={max}
90+
value={selectedMin || min}
91+
step={step}
92+
disabled={disabled}
93+
data-min="true"
94+
/>
95+
<input
96+
type="range"
97+
min={min}
98+
max={max}
99+
class={styles.input}
100+
value={selectedMax || max}
101+
step={step}
102+
disabled={disabled}
103+
data-max="true"
104+
/>
105+
</div>
106+
107+
<ConditionalWrapper condition={!!interactiveLabels}>
108+
<button slot="wrapper" data-dir="right">children</button>
109+
110+
{maxLabel && (
111+
<span data-id="w-max-label" style={labelStyle}>{maxLabel}</span>
112+
)}
113+
{maxIcon && (
114+
<Fragment>
115+
{maxIcon.startsWith('<svg')
116+
? <Fragment set:html={maxIcon} />
117+
: <Icon type={maxIcon} size={14} />
118+
}
119+
</Fragment>
120+
)}
121+
</ConditionalWrapper>
122+
</div>
123+
124+
{subText && <span class="muted">{subText}</span>}
125+
</ConditionalWrapper>
126+
127+
<script>
128+
import { off, on } from '../../utils/DOMUtils'
129+
import { dispatch } from '../../utils/event'
130+
import { interpolate } from '../../utils/interpolate'
131+
132+
type RangeParams = {
133+
range: HTMLDivElement
134+
minValue: number
135+
maxValue: number
136+
min: number
137+
max: number
138+
}
139+
140+
const updateRange = ({ range, minValue, maxValue, min, max }: RangeParams) => {
141+
range.style.left = `${interpolate(minValue, [min, max], [0, 100])}%`
142+
range.style.right = `${interpolate(maxValue, [min, max], [100, 0])}%`
143+
}
144+
145+
const updateLabels = (wrapper: HTMLDivElement, minValue: number, maxValue: number) => {
146+
const minLabel = wrapper.querySelector('[data-id="w-min-label"]')
147+
const maxLabel = wrapper.querySelector('[data-id="w-max-label"]')
148+
149+
if (minLabel instanceof HTMLElement && maxLabel instanceof HTMLElement) {
150+
minLabel.innerText = minLabel.innerText.replace(/\d+(\.\d+)?/, String(minValue))
151+
maxLabel.innerText = maxLabel.innerText.replace(/\d+(\.\d+)?/, String(maxValue))
152+
}
153+
}
154+
155+
const addEventListeners = () => {
156+
on('[data-id="w-range-slider"] input', 'input', (event: Event) => {
157+
const target = event.target
158+
159+
if (!(target instanceof HTMLInputElement)) {
160+
return
161+
}
162+
163+
const range = target.parentElement?.querySelector('[data-id="w-range"]')
164+
const wrapper = target.parentElement?.parentElement
165+
const prevInput = target.previousElementSibling
166+
const nextInput = target.nextElementSibling
167+
168+
if (!(wrapper instanceof HTMLDivElement) || !(range instanceof HTMLDivElement)) {
169+
return
170+
}
171+
172+
const value = Number(target.value)
173+
const min = Number(target.min)
174+
const max = Number(target.max)
175+
const gap = Number(wrapper.dataset.gap)
176+
const shouldUpdateLabels = !!wrapper.dataset.updateLabels
177+
const prevInputValue = prevInput instanceof HTMLInputElement ? prevInput.value : 0
178+
const nextInputValue = nextInput instanceof HTMLInputElement ? nextInput.value : 0
179+
const minValue = target.dataset.min ? value : Number(prevInputValue)
180+
const maxValue = target.dataset.max ? value : Number(nextInputValue)
181+
182+
if (maxValue - minValue >= gap) {
183+
updateRange({
184+
range,
185+
minValue,
186+
maxValue,
187+
min,
188+
max
189+
})
190+
191+
if (shouldUpdateLabels) {
192+
updateLabels(wrapper, minValue, maxValue)
193+
}
194+
195+
dispatch('rangeSliderOnChange', {
196+
min: minValue,
197+
max: maxValue
198+
})
199+
} else if (target.dataset.min) {
200+
target.value = String(maxValue - gap)
201+
} else {
202+
target.value = String(minValue + gap)
203+
}
204+
}, true)
205+
206+
on('[data-id="w-range-slider"] button', 'click', (event: Event) => {
207+
const target = event.currentTarget
208+
209+
if (!(target instanceof HTMLButtonElement)) {
210+
return
211+
}
212+
213+
const wrapper = target.parentElement
214+
const range = wrapper?.querySelector('[data-id="w-range"]')
215+
const minInput = wrapper?.querySelector('[data-min]')
216+
const maxInput = wrapper?.querySelector('[data-max]')
217+
218+
if (!(wrapper instanceof HTMLDivElement)
219+
|| !(range instanceof HTMLDivElement)
220+
|| !(minInput instanceof HTMLInputElement)
221+
|| !(maxInput instanceof HTMLInputElement)
222+
) {
223+
return
224+
}
225+
226+
const dir = target.dataset.dir === 'left' ? -1 : 1
227+
const step = Number(minInput.step)
228+
const min = Number(minInput.min)
229+
const max = Number(minInput.max)
230+
const minValue = Number(minInput.value) + (dir * step)
231+
const maxValue = Number(maxInput.value) + (dir * step)
232+
const shouldUpdateLabels = !!wrapper.dataset.updateLabels
233+
234+
if (minValue < min || maxValue > max) {
235+
return
236+
}
237+
238+
minInput.value = String(minValue)
239+
maxInput.value = String(maxValue)
240+
241+
updateRange({
242+
range,
243+
minValue,
244+
maxValue,
245+
min,
246+
max
247+
})
248+
249+
if (shouldUpdateLabels) {
250+
updateLabels(wrapper, minValue, maxValue)
251+
}
252+
253+
dispatch('rangeSliderOnChange', {
254+
min: minValue,
255+
max: maxValue
256+
})
257+
}, true)
258+
}
259+
260+
off(document, 'astro:after-swap', addEventListeners)
261+
on(document, 'astro:after-swap', addEventListeners)
262+
263+
addEventListeners()
264+
</script>

0 commit comments

Comments
 (0)