Skip to content

Commit

Permalink
KDS-1751-kaio-migration-search-slider (#4109)
Browse files Browse the repository at this point in the history
* 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
gyfchong authored Sep 29, 2023
1 parent 8f6cbc3 commit 4a3ef4e
Show file tree
Hide file tree
Showing 23 changed files with 1,173 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .changeset/empty-kiwis-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
186 changes: 186 additions & 0 deletions packages/components/src/InputRange/InputRange.module.scss
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;
}
39 changes: 39 additions & 0 deletions packages/components/src/InputRange/InputRange.spec.tsx
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()
})
})
103 changes: 103 additions & 0 deletions packages/components/src/InputRange/InputRange.tsx
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"
36 changes: 36 additions & 0 deletions packages/components/src/InputRange/_docs/InputRange.mdx
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} />
Loading

0 comments on commit 4a3ef4e

Please sign in to comment.