-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(slider): init * refactor(slider): add semantic html and story * feat(slider): styling wip * refactor(slider): remove datalist and options * feat(slider): finish styling default slider * refactor(slider): temp remove unnecessary disabled state * refactor(slider): support not focus-visible * test(slider): add no label story and snaps * refactor(slider): shift and dry styles * feat(slider): add cursor and disabled styling * test(slider): add disabled and focus stories * fix(slider): increase size and hitbox for axe * feat(slider): add markers * fix(slider): align markers to values * test(slider): update snaps * fix(slider): make marker color high enough contrast * docs(slider): clean comments and strengthen type * feat(slider): generate markers and more style disabled * docs(slider): add helper fn docstring and dry stories * refactor(slider): move util func to util folder * feat(slider): allow fieldnote as aria description
- Loading branch information
Showing
9 changed files
with
965 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
@import '../../design-tokens/mixins.css'; | ||
|
||
/*------------------------------------*\ | ||
# SLIDER | ||
\*------------------------------------*/ | ||
|
||
/** | ||
* Slider wrapping Label and Input | ||
*/ | ||
.slider { | ||
--slider-track-height: var(--eds-size-1); | ||
--slider-thumb-size: var(--eds-size-3); | ||
|
||
display: flex; | ||
flex-direction: column; | ||
gap: var(--eds-size-1-and-half); | ||
} | ||
|
||
/** | ||
* Slider label disabled | ||
*/ | ||
.slider__label--disabled { | ||
color: var(--eds-theme-color-text-disabled); | ||
} | ||
|
||
/** | ||
* Slider Input | ||
*/ | ||
.slider__input { | ||
/* increases vertical hitbox for target size accessibility */ | ||
padding-top: 22px; | ||
padding-bottom: 22px; | ||
/* fills left side of track as a percentage of the input value */ | ||
--slider-track-background: linear-gradient( | ||
/* fill from left to right */ to right, | ||
/* fill color */ var(--eds-theme-color-background-brand-primary-strong) | ||
/* percent to fill */ calc(var(--ratio) * 100%), | ||
/* unfilled color */ var(--eds-theme-color-background-neutral-medium) | ||
/* don't blend the colors */ 0 | ||
); | ||
|
||
appearance: none; | ||
background: transparent; | ||
|
||
height: var(--slider-thumb-size); | ||
} | ||
.slider__input:focus { | ||
outline: none; | ||
} | ||
.slider__input:disabled { | ||
--slider-track-background: linear-gradient( | ||
/* fill from left to right */ to right, | ||
/* fill color */ var(--eds-theme-color-background-disabled) | ||
/* percent to fill */ calc(var(--ratio) * 100%), | ||
/* unfilled color */ var(--eds-theme-color-background-neutral-medium) | ||
/* don't blend the colors */ 0 | ||
); | ||
cursor: not-allowed; | ||
} | ||
|
||
/* | ||
* Chrome, Safari, Edge Chromium | ||
* Although redundant with Firefox, has to be separate or else Chrome ignores | ||
*/ | ||
.slider__input::-webkit-slider-runnable-track { | ||
background: var(--slider-track-background); | ||
height: var(--slider-track-height); | ||
border-radius: var(--eds-border-radius-full); | ||
} | ||
/** | ||
* Slider Input Track | ||
*/ | ||
/* Firefox */ | ||
.slider__input::-moz-range-track { | ||
background: var(--slider-track-background); | ||
height: var(--slider-track-height); | ||
border-radius: var(--eds-border-radius-full); | ||
} | ||
|
||
/* Chrome, Safari, Edge Chromium */ | ||
.slider__input::-webkit-slider-thumb { | ||
appearance: none; | ||
|
||
height: var(--slider-thumb-size); | ||
width: var(--slider-thumb-size); | ||
background: var(--eds-theme-color-text-neutral-default-inverse); | ||
border: var(--eds-border-width-md) solid | ||
var(--eds-theme-color-border-neutral-default); | ||
border-radius: var(--eds-border-radius-full); | ||
|
||
margin-top: calc( | ||
var(--slider-track-height) / 2 - var(--slider-thumb-size) / 2 | ||
); /* Centers thumb on the track */ | ||
} | ||
.slider__input:not(:disabled)::-webkit-slider-thumb { | ||
cursor: grab; | ||
} | ||
.slider__input:not(:disabled)::-webkit-slider-thumb:active { | ||
cursor: grabbing; | ||
} | ||
/* Chrome, Safari, Edge Chromium Focus */ | ||
.slider__input:focus-visible::-webkit-slider-thumb { | ||
@mixin focus; | ||
} | ||
@supports not selector(:focus-visible) { | ||
.slider__input:focus::-webkit-slider-thumb { | ||
@mixin focus; | ||
} | ||
} | ||
|
||
/* | ||
* Slider Input Thumb | ||
*/ | ||
/* Firefox */ | ||
.slider__input::-moz-range-thumb { | ||
box-sizing: border-box; | ||
|
||
height: var(--slider-thumb-size); | ||
width: var(--slider-thumb-size); | ||
background: var(--eds-theme-color-text-neutral-default-inverse); | ||
border: var(--eds-border-width-md) solid | ||
var(--eds-theme-color-border-neutral-default); | ||
border-radius: var(--eds-border-radius-full); | ||
} | ||
.slider__input:not(:disabled)::-moz-range-thumb { | ||
cursor: grab; | ||
} | ||
.slider__input:not(:disabled)::-moz-range-thumb:active { | ||
cursor: grabbing; | ||
} | ||
/* Firefox Focus */ | ||
.slider__input:focus-visible::-moz-range-thumb { | ||
@mixin focus; | ||
} | ||
@supports not selector(:focus-visible) { | ||
.slider__input:focus::-moz-range-thumb { | ||
@mixin focus; | ||
} | ||
} | ||
|
||
/** | ||
* Slider Markers wrapper below the track | ||
*/ | ||
.slider__markers { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
|
||
/* Calculates offset of the markers to align with actual values */ | ||
padding-left: calc(var(--slider-thumb-size) / 2); | ||
padding-right: calc(var(--slider-thumb-size) / 2); | ||
} | ||
|
||
/** | ||
* Slider Marker | ||
*/ | ||
.slider__marker { | ||
@mixin eds-theme-typography-caption-text-sm; | ||
|
||
/* Centers the text to the marker location */ | ||
width: 0px; | ||
display: flex; | ||
justify-content: center; | ||
} | ||
|
||
.slider__marker--disabled { | ||
color: var(--eds-theme-color-text-disabled); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { BADGE } from '@geometricpanda/storybook-addon-badges'; | ||
import type { StoryObj, Meta } from '@storybook/react'; | ||
import { userEvent } from '@storybook/testing-library'; | ||
import React, { useState } from 'react'; | ||
|
||
import { Slider } from './Slider'; | ||
|
||
export default { | ||
title: 'Components/Slider', | ||
component: Slider, | ||
parameters: { | ||
layout: 'centered', | ||
badges: [BADGE.BETA], | ||
}, | ||
decorators: [ | ||
(Story) => ( | ||
<div className="w-96"> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
render: (args) => <InteractiveSlider {...args} />, | ||
} as Meta<Args>; | ||
|
||
const InteractiveSlider = ({ | ||
min = 0, | ||
max = 100, | ||
step = 1, | ||
value = 50, | ||
...args | ||
}: Args) => { | ||
const [sliderValue, setSliderValue] = useState(value); | ||
return ( | ||
<Slider | ||
max={max} | ||
min={min} | ||
step={step} | ||
{...args} | ||
onChange={(e) => setSliderValue(Number(e.target.value))} | ||
value={sliderValue} | ||
/> | ||
); | ||
}; | ||
|
||
type Args = React.ComponentProps<typeof Slider>; | ||
|
||
export const Default: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
}, | ||
}; | ||
|
||
export const NoVisibleLabel: StoryObj<Args> = { | ||
args: { | ||
'aria-label': 'Not visible slider label', | ||
}, | ||
}; | ||
|
||
export const GeneratedMarkers: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
min: 1, | ||
max: 5, | ||
value: 3, | ||
step: 1, | ||
markers: 'number', | ||
}, | ||
}; | ||
|
||
export const NegativeNonIntegerMarkers: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
min: -1, | ||
max: 1, | ||
value: 0, | ||
step: 0.5, | ||
markers: 'number', | ||
}, | ||
}; | ||
|
||
export const Disabled: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
min: 1, | ||
max: 5, | ||
value: 3, | ||
step: 1, | ||
markers: 'number', | ||
disabled: true, | ||
}, | ||
}; | ||
|
||
export const MarkersSmallValues: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
min: 1, | ||
max: 5, | ||
value: 3, | ||
markers: ['1', '2', '3', '4', '5'], | ||
}, | ||
}; | ||
|
||
export const MarkersLargeValues: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
min: 0, | ||
max: 10000, | ||
value: 5000, | ||
step: 2500, | ||
markers: 'number', | ||
}, | ||
decorators: [ | ||
(Story) => ( | ||
<div className="w-80"> | ||
<Story /> | ||
</div> | ||
), | ||
], | ||
}; | ||
|
||
export const FieldNote: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
fieldNote: 'This is a fieldnote. It overrides the markers', | ||
markers: 'number', | ||
}, | ||
}; | ||
|
||
// For visual regression test | ||
export const Focus: StoryObj<Args> = { | ||
args: { | ||
label: 'Slider Label', | ||
}, | ||
parameters: { | ||
/** | ||
* No point snapping the button as this story is testing visual regression on the focus state (snap no difference than Default story). | ||
*/ | ||
snapshot: { skip: true }, | ||
}, | ||
play: () => { | ||
userEvent.tab(); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { generateSnapshots } from '@chanzuckerberg/story-utils'; | ||
import { render } from '@testing-library/react'; | ||
import React from 'react'; | ||
import * as stories from './Slider.stories'; | ||
import Slider from './'; | ||
|
||
describe('<Slider />', () => { | ||
generateSnapshots(stories); | ||
describe('error throws', () => { | ||
// expect console error from react, suppressed. | ||
const consoleErrorMock = jest.spyOn(console, 'error'); | ||
consoleErrorMock.mockImplementation(); | ||
it('throws an error if no label or aria-label', () => { | ||
expect(() => { | ||
render(<Slider max={5} min={0} step={1} value={2} />); | ||
}).toThrow(/You must provide a visible label or aria-label/); | ||
}); | ||
it('throws an error if told to generate markers, but steps are not integers', () => { | ||
expect(() => { | ||
render( | ||
<Slider | ||
label="Test" | ||
markers="number" | ||
max={5} | ||
min={0} | ||
step={2} | ||
value={2} | ||
/>, | ||
); | ||
}).toThrow( | ||
/Number of markers is not an integer. Change step or supply custom markers/, | ||
); | ||
}); | ||
consoleErrorMock.mockRestore(); | ||
}); | ||
}); |
Oops, something went wrong.