Skip to content

Commit

Permalink
feat(slider): add section labels support
Browse files Browse the repository at this point in the history
- add sections defined a range with edges
- add section labels
- add section labels padding-bottom
- add section track colors
- make slider slider label be active
- add option so mark is only active when equal to slider value
  • Loading branch information
scharinger committed Sep 7, 2024
1 parent 6e299c6 commit 77f3f24
Show file tree
Hide file tree
Showing 22 changed files with 914 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`range-slider > should render 1`] = `
"baseElement": <body>
<div>
<span
class="axis-Slider ___16lm6va_1th6fmc fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1ufm4qn f1qnwcb4 fi52z01 fgrk5zm ffht0p2 f1p0ul1q f1c901ms f1alokd7 fmj8fco f1iwowo3 f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
class="axis-Slider ___1yr5lqm_13alxsp fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1k55ka9 fgclinu fycbxed f16pcs8n ffht0p2 f1p0ul1q f1c901ms f1alokd7 f78i1la f1kvsw7t f8k7e5g f1bw8brt f1pmxfrl fszkfcr f57dp0y f1ap5ily f11dm2qb f136rfnd fdsq1qd f1khssms fq4eyks f1ya6x16 f1e2iu44 ftuszwa f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
>
<span
class="axis-Slider__control ___144j0yg_julj5g0 f10pi13n f14t3ns0 f1l02sjl fcgxt0o f1ujusj6 fz0d3q9 f1k6fduh fdwl3z2"
Expand All @@ -16,7 +16,7 @@ exports[`range-slider > should render 1`] = `
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
style="width: 20%; left: 20%;"
/>
</span>
<div
Expand Down Expand Up @@ -61,7 +61,7 @@ exports[`range-slider > should render 1`] = `
</body>,
"container": <div>
<span
class="axis-Slider ___16lm6va_1th6fmc fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1ufm4qn f1qnwcb4 fi52z01 fgrk5zm ffht0p2 f1p0ul1q f1c901ms f1alokd7 fmj8fco f1iwowo3 f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
class="axis-Slider ___1yr5lqm_13alxsp fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1k55ka9 fgclinu fycbxed f16pcs8n ffht0p2 f1p0ul1q f1c901ms f1alokd7 f78i1la f1kvsw7t f8k7e5g f1bw8brt f1pmxfrl fszkfcr f57dp0y f1ap5ily f11dm2qb f136rfnd fdsq1qd f1khssms fq4eyks f1ya6x16 f1e2iu44 ftuszwa f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
>
<span
class="axis-Slider__control ___144j0yg_julj5g0 f10pi13n f14t3ns0 f1l02sjl fcgxt0o f1ujusj6 fz0d3q9 f1k6fduh fdwl3z2"
Expand All @@ -71,7 +71,7 @@ exports[`range-slider > should render 1`] = `
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
style="width: 20%; left: 20%;"
/>
</span>
<div
Expand Down
8 changes: 4 additions & 4 deletions components/slider/src/__snapshots__/slider.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`slider > should render 1`] = `
"baseElement": <body>
<div>
<span
class="axis-Slider ___16lm6va_1th6fmc fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1ufm4qn f1qnwcb4 fi52z01 fgrk5zm ffht0p2 f1p0ul1q f1c901ms f1alokd7 fmj8fco f1iwowo3 f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
class="axis-Slider ___1yr5lqm_13alxsp fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1k55ka9 fgclinu fycbxed f16pcs8n ffht0p2 f1p0ul1q f1c901ms f1alokd7 f78i1la f1kvsw7t f8k7e5g f1bw8brt f1pmxfrl fszkfcr f57dp0y f1ap5ily f11dm2qb f136rfnd fdsq1qd f1khssms fq4eyks f1ya6x16 f1e2iu44 ftuszwa f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
>
<span
class="axis-Slider__control ___144j0yg_julj5g0 f10pi13n f14t3ns0 f1l02sjl fcgxt0o f1ujusj6 fz0d3q9 f1k6fduh fdwl3z2"
Expand All @@ -16,7 +16,7 @@ exports[`slider > should render 1`] = `
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
style="width: 20%; left: 0%;"
/>
</span>
<div
Expand All @@ -43,7 +43,7 @@ exports[`slider > should render 1`] = `
</body>,
"container": <div>
<span
class="axis-Slider ___16lm6va_1th6fmc fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1ufm4qn f1qnwcb4 fi52z01 fgrk5zm ffht0p2 f1p0ul1q f1c901ms f1alokd7 fmj8fco f1iwowo3 f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
class="axis-Slider ___1yr5lqm_13alxsp fp2oml8 fifp7yv fwrwuds f1asdtw4 f4aqd28 f22iagw f10pi13n fly5x3f f3er7iy f1lx44mx fke05om ftqa4ok f2hkw1w f1b1k54r f4ne723 fh7aioi fqqcjud f1k55ka9 fgclinu fycbxed f16pcs8n ffht0p2 f1p0ul1q f1c901ms f1alokd7 f78i1la f1kvsw7t f8k7e5g f1bw8brt f1pmxfrl fszkfcr f57dp0y f1ap5ily f11dm2qb f136rfnd fdsq1qd f1khssms fq4eyks f1ya6x16 f1e2iu44 ftuszwa f1pffoy2 f1bmyog6 fs6b7xr f15fv2gd"
>
<span
class="axis-Slider__control ___144j0yg_julj5g0 f10pi13n f14t3ns0 f1l02sjl fcgxt0o f1ujusj6 fz0d3q9 f1k6fduh fdwl3z2"
Expand All @@ -53,7 +53,7 @@ exports[`slider > should render 1`] = `
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
style="width: 20%; left: 0%;"
/>
</span>
<div
Expand Down
2 changes: 2 additions & 0 deletions components/slider/src/mark/label/mark-label.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type MarkLabelSlots = {
export type MarkLabelProps = ComponentProps<MarkLabelSlots> & {
value: number;
label: ReactNode;
/** Label is ***active*** only when slider value is **equal** to mark value. */
activeEqual?: boolean;
};

export type MarkLabelState =
Expand Down
6 changes: 5 additions & 1 deletion components/slider/src/mark/label/use-mark-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const useMarkLabel_unstable = (
const minValue = values.length > 1 ? Math.min(...values) : 0;
const maxValue = Math.max(...values);

const active = props.activeEqual
? maxValue === props.value
: props.value >= minValue && props.value <= maxValue;

return {
root: getNativeElementProps("span", {
ref,
Expand All @@ -26,6 +30,6 @@ export const useMarkLabel_unstable = (
offset: toPercent(props.value, min, max),
value: props.value,
disabled,
active: props.value >= minValue && props.value <= maxValue,
active,
};
};
2 changes: 2 additions & 0 deletions components/slider/src/mark/mark.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
export type MarkDef = {
value: number;
label?: ReactNode;
/** Mark label is ***active*** only when slider value is **equal** to mark value. */
labelEqualActive?: boolean;
};

export type MarkSlots = {
Expand Down
5 changes: 3 additions & 2 deletions components/slider/src/range-slider.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { RangeSlider } from "./range-slider";
import React from "react";
import { getControlRoot } from "./test-helpers";
import { expect, vi } from "vitest";

const expectSliderValues = (elements: HTMLElement[], values: number[]) => {
expect(elements).toHaveLength(values.length);
Expand Down
27 changes: 18 additions & 9 deletions components/slider/src/render-slider.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
import React from "react";

import { getSlots } from "@fluentui/react-utilities";

import { SliderContextProvider } from "./context/slider-context";
import { SliderContextValues, SliderSlots, SliderState } from "./slider.types";
import React from "react";

export const renderSlider_unstable = (
state: SliderState,
contextValues: SliderContextValues
) => {
const { slots, slotProps } = getSlots<SliderSlots>(state);

const { marks, markLabels, thumbs } = state;
const { marks, markLabels, sectionLabels, thumbs } = state;

return (
<SliderContextProvider value={contextValues.slider}>
<slots.root {...slotProps.root}>
<slots.control {...slotProps.control}>
<slots.rail {...slotProps.rail}>
<slots.track {...slotProps.track} />
{marks.map((markProps) => (
<slots.mark
key={markProps.value.toString()}
{...slotProps.mark}
{...markProps}
/>
))}
</slots.rail>
{marks.map((markProps) => (
<slots.mark
key={markProps.value.toString()}
{...slotProps.mark}
{...markProps}
/>
))}
{markLabels.map((markLabelProps) => (
<slots.markLabel
key={markLabelProps.value.toString()}
{...slotProps.markLabel}
{...markLabelProps}
/>
))}

{sectionLabels.map((sectionLabelProps) => (
<slots.sectionLabel
key={`${sectionLabelProps.edges.from}-${sectionLabelProps.edges.to}`}
{...slotProps.sectionLabel}
{...sectionLabelProps}
/>
))}
{thumbs.map((thumbProps) => (
<slots.thumb
key={thumbProps["data-index"]}
Expand Down
10 changes: 10 additions & 0 deletions components/slider/src/section/render-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getSlots } from "@fluentui/react-utilities";

import React from "react";
import { SectionSlots, SectionState } from "./section.types";

export const renderSection_unstable = (state: SectionState) => {
const { slots, slotProps } = getSlots<SectionSlots>(state);

return <slots.root {...slotProps.root} />;
};
16 changes: 16 additions & 0 deletions components/slider/src/section/section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ForwardRefComponent } from "@fluentui/react-utilities";
import React from "react";

import { SectionProps } from "./section.types";
import { useSection_unstable } from "./use-section";
import { useSectionStyles_unstable } from "./use-section-styles";
import { renderSection_unstable } from "./render-section";

export const Section: ForwardRefComponent<SectionProps> = React
.forwardRef((props, ref) => {
const state = useSection_unstable(props, ref);

useSectionStyles_unstable(state);

return renderSection_unstable(state);
});
35 changes: 35 additions & 0 deletions components/slider/src/section/section.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
ComponentProps,
ComponentState,
Slot,
} from "@fluentui/react-utilities";
import { ReactNode } from "react";

export type SectionDef = {
edges: SectionEdges;
label: ReactNode;
trackColor?: string;
};

export type SectionSlots = {
root: Slot<"span">;
};

interface SectionEdges {
from?: number;
to?: number;
}

export type SectionProps = ComponentProps<SectionSlots> & {
edges: Required<SectionEdges>;
label: ReactNode;
trackColor?: string;
};

export type SectionState =
& ComponentState<SectionSlots>
& {
offset: number;
disabled: boolean;
active: boolean;
};
71 changes: 71 additions & 0 deletions components/slider/src/section/use-section-styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
makeStyles,
mergeClasses,
shorthands,
tokens,
useFluent,
} from "@fluentui/react-components";

import {
sliderClassNames,
sliderDurations,
sliderEasings,
sliderVars,
} from "../use-slider-styles";
import { SectionState } from "./section.types";

const useStyles = makeStyles({
root: {
...shorthands.transition(
"color",
sliderDurations.short,
sliderEasings.easeOutFast
),
position: "absolute",
color: `var(${sliderVars.section.color})`,
top: `var(${sliderVars.thumb.size})`,
transform: "translateX(-50%)",
whiteSpace: "nowrap",
},
active: {
[sliderVars.section.color]: tokens.colorNeutralForeground1,
},
enabled: {
[sliderVars.section.color]: tokens.colorNeutralForeground2,
},
disabled: {
[sliderVars.section.color]: tokens.colorNeutralForegroundDisabled,
},
});

export const useSectionStyles_unstable = (
state: SectionState
): SectionState => {
const styles = useStyles();

const { offset, disabled, active } = state;

const colorStyles = disabled
? styles.disabled
: active
? styles.active
: styles.enabled;

state.root.className = mergeClasses(
sliderClassNames.section.label,
styles.root,
colorStyles,
state.root.className
);

const { dir } = useFluent();
const offsetDirection = dir === "rtl" ? "right" : "left";
state.root.title = "";
state.root.style = {
cursor: "default",
[offsetDirection]: `${offset}%`,
...state.root.style,
};

return state;
};
51 changes: 51 additions & 0 deletions components/slider/src/section/use-section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getNativeElementProps } from "@fluentui/react-utilities";
import React from "react";

import { useSliderContext } from "../context/slider-context";
import { SectionProps, SectionState } from "./section.types";
import { toPercent } from "../utils";

export const useSection_unstable = (
props: SectionProps,
ref: React.Ref<HTMLElement>
): SectionState => {
const { min, max, disabled, values } = useSliderContext();

/**
* The left (end) section position.
*/
const left = props.edges.from ?? min;

/**
* The right (start) section position.
*/
const right = props.edges.to ?? max;

/**
* The sections center position, calculated as the midpoint between left and right.
*/
const center = left + (right - left) / 2;

const maxValue = Math.max(...values);

// Purpose is to only active labels when value is on the section label and not on the mark.
const isBeyondLeftOrLeftIsMin = maxValue > left
|| (maxValue === left && left === min);
const isBeforeRightOrRightIsMax = maxValue < right
|| (maxValue === right && right === max);
const active = isBeyondLeftOrLeftIsMin && isBeforeRightOrRightIsMax;

return {
root: getNativeElementProps("span", {
ref,
children: props.label,
...props,
}),
components: {
root: "span",
},
offset: toPercent(center, min, max),
disabled,
active,
};
};
4 changes: 4 additions & 0 deletions components/slider/src/slider.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SliderContextValue } from "./context/slider-context";
import { MarkDef, MarkProps } from "./mark/mark.types";
import { ThumbProps } from "./thumb/thumb.types";
import { MarkLabelProps } from "./mark/label/mark-label.types";
import { SectionDef, SectionProps } from "./section/section.types";

export type SliderContextValues = {
slider: SliderContextValue;
Expand All @@ -21,6 +22,7 @@ export type SliderSlots = {
thumb: Slot<Partial<ThumbProps>>;
mark: Slot<Partial<MarkProps>>;
markLabel: Slot<Partial<MarkLabelProps>>;
sectionLabel: Slot<Partial<SectionProps>>;
};

export type SliderOnChangeData = {
Expand All @@ -39,6 +41,7 @@ export type RangeSliderProps =
& {
disabled?: boolean;
marks?: boolean | MarkDef[];
sectionLabels?: SectionDef[];
step?: number | "marks";
size?: "small" | "medium";
min: number;
Expand Down Expand Up @@ -69,6 +72,7 @@ export type SliderState =
values: number[];
marks: MarkProps[];
markLabels: MarkLabelProps[];
sectionLabels: SectionProps[];
thumbs: ThumbProps[];
trackOffset: number;
trackWidth: number;
Expand Down
Loading

0 comments on commit 77f3f24

Please sign in to comment.