From 816e7a4c5d1fd31e2ec03ccfe3a395192a659790 Mon Sep 17 00:00:00 2001 From: Alexandre Esteves Date: Fri, 6 Dec 2024 16:08:24 +0100 Subject: [PATCH] feat(range): add ticks props Signed-off-by: Alexandre Esteves --- .../src/components/ods-range/ods-range.scss | 19 +++ .../src/components/ods-range/ods-range.tsx | 130 ++++++++++++------ .../range/src/controller/ods-range.ts | 15 ++ .../ods/src/components/range/src/index.html | 21 +++ .../range/tests/controller/ods-range.spec.ts | 30 +++- .../range/tests/rendering/ods-range.e2e.ts | 108 ++++++++++++++- .../range/tests/rendering/ods-range.spec.ts | 22 ++- packages/ods/src/style/_range.scss | 3 +- .../stories/components/range/range.stories.ts | 62 +++++++-- .../range/technical-information.mdx | 8 ++ 10 files changed, 364 insertions(+), 54 deletions(-) diff --git a/packages/ods/src/components/range/src/components/ods-range/ods-range.scss b/packages/ods/src/components/range/src/components/ods-range/ods-range.scss index 819bbe73b5..502f13d9b0 100644 --- a/packages/ods/src/components/range/src/components/ods-range/ods-range.scss +++ b/packages/ods/src/components/range/src/components/ods-range/ods-range.scss @@ -4,6 +4,7 @@ :host { display: inline-flex; position: relative; + padding-top: 5px; width: inherit; height: range.$ods-range-height; } @@ -44,4 +45,22 @@ width: 16px; height: 16px; } + + &__ticks { + position: absolute; + inset: 0 calc(range.$ods-input-range-size-thumb / 2); + z-index: range.$range-z-index-dual - 2; + + &__tick { + position: absolute; + border-radius: 6px; + background-color: range.$range-background-color; + width: 3px; + height: 18px; + + &--activated { + background-color: range.$range-background-color-active; + } + } + } } diff --git a/packages/ods/src/components/range/src/components/ods-range/ods-range.tsx b/packages/ods/src/components/range/src/components/ods-range/ods-range.tsx index b8ae326ee5..53f2b5ec0b 100644 --- a/packages/ods/src/components/range/src/components/ods-range/ods-range.tsx +++ b/packages/ods/src/components/range/src/components/ods-range/ods-range.tsx @@ -3,7 +3,7 @@ import { AttachInternals, Component, Element, Event, Host, Listen, Method, Prop, import { type OdsFormElement } from '../../../../../types'; import { getRandomHTMLId } from '../../../../../utils/dom'; import { type OdsTooltip } from '../../../../tooltip/src'; -import { VALUE_DEFAULT_VALUE, getInitialValue, isDualRange, toPercentage, updateInternals } from '../../controller/ods-range'; +import { VALUE_DEFAULT_VALUE, getInitialValue, getTicks, isDualRange, toPercentage, updateInternals } from '../../controller/ods-range'; import { type OdsRangeChangeEventDetail } from '../../interfaces/event'; @Component({ @@ -24,11 +24,14 @@ export class OdsRange implements OdsFormElement { private shouldUpdateIsInvalidState: boolean = false; private tooltip?: OdsTooltip; private tooltipDual?: OdsTooltip; + private listId = getRandomHTMLId(); @State() private dualValue?: number; @State() private currentValue?: number; @State() private isDualRange: boolean = false; @State() private isInvalid: boolean | undefined; + @State() private parsedTicks: number[] = []; + @State() private activatedTicks: number[] = []; @Element() el!: HTMLElement; @@ -42,6 +45,7 @@ export class OdsRange implements OdsFormElement { @Prop({ reflect: true }) public min: number = 0; @Prop({ reflect: true }) public name!: string; @Prop({ reflect: true }) public step?: number; + @Prop({ reflect: true }) public ticks?: string | number[]; @Prop({ mutable: true, reflect: true }) public value: number | [number, number] | null | [null, null] = VALUE_DEFAULT_VALUE; @Event() odsBlur!: EventEmitter; @@ -151,6 +155,12 @@ export class OdsRange implements OdsFormElement { } } + @Watch('ticks') + onTicksChange(): void { + this.parsedTicks = getTicks(this.ticks, this.min, this.max); + this.setActivatedTicks(); + } + @Watch('value') private onValueChange(value: number | [number, number] | [ null, null] | null = this.value): void { if (isDualRange(value)) { @@ -161,6 +171,8 @@ export class OdsRange implements OdsFormElement { this.fillInputs(value); } + this.setActivatedTicks(); + updateInternals(this.internals, value, this.isRequired); // In case the value gets updated from an other source than a blur event @@ -175,6 +187,7 @@ export class OdsRange implements OdsFormElement { this.hostId = this.el.id || getRandomHTMLId(); this.value = getInitialValue(this.value, this.min, this.max, this.defaultValue); + this.onTicksChange(); this.onMinOrMaxChange(); this.onValueChange(); this.emitOdsChange(); @@ -280,6 +293,17 @@ export class OdsRange implements OdsFormElement { } } + private setActivatedTicks(): void { + this.activatedTicks = this.parsedTicks.filter((tick) => { + if (isDualRange(this.value)) { + const [first, second] = this.value as [number, number]; + return this.value && tick >= first && tick <= second; + } else { + return this.value && tick <= (this.value as number); + } + }); + } + private showTooltip(): void { this.tooltip?.show(); } @@ -288,6 +312,67 @@ export class OdsRange implements OdsFormElement { this.tooltipDual?.show(); } + private renderTicks(): FunctionalComponent | undefined { + if (!this.parsedTicks || this.parsedTicks.length === 0) { + return; + } + const ratio = 100 / this.parsedTicks[this.parsedTicks.length - 1]; + + return (
+ + { + this.parsedTicks.map((tick) => ()) + } + + +
+ { + this.parsedTicks.map((tick) => ( +
-1, + }} + style={{ left: `calc(${tick * ratio}% - 2px)` }}> +
+ )) + } +
+
); + } + + private renderTooltip(percentage: number, value?: number, isDualRange: boolean = false): FunctionalComponent { + const shadowThumbId = isDualRange ? 'ods-range-shadow-thumb-dual': 'ods-range-shadow-thumb'; + return ( +
+
+
+ + { + !this.isDisabled && value !== undefined && + { + if (isDualRange) { + return this.tooltipDual = el as unknown as OdsTooltip; + } + return this.tooltip = el as unknown as OdsTooltip; + } } + shadowDomTriggerId={ shadowThumbId } + triggerId={ this.hostId } + withArrow> + { value } + + } +
+ ); + } + render(): FunctionalComponent { const percentage = toPercentage(this.max, this.min, this.currentValue); const percentageDual = toPercentage(this.max, this.min, this.dualValue); @@ -314,6 +399,7 @@ export class OdsRange implements OdsFormElement { aria-valuenow={ this.value } disabled={ this.isDisabled } id={ this.inputRangeId } + list={ this.listId } max={ this.max } min={ this.min } onBlur={ () => this.onBlur() } @@ -332,25 +418,9 @@ export class OdsRange implements OdsFormElement { value={ this.currentValue?.toString() } /> -
-
+ { this.renderTicks() } - { - !this.isDisabled && this.currentValue !== undefined && - this.tooltip = el as unknown as OdsTooltip } - shadowDomTriggerId="ods-range-shadow-thumb" - triggerId={ this.hostId } - withArrow> - { this.currentValue } - - } + { this.renderTooltip(percentage, this.currentValue) } { this.isDualRange && @@ -364,6 +434,7 @@ export class OdsRange implements OdsFormElement { aria-valuenow={ this.value } disabled={ this.isDisabled } id={ this.inputRangeDualId } + list={ this.listId } max={ this.max } min={ this.min } onChange={ () => this.emitOdsChange() } @@ -381,26 +452,7 @@ export class OdsRange implements OdsFormElement { /> } - { - !this.isDisabled && this.isDualRange && -
-
- } - { !this.isDisabled && this.isDualRange && this.dualValue && - this.tooltipDual = el as OdsTooltip } - shadowDomTriggerId="ods-range-shadow-thumb-dual" - triggerId={ this.hostId } - withArrow> - { this.dualValue } - - } + { this.isDualRange && this.renderTooltip(percentageDual, this.dualValue, true) } { this.min } { this.max } diff --git a/packages/ods/src/components/range/src/controller/ods-range.ts b/packages/ods/src/components/range/src/controller/ods-range.ts index 8ee4aad6e4..6bc42cbc00 100644 --- a/packages/ods/src/components/range/src/controller/ods-range.ts +++ b/packages/ods/src/components/range/src/controller/ods-range.ts @@ -10,6 +10,20 @@ function getInitialValue(value: RangeValue, min: number, max: number, defaultVal return value ?? (max < min ? min : min + (max - min) / 2); } +function getTicks(ticks: string | number[] | undefined, min: number, max: number): number[] { + if (!ticks) { + return []; + } + let parsedTicks: number[] = []; + try { + parsedTicks = Array.isArray(ticks) ? ticks : JSON.parse(ticks); + } catch (error) { + console.warn('[OdsRange] ticks string could not be parsed.'); + parsedTicks = []; + } + return parsedTicks.filter((tick) => tick >= min && tick <= max).sort((a, b) => a - b); +} + function isDualRange(value: RangeValue): value is [number, number] | [null, null] { return Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === 'number' || v === null); } @@ -35,6 +49,7 @@ function toPercentage(max: number, min: number, value?: number): number { export { getInitialValue, + getTicks, isDualRange, toPercentage, updateInternals, diff --git a/packages/ods/src/components/range/src/index.html b/packages/ods/src/components/range/src/index.html index 16ae411021..35377dcaa8 100644 --- a/packages/ods/src/components/range/src/index.html +++ b/packages/ods/src/components/range/src/index.html @@ -56,6 +56,27 @@ // }); +

Ticks

+
+ + + + + + + + +
+