Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(slider): add support for custom label formatting #9179

Merged
merged 3 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/calcite-components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4206,6 +4206,14 @@ export namespace Components {
* A set of single color stops for a histogram, sorted by offset ascending.
*/
"histogramStops": ColorStop[];
/**
* When specified, allows users to customize handle labels.
*/
"labelFormatter": (
value: number,
type: "value" | "min" | "max" | "tick",
defaultFormatter: (value: number) => string,
) => string | undefined;
/**
* When `true`, displays label handles with their numeric value.
*/
Expand Down Expand Up @@ -11774,6 +11782,14 @@ declare namespace LocalJSX {
* A set of single color stops for a histogram, sorted by offset ascending.
*/
"histogramStops"?: ColorStop[];
/**
* When specified, allows users to customize handle labels.
*/
"labelFormatter"?: (
value: number,
type: "value" | "min" | "max" | "tick",
defaultFormatter: (value: number) => string,
) => string | undefined;
/**
* When `true`, displays label handles with their numeric value.
*/
Expand Down
158 changes: 158 additions & 0 deletions packages/calcite-components/src/components/slider/slider.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ describe("calcite-slider", () => {
propertyName: "hasHistogram",
defaultValue: false,
},
{
propertyName: "labelFormatter",
defaultValue: undefined,
},
{
propertyName: "max",
defaultValue: 100,
Expand Down Expand Up @@ -964,4 +968,158 @@ describe("calcite-slider", () => {
expect(await slider.getProperty("value")).toBe(10);
});
});

describe("labelFormatter", () => {
const frGroupSeparator = " ";

describe("single value", () => {
it("allows formatting of the handle and ticks", async () => {
const page = await newE2EPage();
await page.setContent(
html` <calcite-slider min="0" max="100" value="50" label-handles label-ticks ticks="25"></calcite-slider>`,
);

await page.$eval(
"calcite-slider",
(slider: HTMLCalciteSliderElement) =>
(slider.labelFormatter = (value, type) => {
if (type === "value") {
return `${value}%`;
}

if (type === "tick") {
return "^";
}

return undefined;
}),
);
await page.waitForChanges();

const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`);
const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`);

expect(valueLabel.innerText).toBe("50%");

expect(tickLabels).toHaveLength(5);
tickLabels.forEach((tickLabel) => {
expect(tickLabel.innerText).toBe("^");
});
});

it("allows formatting with the default formatter", async () => {
const page = await newE2EPage();
await page.setContent(
html` <calcite-slider
min="0"
max="10000"
value="5000"
label-handles
lang="fr"
group-separator
></calcite-slider>`,
);

await page.$eval(
"calcite-slider",
(slider: HTMLCalciteSliderElement) =>
(slider.labelFormatter = (value, type, defaultFormatter) => {
if (type === "value") {
return defaultFormatter(value);
}

return undefined;
}),
);
await page.waitForChanges();

const valueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`);

expect(valueLabel.innerText).toBe(`5${frGroupSeparator}000`);
});
});

describe("min/max value", () => {
it("allows formatting of the min/max handle and ticks", async () => {
const page = await newE2EPage();
await page.setContent(
html` <calcite-slider
min="0"
max="100"
min-value="25"
max-value="75"
label-handles
label-ticks
ticks="25"
></calcite-slider>`,
);

await page.$eval(
"calcite-slider",
(slider: HTMLCalciteSliderElement) =>
(slider.labelFormatter = (value, type) => {
if (type === "min") {
return `-${value}%`;
}

if (type === "max") {
return `+${value}%`;
}

if (type === "tick") {
return "^";
}

return undefined;
}),
);
await page.waitForChanges();

const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`);
const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`);
const tickLabels = await page.findAll(`calcite-slider >>> .${CSS.tickLabel}`);

expect(minValueLabel.innerText).toBe("-25%");
expect(maxValueLabel.innerText).toBe("+75%");

expect(tickLabels).toHaveLength(5);
tickLabels.forEach((tickLabel) => {
expect(tickLabel.innerText).toBe("^");
});
});

it("allows formatting with the default formatter", async () => {
const page = await newE2EPage();
await page.setContent(
html` <calcite-slider
min="0"
max="10000"
min-value="2500"
max-value="7500"
label-handles
lang="fr"
group-separator
></calcite-slider>`,
);

await page.$eval(
"calcite-slider",
(slider: HTMLCalciteSliderElement) =>
(slider.labelFormatter = (value, type, defaultFormatter) =>
type === "min"
? // default formatting
undefined
: // using the default formatter
defaultFormatter(value)),
);
await page.waitForChanges();

const minValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelMinValue}`);
const maxValueLabel = await page.find(`calcite-slider >>> .${CSS.handleLabelValue}`);

expect(minValueLabel.innerText).toBe(`2${frGroupSeparator}500`);
expect(maxValueLabel.innerText).toBe(`7${frGroupSeparator}500`);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -462,3 +462,63 @@ export const fillPlacements = (): string => html`
<calcite-slider min="0" max="100" value="50" fill-placement="end"></calcite-slider>
<calcite-slider min="0" max="100" value="100" fill-placement="end"></calcite-slider>
`;

export const customLabelsAndTicks = (): string => html`
<label>Label formatter (single value)</label>
<calcite-slider
id="singleFormattedLabelSlider"
label-handles
label-ticks
ticks="100"
min="0"
max="100"
value="50"
step="1"
min-label="Temperature"
></calcite-slider>

<label>Label formatter (min/max value)</label>
<calcite-slider
id="minMaxFormattedLabelSlider"
label-handles
label-ticks
ticks="10"
min="0"
max="100"
min-value="25"
max-value="75"
step="1"
min-label="Temperature"
></calcite-slider>

<script>
const singleValueSlider = document.getElementById("singleFormattedLabelSlider");

singleValueSlider.labelFormatter = function (value, type) {
if (type === "value") {
return value < 60 ? "🥶" : value > 80 ? "🥵" : "😎";
jcfranco marked this conversation as resolved.
Show resolved Hide resolved
}

if (type === "tick") {
return value === singleValueSlider.min ? "Cold" : value === singleValueSlider.max ? "Hot" : undefined;
}
};

const minMaxValueSlider = document.getElementById("minMaxFormattedLabelSlider");

minMaxValueSlider.labelFormatter = function (value, type) {
if (type === "min" || type === "max") {
const status = value < 60 ? "🥶" : value > 80 ? "🥵" : "😎";
return type === "min" ? value + "ºF" + " " + status : status + " " + value + "ºF";
}

if (type === "tick") {
return value === minMaxValueSlider.max ? value + "ºF" : value + "º";
}
};
</script>
`;

customLabelsAndTicks.parameters = {
chromatic: { delay: 500 },
};
34 changes: 32 additions & 2 deletions packages/calcite-components/src/components/slider/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ export class Slider
/** When `true`, displays label handles with their numeric value. */
@Prop({ reflect: true }) labelHandles = false;

/**
* When specified, allows users to customize handle labels.
*/
@Prop() labelFormatter: (
value: number,
type: "value" | "min" | "max" | "tick",
defaultFormatter: (value: number) => string,
) => string | undefined;

/** When `true` and `ticks` is specified, displays label tick marks with their numeric value. */
@Prop({ reflect: true }) labelTicks = false;

Expand Down Expand Up @@ -389,7 +398,12 @@ export class Slider
const valueProp = isMinThumb ? "minValue" : valueIsRange ? "maxValue" : "value";
const ariaLabel = isMinThumb ? this.minLabel : valueIsRange ? this.maxLabel : this.minLabel;
const ariaValuenow = isMinThumb ? this.minValue : value;
const displayedValue = isMinThumb ? this.formatValue(this.minValue) : this.formatValue(value);
const displayedValue =
valueProp === "minValue"
? this.internalLabelFormatter(this.minValue, "min")
: valueProp === "maxValue"
? this.internalLabelFormatter(this.maxValue, "max")
: this.internalLabelFormatter(value, "value");
const thumbStyle: SideOffset = isMinThumb
? { left: `${mirror ? 100 - minInterval : minInterval}%` }
: { right: `${mirror ? maxInterval : 100 - maxInterval}%` };
Expand Down Expand Up @@ -486,7 +500,7 @@ export class Slider
[CSS.tickMax]: isMaxTickLabel,
}}
>
{this.formatValue(tick)}
{this.internalLabelFormatter(tick, "tick")}
</span>
) : null;
}
Expand Down Expand Up @@ -1247,4 +1261,20 @@ export class Slider

return numberStringFormatter.localize(value.toString());
};

private internalLabelFormatter(value: number, type: "max" | "min" | "value" | "tick"): string {
const customFormatter = this.labelFormatter;

if (!customFormatter) {
return this.formatValue(value);
}

const formattedValue = customFormatter(value, type, this.formatValue);

if (formattedValue == null) {
return this.formatValue(value);
}

return formattedValue;
}
}
Loading