-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
KDS-1751-kaio-migration-search-slider (#4109)
* add SearchField * add Slider * add InputRange * add more stories for search * fix Sldier docs * fix errors * add changeset * fix stickersheet * fix pseudo states * fix colour contrast issues * apply a11y ignore to inputRange stories * add disabled inputRange to stickersheet * fix story
- Loading branch information
Showing
23 changed files
with
1,173 additions
and
1 deletion.
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,2 @@ | ||
--- | ||
--- |
186 changes: 186 additions & 0 deletions
186
packages/components/src/InputRange/InputRange.module.scss
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,186 @@ | ||
@import "~@kaizen/design-tokens/sass/spacing"; | ||
@import "~@kaizen/design-tokens/sass/color"; | ||
|
||
$thumb-color: $color-blue-500; | ||
$thumb-color-hover: $color-blue-600; | ||
$thumb-radius: 100%; | ||
$thumb-border-width: 4px; | ||
$thumb-border-color: $color-white; | ||
$thumb-height: 26px; | ||
$thumb-width: 26px; | ||
$thumb-height-with-border: $thumb-height + ($thumb-border-width * 2); | ||
$thumb-width-with-border: $thumb-width + ($thumb-border-width * 2); | ||
|
||
// The range | ||
$track-color: $color-gray-500; | ||
$track-radius: 4px; | ||
|
||
@mixin track { | ||
width: auto; | ||
height: 0; | ||
box-sizing: border-box; | ||
border-top: 1px solid $track-color; | ||
border-bottom: 2px solid $track-color; | ||
border-radius: $track-radius; | ||
} | ||
|
||
@mixin thumb { | ||
box-sizing: content-box; | ||
width: $thumb-width; | ||
height: $thumb-height; | ||
border: $thumb-border-width solid $thumb-border-color; | ||
border-radius: $thumb-radius; | ||
background: $thumb-color; | ||
|
||
&:not(:disabled) { | ||
transition: all 0.2s; | ||
transition-property: background, height, width; | ||
|
||
&:hover { | ||
background: $thumb-color-hover; | ||
width: $thumb-height + 6px; | ||
height: $thumb-width + 6px; | ||
} | ||
} | ||
} | ||
|
||
@mixin hidden-thumb { | ||
width: 0; | ||
height: 0; | ||
border: none; | ||
} | ||
|
||
// extra input[type="range"] is required to override materialize.css in performance-ui | ||
input[type="range"].ratingScaleRange { | ||
all: unset; | ||
appearance: none; | ||
width: 100%; | ||
margin: 0; // performance-ui materialize override | ||
|
||
&.disabled { | ||
opacity: 40%; | ||
} | ||
|
||
&::-moz-focus-outer { | ||
border: 0; | ||
} | ||
|
||
&:focus { | ||
outline: 0; | ||
} | ||
|
||
&:focus-visible { | ||
outline: 2px solid $color-blue-500; | ||
} | ||
|
||
&::-webkit-slider-runnable-track { | ||
@include track; | ||
|
||
margin: $spacing-sm 0; | ||
} | ||
|
||
&::-moz-range-track { | ||
@include track; | ||
} | ||
|
||
&::-webkit-slider-thumb { | ||
@include thumb; | ||
|
||
-webkit-appearance: none; | ||
margin-top: ($thumb-height-with-border/2) * -1; | ||
} | ||
|
||
&::-moz-range-thumb { | ||
@include thumb; | ||
} | ||
|
||
&.hideThumb::-webkit-slider-thumb { | ||
@include hidden-thumb; | ||
} | ||
|
||
&.hideThumb::-moz-range-thumb { | ||
@include hidden-thumb; | ||
} | ||
|
||
&::-ms-track { | ||
@include track; | ||
|
||
color: transparent; | ||
border-width: $thumb-width 0; | ||
border-color: transparent; | ||
background: transparent; | ||
} | ||
|
||
&::-ms-fill-lower { | ||
border: none; | ||
border-radius: $track-radius * 2; | ||
background: $track-color; | ||
} | ||
|
||
&::-ms-fill-upper { | ||
border: none; | ||
border-radius: $track-radius * 2; | ||
background: $track-color; | ||
} | ||
|
||
&::-ms-thumb { | ||
@include thumb; | ||
} | ||
} | ||
|
||
.spokes { | ||
display: flex; | ||
justify-content: space-between; | ||
padding: 0 $thumb-width-with-border/2 $spacing-sm; | ||
} | ||
|
||
.spokes.disabled { | ||
opacity: 40%; | ||
} | ||
|
||
.spokeContainer { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: 1px; | ||
} | ||
|
||
.spoke { | ||
width: 0; | ||
height: 0; | ||
background: $track-color; | ||
border: 2px solid $track-color; | ||
border-radius: 100%; | ||
} | ||
|
||
.labelsContainer { | ||
display: flex; | ||
justify-content: center; | ||
width: 100%; | ||
} | ||
|
||
.sliderLabels { | ||
width: 100%; | ||
display: flex; | ||
justify-content: space-between; | ||
} | ||
|
||
.sliderLabels.disabled { | ||
opacity: 40%; | ||
} | ||
|
||
// If the .visually-hidden class is applied to natively focusable elements | ||
// (such as a, button, input, etc) they must become visible when they receive | ||
// keyboard focus. Otherwise, a sighted keyboard user would have to try and | ||
// figure out where their visible focus indicator had gone to. | ||
.visuallyHidden:not(:focus, :active) { | ||
clip: rect(0, 0, 0, 0); | ||
clip-path: inset(50%); | ||
position: absolute; | ||
width: 1px; | ||
height: 1px; | ||
padding: 0; | ||
overflow: hidden; | ||
white-space: nowrap; | ||
border: 0; | ||
} |
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,39 @@ | ||
import React from "react" | ||
import { render, screen, fireEvent, waitFor } from "@testing-library/react" | ||
import { InputRange } from "./index" | ||
|
||
describe("<InputRange />", () => { | ||
it("fires onChange after interaction", async () => { | ||
const onChange = jest.fn() | ||
render( | ||
<InputRange | ||
id="unique-3" | ||
onChange={onChange} | ||
minLabel="Awful" | ||
maxLabel="Fantastic" | ||
/> | ||
) | ||
|
||
const slider = await screen.findByRole("slider") | ||
|
||
fireEvent.change(slider, { target: { value: 8 } }) | ||
|
||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1)) | ||
screen.getByDisplayValue("8") | ||
}) | ||
|
||
it("renders the screenreader help text", async () => { | ||
render( | ||
<InputRange | ||
id="unique-6" | ||
min={1} | ||
max={10} | ||
minLabel="bad" | ||
maxLabel="good" | ||
/> | ||
) | ||
const helpText = await screen.findByText(/1 is bad, 10 is good/i) | ||
|
||
expect(helpText).toBeInTheDocument() | ||
}) | ||
}) |
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,103 @@ | ||
import React, { InputHTMLAttributes, ReactNode, useState } from "react" | ||
import classnames from "classnames" | ||
import { Text } from "~components/Text" | ||
import { OverrideClassName } from "~types/OverrideClassName" | ||
import styles from "./InputRange.module.scss" | ||
|
||
export type InputRangeProps = { | ||
id: string | ||
defaultValue?: number | ||
value?: number | ||
minLabel: ReactNode | ||
maxLabel: ReactNode | ||
min?: number | ||
max?: number | ||
} & OverrideClassName<InputHTMLAttributes<HTMLInputElement>> | ||
|
||
/** | ||
* {@link https://cultureamp.design/?path=/docs/components-input-range--docs Storybook} | ||
*/ | ||
export const InputRange = ({ | ||
id, | ||
defaultValue, | ||
value, | ||
minLabel, | ||
maxLabel, | ||
min = 1, | ||
max = 10, | ||
onChange, | ||
"aria-describedby": ariaDescribedby, | ||
classNameOverride, | ||
disabled, | ||
readOnly, | ||
...restProps | ||
}: InputRangeProps): JSX.Element => { | ||
const [step, setStep] = useState(0.5) // Let the dot center between the notch initially | ||
const visuallyHiddenHintId = `${id}-helper` | ||
const readOnlyWithNoValue = readOnly && !value && !defaultValue | ||
|
||
// This has been split out into a different variable to allow usage of defaultValue above^ | ||
// Plus it lets us use max from props with its default value | ||
const defaultValueWithDefault = defaultValue || (max + 1) / 2 | ||
|
||
return ( | ||
<> | ||
<input | ||
id={id} | ||
className={classnames( | ||
styles.ratingScaleRange, | ||
classNameOverride, | ||
readOnlyWithNoValue && styles.hideThumb, | ||
disabled && styles.disabled | ||
)} | ||
disabled={disabled || readOnly} | ||
type="range" | ||
min={min} | ||
max={max} | ||
step={step} | ||
defaultValue={value ? undefined : defaultValueWithDefault} | ||
value={value} | ||
aria-valuenow={value} | ||
aria-valuemin={min} | ||
aria-valuemax={max} | ||
aria-describedby={`${visuallyHiddenHintId} ${ | ||
ariaDescribedby ? ariaDescribedby : "" | ||
}`} | ||
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => { | ||
setStep(1) // Put the stepper to 1 to avoid floating value | ||
onChange?.(e) | ||
}} | ||
{...restProps} | ||
/> | ||
<div className={classnames(styles.spokes, disabled && styles.disabled)}> | ||
{[...Array(max)].map((_, index) => ( | ||
<div key={`${id}-spoke-${index}`} className={styles.spokeContainer}> | ||
<div className={styles.spoke} /> | ||
</div> | ||
))} | ||
</div> | ||
<div className={styles.visuallyHidden} id={visuallyHiddenHintId}> | ||
{min} is {minLabel}, {max} is {maxLabel} | ||
</div> | ||
<div className={styles.labelsContainer}> | ||
{!readOnlyWithNoValue && ( | ||
<div | ||
className={classnames( | ||
styles.sliderLabels, | ||
disabled && styles.disabled | ||
)} | ||
> | ||
<Text variant="small" color="dark-reduced-opacity" tag="span"> | ||
{minLabel} | ||
</Text> | ||
<Text variant="small" color="dark-reduced-opacity" tag="span"> | ||
{maxLabel} | ||
</Text> | ||
</div> | ||
)} | ||
</div> | ||
</> | ||
) | ||
} | ||
|
||
InputRange.displayName = "InputRange" |
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 { Canvas, Controls, Meta } from "@storybook/blocks" | ||
import { ResourceLinks, KaioNotification, Installation } from "~storybook/components" | ||
import * as InputRangeStories from "./InputRange.stories" | ||
|
||
<Meta of={InputRangeStories} /> | ||
|
||
# InputRange | ||
|
||
<ResourceLinks | ||
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/InputRange" | ||
className="!mb-8" | ||
/> | ||
|
||
<KaioNotification /> | ||
|
||
<Installation | ||
installCommand="yarn add @kaizen/components" | ||
importStatement='import { InputRange } from "@kaizen/components"' | ||
/> | ||
|
||
## Overview | ||
|
||
A range of things. | ||
|
||
<Canvas of={InputRangeStories.Playground} /> | ||
<Controls of={InputRangeStories.Playground} /> | ||
|
||
## API | ||
|
||
## Labels | ||
|
||
<Canvas of={InputRangeStories.Labels} /> | ||
|
||
## Min/Max Range | ||
|
||
<Canvas of={InputRangeStories.Range} /> |
Oops, something went wrong.