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 12, 2024
1 parent fd5e179 commit b90ee5e
Show file tree
Hide file tree
Showing 25 changed files with 1,092 additions and 54 deletions.
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.workingDirectories": [
{"mode":"auto"}
]
],
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
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
? minValue === props.value || 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;
};
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 b90ee5e

Please sign in to comment.