Skip to content

Commit

Permalink
feat(range): add ticks props
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Esteves <[email protected]>
  • Loading branch information
aesteves60 authored and dpellier committed Dec 11, 2024
1 parent cbc67df commit 816e7a4
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:host {
display: inline-flex;
position: relative;
padding-top: 5px;
width: inherit;
height: range.$ods-range-height;
}
Expand Down Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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;

Expand All @@ -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<void>;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
Expand All @@ -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 (<div>
<datalist id={ this.listId }>
{
this.parsedTicks.map((tick) => (<option value={ tick }></option>))
}
</datalist>

<div class="ods-range__ticks">
{
this.parsedTicks.map((tick) => (
<div
class={{
'ods-range__ticks__tick': true,
'ods-range__ticks__tick--activated': this.activatedTicks.indexOf(tick) > -1,
}}
style={{ left: `calc(${tick * ratio}% - 2px)` }}>
</div>
))
}
</div>
</div>);
}

private renderTooltip(percentage: number, value?: number, isDualRange: boolean = false): FunctionalComponent {
const shadowThumbId = isDualRange ? 'ods-range-shadow-thumb-dual': 'ods-range-shadow-thumb';
return (
<div>
<div
class="ods-range__shadow-thumb"
id={ shadowThumbId }
style={{
left: `calc(${percentage}% - (${percentage * 0.15}px))`,
}}>
</div>

{
!this.isDisabled && value !== undefined &&
<ods-tooltip
position="top"
ref={ (el: unknown) => {
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 }
</ods-tooltip>
}
</div>
);
}

render(): FunctionalComponent {
const percentage = toPercentage(this.max, this.min, this.currentValue);
const percentageDual = toPercentage(this.max, this.min, this.dualValue);
Expand All @@ -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() }
Expand All @@ -332,25 +418,9 @@ export class OdsRange implements OdsFormElement {
value={ this.currentValue?.toString() }
/>

<div
class="ods-range__shadow-thumb"
id="ods-range-shadow-thumb"
style={{
left: `calc(${percentage}% - (${percentage * 0.15}px))`,
}}>
</div>
{ this.renderTicks() }

{
!this.isDisabled && this.currentValue !== undefined &&
<ods-tooltip
position="top"
ref={ (el: unknown) => this.tooltip = el as unknown as OdsTooltip }
shadowDomTriggerId="ods-range-shadow-thumb"
triggerId={ this.hostId }
withArrow>
{ this.currentValue }
</ods-tooltip>
}
{ this.renderTooltip(percentage, this.currentValue) }

{
this.isDualRange &&
Expand All @@ -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() }
Expand All @@ -381,26 +452,7 @@ export class OdsRange implements OdsFormElement {
/>
}

{
!this.isDisabled && this.isDualRange &&
<div
class="ods-range__shadow-thumb"
id="ods-range-shadow-thumb-dual"
style={{
left: `calc(${percentageDual}% - (${percentageDual * 0.15}px))`,
}}>
</div>
}
{ !this.isDisabled && this.isDualRange && this.dualValue &&
<ods-tooltip
position="top"
ref={ (el: unknown) => this.tooltipDual = el as OdsTooltip }
shadowDomTriggerId="ods-range-shadow-thumb-dual"
triggerId={ this.hostId }
withArrow>
{ this.dualValue }
</ods-tooltip>
}
{ this.isDualRange && this.renderTooltip(percentageDual, this.dualValue, true) }

<span class="ods-range__min">{ this.min }</span>
<span class="ods-range__max">{ this.max }</span>
Expand Down
15 changes: 15 additions & 0 deletions packages/ods/src/components/range/src/controller/ods-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -35,6 +49,7 @@ function toPercentage(max: number, min: number, value?: number): number {

export {
getInitialValue,
getTicks,
isDualRange,
toPercentage,
updateInternals,
Expand Down
21 changes: 21 additions & 0 deletions packages/ods/src/components/range/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@
// });
</script>

<p>Ticks</p>
<div>
<ods-range id="ticks-range" ticks="[0,-10,10,25,50,75,100,120]">
</ods-range>
<ods-range id="ticks-range-dual" ticks="[0,25,50,75,100]">
</ods-range>

<button id="button-change-ticks">change ticks</button>

<script>
const ticksRange = document.getElementById('ticks-range');
const ticksRangeDual = document.getElementById('ticks-range-dual');
const buttonChangeTicks = document.getElementById('button-change-ticks');

ticksRangeDual.value = [15, 60];
buttonChangeTicks.addEventListener('click', () => {
ticksRange.ticks = '[5,6,7]'
})
</script>
</div>

<!-- <p>Disabled</p>
<ods-range value="40" is-disabled>
</ods-range>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { getInitialValue, isDualRange, toPercentage } from '../../src/controller/ods-range';
import { getInitialValue, getTicks, isDualRange, toPercentage } from '../../src/controller/ods-range';

describe('ods-range controller', () => {

beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});

describe('getInitialValue', () => {
it('returns the provided defaultValue if specified', () => {
expect(getInitialValue(null, 0, 10, 5)).toBe(5);
Expand All @@ -19,6 +23,30 @@ describe('ods-range controller', () => {
});
});

describe('getTicks', () => {
it('returns ticks parsed', () => {
expect(getTicks([25, 50, 75], 0 , 100)).toEqual([25, 50, 75]);
expect(getTicks([0, 100], 0 , 100)).toEqual([0, 100]);
expect(getTicks(JSON.stringify([25, 50, 75]), 0 , 100)).toEqual([25, 50, 75]);
expect(getTicks('[25, 50, 75]', 0 , 100)).toEqual([25, 50, 75]);
});

it('should return an empty array and warn if the string cannot be parsed', () => {
expect(getTicks(undefined, 0 , 100)).toEqual([]);
expect(getTicks('', 0 , 100)).toEqual([]);
expect(getTicks('dummy', 0 , 100)).toEqual([]);
expect(console.warn).toHaveBeenCalledTimes(1);
});

it('returns remove ticks out of the range', () => {
expect(getTicks([-10, 25, 50, 75, 110], 0 , 100)).toEqual([25, 50, 75]);
});

it('returns sort ticks', () => {
expect(getTicks([100, 25, 50, 15, 75], 0 , 100)).toEqual([15, 25, 50, 75, 100]);
});
});

describe('isDualRange', () => {
it('should return true for valid dual range', () => {
expect(isDualRange([1, 2])).toBe(true);
Expand Down
Loading

0 comments on commit 816e7a4

Please sign in to comment.