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 16, 2024
1 parent cecc04a commit 8092271
Show file tree
Hide file tree
Showing 24 changed files with 1,068 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -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
4 changes: 2 additions & 2 deletions components/slider/src/__snapshots__/slider.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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 Down Expand Up @@ -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
? values.some((value) => value === 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;
};
42 changes: 42 additions & 0 deletions components/slider/src/section/use-section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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();

const from = props.edges.from ?? min;
const to = props.edges.to ?? max;

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

const minValue = values.length > 1 ? Math.min(...values) : min;
const maxValue = Math.max(...values);

const active = (minValue > from && minValue < to)
|| (minValue <= from && maxValue > to)
|| (maxValue > from && maxValue <= to);

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 8092271

Please sign in to comment.