Skip to content

Commit ce5ced2

Browse files
committed
Range: lay out tick marks with CSS grid instead of absolute positioning
Build `grid-template-columns` from the gaps between the datalist values so each tick lands on a grid line — this handles unevenly-spaced values just like the old per-tick calc did, but keeps the ticks and their labels in normal flow (the container sizes to fit the labels instead of them overflowing an absolutely-positioned box). Inset by half the thumb so the end ticks align with the thumb travel. Also wraps the token map in the `custom-property-no-missing-var-function` stylelint disable (matching _strength.scss). Includes the disabled-track fill styling.
1 parent e7b86c3 commit ce5ced2

3 files changed

Lines changed: 60 additions & 29 deletions

File tree

js/src/range.js

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const CLASS_NAME_TICK_LABEL = 'form-range-tick-label'
3636
// Shipped (`--bs-`-prefixed) custom properties; the build prefixes the SCSS tokens, so the
3737
// plugin must write the prefixed names to interoperate with the rendered CSS.
3838
const PROPERTY_FILL = '--bs-range-fill'
39-
const PROPERTY_TICK = '--bs-range-tick'
4039

4140
const Default = {
4241
bubble: false, // Show a value bubble above the thumb
@@ -182,25 +181,38 @@ class Range extends BaseComponent {
182181
const min = this._min()
183182
const span = this._max() - min || 1
184183

185-
this._ticks = document.createElement('div')
186-
this._ticks.className = CLASS_NAME_TICKS
187-
this._ticks.setAttribute('aria-hidden', 'true')
188-
184+
const points = []
189185
for (const option of SelectorEngine.find('option', datalist)) {
190186
const value = Number.parseFloat(option.value)
191187

192-
if (Number.isNaN(value)) {
193-
continue
188+
if (!Number.isNaN(value)) {
189+
points.push({ ratio: (value - min) / span, label: option.label })
194190
}
191+
}
192+
193+
if (points.length === 0) {
194+
return
195+
}
196+
197+
points.sort((a, b) => a.ratio - b.ratio)
198+
199+
this._ticks = document.createElement('div')
200+
this._ticks.className = CLASS_NAME_TICKS
201+
this._ticks.setAttribute('aria-hidden', 'true')
202+
203+
// Columns are the gaps between 0, each tick, and 1, so every tick lands on a grid line
204+
const stops = [0, ...points.map(point => point.ratio), 1]
205+
this._ticks.style.gridTemplateColumns = stops.slice(1).map((stop, index) => `${stop - stops[index]}fr`).join(' ')
195206

207+
for (const [index, point] of points.entries()) {
196208
const tick = document.createElement('span')
197209
tick.className = CLASS_NAME_TICK
198-
tick.style.setProperty(PROPERTY_TICK, `${(value - min) / span}`)
210+
tick.style.gridColumnStart = `${index + 2}`
199211

200-
if (option.label) {
212+
if (point.label) {
201213
const label = document.createElement('span')
202214
label.className = CLASS_NAME_TICK_LABEL
203-
label.textContent = option.label
215+
label.textContent = point.label
204216
tick.append(label)
205217
}
206218

js/tests/unit/range.spec.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,20 @@ describe('Range', () => {
185185
expect(ticks.length).toEqual(3)
186186
})
187187

188-
it('should set the --bs-range-tick ratio per tick (handles uneven values)', () => {
188+
it('should place each tick on a grid line via grid-template-columns (handles uneven values)', () => {
189189
fixtureEl.innerHTML = getTicksHtml()
190190

191191
const rangeEl = fixtureEl.querySelector('.form-range')
192192
new Range(rangeEl) // eslint-disable-line no-new
193193

194+
// datalist values 0/10/100 -> gaps between 0, .1, 1, and 1
195+
const ticksEl = fixtureEl.querySelector('.form-range-ticks')
196+
expect(ticksEl.style.gridTemplateColumns).toEqual('0fr 0.1fr 0.9fr 0fr')
197+
194198
const ticks = fixtureEl.querySelectorAll('.form-range-tick')
195-
expect(ticks[0].style.getPropertyValue('--bs-range-tick')).toEqual('0')
196-
expect(ticks[1].style.getPropertyValue('--bs-range-tick')).toEqual('0.1')
197-
expect(ticks[2].style.getPropertyValue('--bs-range-tick')).toEqual('1')
199+
expect(ticks[0].style.gridColumnStart).toEqual('2')
200+
expect(ticks[1].style.gridColumnStart).toEqual('3')
201+
expect(ticks[2].style.gridColumnStart).toEqual('4')
198202
})
199203

200204
it('should render labels from the option label only', () => {

scss/forms/_form-range.scss

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@use "../mixins/gradients" as *;
77
@use "../mixins/tokens" as *;
88

9+
// stylelint-disable custom-property-no-missing-var-function
910
$range-tokens: () !default;
1011

1112
// scss-docs-start range-tokens
@@ -18,6 +19,7 @@ $range-tokens: defaults(
1819
--range-track-bg: var(--bg-3),
1920
--range-track-border-radius: 1rem,
2021
--range-track-fill-bg: var(--primary-base),
22+
--range-track-disabled-bg: color-mix(in oklch, var(--bg-4), var(--fg-3)),
2123
--range-thumb-width: 1rem,
2224
--range-thumb-height: var(--range-thumb-width),
2325
--range-thumb-bg: var(--primary-base),
@@ -36,6 +38,7 @@ $range-tokens: defaults(
3638
$range-tokens
3739
);
3840
// scss-docs-end range-tokens
41+
// stylelint-enable custom-property-no-missing-var-function
3942

4043
// scss-docs-start range-mixins
4144
@mixin range-thumb() {
@@ -144,6 +147,14 @@ $range-tokens: defaults(
144147
&::-moz-range-thumb {
145148
background-color: var(--range-thumb-disabled-bg);
146149
}
150+
151+
&::-webkit-slider-runnable-track {
152+
--range-track-fill-bg: var(--range-track-disabled-bg);
153+
}
154+
155+
&::-moz-range-track {
156+
--range-track-fill-bg: var(--range-track-disabled-bg);
157+
}
147158
}
148159
}
149160

@@ -165,30 +176,34 @@ $range-tokens: defaults(
165176
}
166177
}
167178

168-
// Tick marks generated from the linked <datalist>. Each tick gets `--range-tick` (0–1) and is
169-
// placed along the track, inset by half the thumb so the ends line up with the thumb travel.
179+
// Tick marks generated from the linked <datalist>. The plugin builds `grid-template-columns`
180+
// from the gaps between values so each tick lands on a grid line (handles uneven values),
181+
// inset by half the thumb so the ends line up with the thumb travel.
170182
.form-range-ticks {
171-
position: relative;
172-
height: var(--range-tick-height);
183+
display: grid;
184+
padding-inline: calc(var(--range-thumb-width) * .5);
173185
margin-top: .25rem;
174186
}
175187

176188
.form-range-tick {
177-
position: absolute;
178-
left: calc((var(--range-thumb-width) * .5) + var(--range-tick, 0) * (100% - var(--range-thumb-width)));
179-
width: var(--range-tick-width);
180-
height: var(--range-tick-height);
181-
background-color: var(--range-tick-bg);
189+
display: flex;
190+
flex-direction: column;
191+
align-items: center;
192+
justify-self: start;
182193
transform: translateX(-50%);
194+
195+
&::before {
196+
width: var(--range-tick-width);
197+
height: var(--range-tick-height);
198+
content: "";
199+
background-color: var(--range-tick-bg);
200+
}
183201
}
184202

185203
.form-range-tick-label {
186-
position: absolute;
187-
top: 100%;
188-
left: 50%;
189-
font-size: var(--font-size-xs);
190-
color: var(--fg-1);
204+
margin-top: .125rem;
205+
font-size: var(--font-size-sm);
206+
color: var(--fg-2);
191207
white-space: nowrap;
192-
transform: translateX(-50%);
193208
}
194209
}

0 commit comments

Comments
 (0)