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 Aug 14, 2024
1 parent daf3641 commit 2271f65
Show file tree
Hide file tree
Showing 20 changed files with 642 additions and 53 deletions.
22 changes: 10 additions & 12 deletions components/slider/src/__snapshots__/range-slider.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ exports[`range-slider > should render 1`] = `
>
<span
class="axis-Slider__rail ___kotjr40_1djcq1g fnivh3a fc7yr5o f1el4m67 f8yange f1p9o1ba f1sil6mw ftgm304 f1euv43f fly5x3f f1erzq44 f1i1t8d1 f188r07x f1d3ncnn"
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
/>
</span>
/>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
/>
<div
class="axis-Slider__thumb ___14hb9vm_jmfl5w0 f8fbkgy f1nfllo7 f1djnp8u f1s8kh49 f22iagw f4d9j23 f122n59 f1euv43f f1t2h5zl f4aqd28 f1i1t8d1 f1vcxb5z fk1lali f1ewtqcl frcgkvu feh19e8 f3j3guj fo75joz f1yag6sl fsc6avr f1w15cjc fkaq55s f1fp5o0y f163fonl f1yq6w5o f11yjt3y f1jpmc5p f10tv6oz f16xp3sf fwrmqbx f1seuxxq f1j7ml58 f14u7mkt f5zrw40 fto0uou f1ks5ppg fyl8oag f1wl9k8s f1mdlcz9 fmmikit f10awi5f f1mfomsq fftmtmu f13zj6fq fgougqc fprarqb f14vs0nd f1gtfqs9 f18zvfd9 f8sy9bk fufffdp f1sclg23 f132a78j fcetbih f1kt6s4o"
style="left: 20%;"
Expand Down Expand Up @@ -68,12 +67,11 @@ exports[`range-slider > should render 1`] = `
>
<span
class="axis-Slider__rail ___kotjr40_1djcq1g fnivh3a fc7yr5o f1el4m67 f8yange f1p9o1ba f1sil6mw ftgm304 f1euv43f fly5x3f f1erzq44 f1i1t8d1 f188r07x f1d3ncnn"
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
/>
</span>
/>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 20%; width: 20%;"
/>
<div
class="axis-Slider__thumb ___14hb9vm_jmfl5w0 f8fbkgy f1nfllo7 f1djnp8u f1s8kh49 f22iagw f4d9j23 f122n59 f1euv43f f1t2h5zl f4aqd28 f1i1t8d1 f1vcxb5z fk1lali f1ewtqcl frcgkvu feh19e8 f3j3guj fo75joz f1yag6sl fsc6avr f1w15cjc fkaq55s f1fp5o0y f163fonl f1yq6w5o f11yjt3y f1jpmc5p f10tv6oz f16xp3sf fwrmqbx f1seuxxq f1j7ml58 f14u7mkt f5zrw40 fto0uou f1ks5ppg fyl8oag f1wl9k8s f1mdlcz9 fmmikit f10awi5f f1mfomsq fftmtmu f13zj6fq fgougqc fprarqb f14vs0nd f1gtfqs9 f18zvfd9 f8sy9bk fufffdp f1sclg23 f132a78j fcetbih f1kt6s4o"
style="left: 20%;"
Expand Down
22 changes: 10 additions & 12 deletions components/slider/src/__snapshots__/slider.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ exports[`slider > should render 1`] = `
>
<span
class="axis-Slider__rail ___kotjr40_1djcq1g fnivh3a fc7yr5o f1el4m67 f8yange f1p9o1ba f1sil6mw ftgm304 f1euv43f fly5x3f f1erzq44 f1i1t8d1 f188r07x f1d3ncnn"
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
/>
</span>
/>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
/>
<div
class="axis-Slider__thumb ___14hb9vm_jmfl5w0 f8fbkgy f1nfllo7 f1djnp8u f1s8kh49 f22iagw f4d9j23 f122n59 f1euv43f f1t2h5zl f4aqd28 f1i1t8d1 f1vcxb5z fk1lali f1ewtqcl frcgkvu feh19e8 f3j3guj fo75joz f1yag6sl fsc6avr f1w15cjc fkaq55s f1fp5o0y f163fonl f1yq6w5o f11yjt3y f1jpmc5p f10tv6oz f16xp3sf fwrmqbx f1seuxxq f1j7ml58 f14u7mkt f5zrw40 fto0uou f1ks5ppg fyl8oag f1wl9k8s f1mdlcz9 fmmikit f10awi5f f1mfomsq fftmtmu f13zj6fq fgougqc fprarqb f14vs0nd f1gtfqs9 f18zvfd9 f8sy9bk fufffdp f1sclg23 f132a78j fcetbih f1kt6s4o"
style="left: 20%;"
Expand Down Expand Up @@ -50,12 +49,11 @@ exports[`slider > should render 1`] = `
>
<span
class="axis-Slider__rail ___kotjr40_1djcq1g fnivh3a fc7yr5o f1el4m67 f8yange f1p9o1ba f1sil6mw ftgm304 f1euv43f fly5x3f f1erzq44 f1i1t8d1 f188r07x f1d3ncnn"
>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
/>
</span>
/>
<span
class="axis-Slider__track ___pq61zr0_1xft6bt fnivh3a fc7yr5o f1el4m67 f8yange ftgm304 f1euv43f f1erzq44 f1i1t8d1 f188r07x f1yrba31 f174n9yu"
style="left: 0%; width: 20%;"
/>
<div
class="axis-Slider__thumb ___14hb9vm_jmfl5w0 f8fbkgy f1nfllo7 f1djnp8u f1s8kh49 f22iagw f4d9j23 f122n59 f1euv43f f1t2h5zl f4aqd28 f1i1t8d1 f1vcxb5z fk1lali f1ewtqcl frcgkvu feh19e8 f3j3guj fo75joz f1yag6sl fsc6avr f1w15cjc fkaq55s f1fp5o0y f163fonl f1yq6w5o f11yjt3y f1jpmc5p f10tv6oz f16xp3sf fwrmqbx f1seuxxq f1j7ml58 f14u7mkt f5zrw40 fto0uou f1ks5ppg fyl8oag f1wl9k8s f1mdlcz9 fmmikit f10awi5f f1mfomsq fftmtmu f13zj6fq fgougqc fprarqb f14vs0nd f1gtfqs9 f18zvfd9 f8sy9bk fufffdp f1sclg23 f132a78j fcetbih f1kt6s4o"
style="left: 20%;"
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 === 0 && 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
37 changes: 24 additions & 13 deletions components/slider/src/render-slider.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
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";
import {
SliderContextValues,
SliderSlots,
SliderState,
} from "./slider.types";

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

import React from "react";
import {
SectionLabelSlots,
SectionLabelState,
} from "./section-label.types";

export const renderSectionLabel_unstable = (state: SectionLabelState) => {
const { slots, slotProps } = getSlots<SectionLabelSlots>(state);

return <slots.root {...slotProps.root} />;
};
16 changes: 16 additions & 0 deletions components/slider/src/section/label/section-label.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 { SectionLabelProps } from "./section-label.types";
import { useSectionLabel_unstable } from "./use-section-label";
import { useSectionLabelStyles_unstable } from "./use-section-label-styles";
import { renderSectionLabel_unstable } from "./render-section-label";

export const SectionLabel: ForwardRefComponent<SectionLabelProps> = React
.forwardRef((props, ref) => {
const state = useSectionLabel_unstable(props, ref);

useSectionLabelStyles_unstable(state);

return renderSectionLabel_unstable(state);
});
35 changes: 35 additions & 0 deletions components/slider/src/section/label/section-label.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 SectionLabelDef = {
edges: SectionEdges;
label: ReactNode;
trackColor?: string;
};

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

interface SectionEdges {
left?: number;
right?: number;
}

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

export type SectionLabelState =
& ComponentState<SectionLabelSlots>
& {
offset: number;
disabled: boolean;
active: boolean;
};
71 changes: 71 additions & 0 deletions components/slider/src/section/label/use-section-label-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 { SectionLabelState } from "./section-label.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 useSectionLabelStyles_unstable = (
state: SectionLabelState
): SectionLabelState => {
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;
};
55 changes: 55 additions & 0 deletions components/slider/src/section/label/use-section-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getNativeElementProps } from "@fluentui/react-utilities";
import React from "react";

import { useSliderContext } from "../../context/slider-context";
import {
SectionLabelProps,
SectionLabelState,
} from "./section-label.types";
import { toPercent } from "../../utils";

export const useSectionLabel_unstable = (
props: SectionLabelProps,
ref: React.Ref<HTMLElement>
): SectionLabelState => {
const { min, max, disabled, values } = useSliderContext();

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

/**
* The right (start) section position.
*/
const right = props.edges.right ?? 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,
};
};
Loading

0 comments on commit 2271f65

Please sign in to comment.