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 13, 2024
1 parent fd5e179 commit 04ac7ef
Show file tree
Hide file tree
Showing 25 changed files with 1,087 additions and 54 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;
};
43 changes: 43 additions & 0 deletions components/slider/src/section/use-section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 04ac7ef

Please sign in to comment.