Skip to content

Commit

Permalink
feat(slider): add support for custom label formatting (#9179)
Browse files Browse the repository at this point in the history
**Related Issue:** #4004

## Summary

Adds `labelFormatter` prop to allow customization of handle and tick
labels.

```ts
interface CalciteSlider {

  /** 
    * Formatter for custom handle/ticks labels.
    * 
    * If the returned value is a string, it will get used in the label, 
    * and if it is `undefined`, it will use the default, formatted label.
    */
  labelFormatter(

    // the associated label value
    value: number, 

    // the label type
    type: "min" | "max" | "value" | "tick" 

    // the default formatter (configured with corresponding locale, numbering system and group separator
    defaultFormatter: (value: number): string;

  ): string | undefined;
}
```
  • Loading branch information
jcfranco authored Apr 24, 2024
1 parent b187340 commit 710d1ee
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 2 deletions.
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 ? "🥵" : "😎";
}
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;
}
}

0 comments on commit 710d1ee

Please sign in to comment.