From 15acb4be37eef4d1c90229cf64fb836e249c225c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bengt=20Wei=C3=9Fe?= Date: Tue, 12 Feb 2019 23:17:23 +0100 Subject: [PATCH] feat(range): add neutral point (#17400) * feat(Range): add neutral point * feat(Range): generate proxies and api * fix(): check positive case in neutralPointChanged * fix(Range): neutralPoint to min if neutralPoint < min * fix(Range): active bar style * fix(Range): tick styling --- angular/src/directives/proxies.ts | 4 +- core/api.txt | 3 +- core/src/components.d.ts | 12 ++- core/src/components/range/range.tsx | 99 +++++++++++++++---- core/src/components/range/readme.md | 45 ++++++--- .../components/range/test/basic/index.html | 6 ++ core/src/components/range/usage/angular.md | 8 ++ core/src/components/range/usage/javascript.md | 8 ++ 8 files changed, 149 insertions(+), 36 deletions(-) diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 3c76469d93..1b01f51f2d 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -586,7 +586,7 @@ export class IonRadioGroup { proxyInputs(IonRadioGroup, ['allowEmptySelection', 'name', 'value']); export declare interface IonRange extends StencilComponents<'IonRange'> {} -@Component({ selector: 'ion-range', changeDetection: 0, template: '', inputs: ['color', 'mode', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value'] }) +@Component({ selector: 'ion-range', changeDetection: 0, template: '', inputs: ['color', 'mode', 'neutralPoint', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value'] }) export class IonRange { ionChange!: EventEmitter; ionFocus!: EventEmitter; @@ -598,7 +598,7 @@ export class IonRange { proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); } } -proxyInputs(IonRange, ['color', 'mode', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value']); +proxyInputs(IonRange, ['color', 'mode', 'neutralPoint', 'debounce', 'name', 'dualKnobs', 'min', 'max', 'pin', 'snaps', 'step', 'disabled', 'value']); export declare interface IonRefresher extends StencilComponents<'IonRefresher'> {} @Component({ selector: 'ion-refresher', changeDetection: 0, template: '', inputs: ['pullMin', 'pullMax', 'closeDuration', 'snapbackDuration', 'disabled'] }) diff --git a/core/api.txt b/core/api.txt index 679d727fb6..dba34552d3 100644 --- a/core/api.txt +++ b/core/api.txt @@ -800,10 +800,11 @@ ion-range,prop,max,number,100,false,false ion-range,prop,min,number,0,false,false ion-range,prop,mode,"ios" | "md",undefined,false,false ion-range,prop,name,string,'',false,false +ion-range,prop,neutralPoint,number,0,false,false ion-range,prop,pin,boolean,false,false,false ion-range,prop,snaps,boolean,false,false,false ion-range,prop,step,number,1,false,false -ion-range,prop,value,number | { lower: number; upper: number; },0,false,false +ion-range,prop,value,null | number | { lower: number; upper: number; },null,false,false ion-range,event,ionBlur,void,true ion-range,event,ionChange,RangeChangeEventDetail,true ion-range,event,ionFocus,void,true diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 9119916d90..baca746f3b 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3309,6 +3309,10 @@ export namespace Components { */ 'name': string; /** + * The neutral point of the range slider. Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`. + */ + 'neutralPoint': number; + /** * If `true`, a pin with integer value is shown when the knob is pressed. */ 'pin': boolean; @@ -3323,7 +3327,7 @@ export namespace Components { /** * the value of the range. */ - 'value': RangeValue; + 'value': RangeValue | null; } interface IonRangeAttributes extends StencilHTMLAttributes { /** @@ -3359,6 +3363,10 @@ export namespace Components { */ 'name'?: string; /** + * The neutral point of the range slider. Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`. + */ + 'neutralPoint'?: number; + /** * Emitted when the range loses focus. */ 'onIonBlur'?: (event: CustomEvent) => void; @@ -3385,7 +3393,7 @@ export namespace Components { /** * the value of the range. */ - 'value'?: RangeValue; + 'value'?: RangeValue | null; } interface IonRefresherContent { diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index d84a8e095a..56c4c3841a 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -44,6 +44,25 @@ export class Range implements ComponentInterface { */ @Prop() mode!: Mode; + /** + * The neutral point of the range slider. + * Default: value is `0` or the `min` when `neutralPoint < min` or `max` when `max < neutralPoint`. + */ + @Prop() neutralPoint = 0; + protected neutralPointChanged() { + if (this.noUpdate) { + return; + } + const { min, max, neutralPoint } = this; + + if (max < neutralPoint) { + this.neutralPoint = max; + } + if (neutralPoint < min) { + this.neutralPoint = min; + } + } + /** * How long, in milliseconds, to wait to trigger the * `ionChange` event after each change in the range value. @@ -119,7 +138,7 @@ export class Range implements ComponentInterface { /** * the value of the range. */ - @Prop({ mutable: true }) value: RangeValue = 0; + @Prop({ mutable: true }) value: RangeValue | null = null; @Watch('value') protected valueChanged(value: RangeValue) { if (!this.noUpdate) { @@ -210,14 +229,14 @@ export class Range implements ComponentInterface { } private getValue(): RangeValue { - const value = this.value || 0; + const value = this.value || this.neutralPoint || 0; if (this.dualKnobs) { if (typeof value === 'object') { return value; } return { - lower: 0, - upper: value + lower: this.value === null ? this.neutralPoint : 0, + upper: this.value === null ? this.neutralPoint : value }; } else { if (typeof value === 'object') { @@ -361,25 +380,67 @@ export class Range implements ComponentInterface { } }; } + protected getActiveBarPosition() { + const { min, max, neutralPoint, ratioLower, ratioUpper } = this; + const neutralPointRatio = valueToRatio(neutralPoint, min, max); + + // dual knob handling + let left = `${ratioLower * 100}%`; + let right = `${100 - ratioUpper * 100}%`; + + // single knob handling + if (!this.dualKnobs) { + if (this.ratioA < neutralPointRatio) { + right = `${neutralPointRatio * 100}%`; + left = `${this.ratioA * 100}%`; + } else { + right = `${100 - this.ratioA * 100}%`; + left = `${neutralPointRatio * 100}%`; + } + } + + return { + left, + right + }; + } + + protected isTickActive(stepRatio: number) { + const { min, max, neutralPoint, ratioLower, ratioUpper } = this; + const neutralPointRatio = valueToRatio(neutralPoint, min, max); + + if (this.dualKnobs) { + return (stepRatio >= ratioLower && stepRatio <= ratioUpper); + } + + if (this.ratioA <= neutralPointRatio && stepRatio >= this.ratioA && stepRatio <= neutralPointRatio) { + return true; + } + + if (this.ratioA >= neutralPointRatio && stepRatio <= this.ratioA && stepRatio >= neutralPointRatio) { + return true; + } + + return false; + } render() { - const { min, max, step, ratioLower, ratioUpper } = this; - - const barStart = `${ratioLower * 100}%`; - const barEnd = `${100 - ratioUpper * 100}%`; + const { min, max, neutralPoint, step } = this; + const barPosition = this.getActiveBarPosition(); const isRTL = document.dir === 'rtl'; const start = isRTL ? 'right' : 'left'; const end = isRTL ? 'left' : 'right'; - const ticks = []; + const ticks: any[] = []; + if (this.snaps) { for (let value = min; value <= max; value += step) { const ratio = valueToRatio(value, min, max); const tick: any = { ratio, - active: ratio >= ratioLower && ratio <= ratioUpper, + active: this.isTickActive(ratio), }; tick[start] = `${ratio * 100}%`; @@ -390,7 +451,6 @@ export class Range implements ComponentInterface { const tickStyle = (tick: any) => { const style: any = {}; - style[start] = tick[start]; return style; @@ -399,8 +459,8 @@ export class Range implements ComponentInterface { const barStyle = () => { const style: any = {}; - style[start] = barStart; - style[end] = barEnd; + style[start] = barPosition[start]; + style[end] = barPosition[end]; return style; }; @@ -435,7 +495,8 @@ export class Range implements ComponentInterface { disabled: this.disabled, handleKeyboard: this.handleKeyboard, min, - max + max, + neutralPoint })} { this.dualKnobs && renderKnob({ @@ -447,7 +508,8 @@ export class Range implements ComponentInterface { disabled: this.disabled, handleKeyboard: this.handleKeyboard, min, - max + max, + neutralPoint })} , @@ -461,6 +523,7 @@ interface RangeKnob { ratio: number; min: number; max: number; + neutralPoint: number; disabled: boolean; pressed: boolean; pin: boolean; @@ -468,7 +531,7 @@ interface RangeKnob { handleKeyboard: (name: KnobName, isIncrease: boolean) => void; } -function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard }: RangeKnob) { +function renderKnob({ knob, value, ratio, min, max, neutralPoint, disabled, pressed, pin, handleKeyboard }: RangeKnob) { const isRTL = document.dir === 'rtl'; const start = isRTL ? 'right' : 'left'; @@ -501,7 +564,8 @@ function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, hand 'range-knob-b': knob === 'B', 'range-knob-pressed': pressed, 'range-knob-min': value === min, - 'range-knob-max': value === max + 'range-knob-max': value === max, + 'range-knob-neutral': value === neutralPoint }} style={knobStyle()} role="slider" @@ -510,6 +574,7 @@ function renderKnob({ knob, value, ratio, min, max, disabled, pressed, pin, hand aria-valuemax={max} aria-disabled={disabled ? 'true' : null} aria-valuenow={value} + aria-valueneutral={neutralPoint} > {pin && }