diff --git a/packages/components/src/components/slider/__snapshots__/slider.spec.ts.snap b/packages/components/src/components/slider/__snapshots__/slider.spec.ts.snap index 69e8f2e47f..aaf8c7f41f 100644 --- a/packages/components/src/components/slider/__snapshots__/slider.spec.ts.snap +++ b/packages/components/src/components/slider/__snapshots__/slider.spec.ts.snap @@ -1,19 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Slider should match snapshot 1`] = ` - + -
-
-
-
-
-
+
+
+
+ 0 +
+
+
+
+
+
+
+
+
- -
+
Label @@ -21,21 +27,25 @@ exports[`Slider should match snapshot 1`] = ` `; exports[`Slider should match snapshot 2`] = ` - + -
-
-
-
-
-
-
+
+
+
+ 10
- -
- 10% +
+
+
+
+
+
+
+
+
+
Label diff --git a/packages/components/src/components/slider/readme.md b/packages/components/src/components/slider/readme.md index 6f572efdc9..7102336dc6 100644 --- a/packages/components/src/components/slider/readme.md +++ b/packages/components/src/components/slider/readme.md @@ -7,23 +7,29 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------- | -------------- | ----------------------------------------------------------------------------------------- | ------------- | ----------- | -| `customColor` | `custom-color` | **[DEPRECATED]** - optional) slider custom color

| `string` | `undefined` | -| `decimals` | `decimals` | (optional) number of decimal places | `0 \| 1 \| 2` | `0` | -| `disabled` | `disabled` | (optional) disabled | `boolean` | `false` | -| `label` | `label` | (optional) slider label | `string` | `undefined` | -| `max` | `max` | (optional) the maximal value of the slider | `number` | `100` | -| `min` | `min` | t(optional) he minimal value of the slider | `number` | `0` | -| `name` | `name` | (optional) the name of the slider | `string` | `undefined` | -| `showValue` | `show-value` | (optional) slider display value | `boolean` | `true` | -| `sliderId` | `slider-id` | (optional) Slider id | `string` | `undefined` | -| `step` | `step` | (optional) the step size to increase or decrease when dragging slider | `number` | `1` | -| `styles` | `styles` | (optional) Injected CSS styles | `string` | `undefined` | -| `thumbLarge` | `thumb-large` | (optional) larger thumb | `boolean` | `false` | -| `trackSmall` | `track-small` | (optional) smaller track | `boolean` | `false` | -| `unit` | `unit` | (optional) slider value unit | `string` | `'%'` | -| `value` | `value` | (optional) the display value of the slider | `number` | `undefined` | +| Property | Attribute | Description | Type | Default | +| --------------- | ----------------- | ---------------------------------------------------------------------------------------- | --------------------- | ----------- | +| `customColor` | `custom-color` | **[DEPRECATED]** (optional) slider custom color

| `string` | `undefined` | +| `decimals` | `decimals` | (optional) number of decimal places | `0 \| 1 \| 2` | `0` | +| `disabled` | `disabled` | (optional) disabled | `boolean` | `false` | +| `helperText` | `helper-text` | (optional) helper text | `string` | `undefined` | +| `label` | `label` | (optional) slider label | `string` | `undefined` | +| `max` | `max` | (optional) the maximal value of the slider | `number` | `100` | +| `min` | `min` | t(optional) he minimal value of the slider | `number` | `0` | +| `name` | `name` | (optional) the name of the slider | `string` | `undefined` | +| `range` | `range` | (optional) multi-thumb | `boolean` | `false` | +| `showStepMarks` | `show-step-marks` | (optional) show a mark for each step | `boolean` | `false` | +| `showValue` | `show-value` | (optional) slider display value | `boolean` | `true` | +| `sliderId` | `slider-id` | (optional) Slider id | `string` | `undefined` | +| `step` | `step` | (optional) the step size to increase or decrease when dragging slider | `number` | `1` | +| `styles` | `styles` | (optional) Injected CSS styles | `string` | `undefined` | +| `thumbLarge` | `thumb-large` | **[DEPRECATED]** (optional) larger thumb

| `boolean` | `undefined` | +| `trackSmall` | `track-small` | **[DEPRECATED]** (optional) smaller track

| `boolean` | `undefined` | +| `unit` | `unit` | (optional) slider value unit | `string` | `''` | +| `unitPosition` | `unit-position` | (optional) unit position | `"after" \| "before"` | `'after'` | +| `value` | `value` | (optional) the value of the slider | `number` | `0` | +| `valueFrom` | `value-from` | (optional) when `range` is true, the "from" value | `number` | `0` | +| `valueTo` | `value-to` | (optional) when `range` is true, the "to" value | `number` | `0` | ## Events @@ -41,12 +47,19 @@ | Part | Description | | ----------------- | ----------- | | `"bar"` | | -| `"display-value"` | | +| `"from"` | | +| `"helper-text"` | | +| `"inner-track"` | | | `"label"` | | -| `"thumb"` | | +| `"label-wrapper"` | | +| `"meta"` | | +| `"step-mark"` | | +| `"step-marks"` | | | `"thumb-wrapper"` | | +| `"to"` | | | `"track"` | | | `"track-wrapper"` | | +| `"value-text"` | | ---------------------------------------------- diff --git a/packages/components/src/components/slider/slider.css b/packages/components/src/components/slider/slider.css index 0617fbb310..4be389e513 100644 --- a/packages/components/src/components/slider/slider.css +++ b/packages/components/src/components/slider/slider.css @@ -10,146 +10,185 @@ */ :host { - --border: 1px solid var(--telekom-color-ui-border-standard); - --background-bar: var(--telekom-color-primary-standard); - --border-color-thumb: var(--telekom-color-ui-border-standard); - --box-shadow-thumb: var(--telekom-shadow-resting-standard); - --border-color-thumb-hover: var(--telekom-color-ui-border-hovered); - --border-color-thumb-active: var(--telekom-color-ui-border-pressed); - --box-shadow-thumb-focus: 0 0 0 var(--telekom-line-weight-highlight) - var(--telekom-color-functional-focus); - --color-display-value: var(--telekom-color-text-and-icon-additional); - --font-weight-display-value: var(--telekom-typography-font-weight-bold); - --font-size-display-value: var(--telekom-typography-font-size-small); + --width: 368px; + --height-track: 6px; --background-track: var(--telekom-color-ui-faint); - --background-bar-disabled: var(--telekom-color-ui-disabled); - --color-label-disabled: var(--telekom-color-text-and-icon-disabled); + --radius-track: var(--telekom-radius-pill); + --spacing-track: var(--telekom-spacing-unit-x5) 0 + var(--telekom-spacing-unit-x4); + --spacing-x-inner-track: 10px; + --background-bar: var(--telekom-color-primary-standard); + --radius-thumb: var(--telekom-radius-circle); + --size-thumb: 24px; + --border-color-thumb: rgba(0, 0, 0, 0.05); + --background-thumb: var(--telekom-color-ui-white); + --color-focus: var(--telekom-color-functional-focus); + --spacing-x-step-marks: 8px; + --color-step-mark: var(--telekom-color-text-and-icon-additional); + --radius-step-mark: var(--telekom-radius-circle); + --size-step-mark: 4px; + --font-label: var(--telekom-text-style-ui); + --font-helper-text: var(--telekom-text-style-small-bold); + --color-helper-text: var(--telekom-color-text-and-icon-additional); } -.slider { - width: 100%; - display: block; - align-items: center; +[part~='base'] { + width: var(--width); } -.slider .slider__track-wrapper { +[part='label-wrapper'] { display: flex; - align-items: center; + justify-content: space-between; + align-items: flex-start; +} + +[part='label'] { + font: var(--font-label); +} + +[part='value-text'] { + font: var(--font-label); + font-variant-numeric: tabular-nums; } -.slider .slider__track { - width: 303px; - border: var(--border); - height: 6px; - margin: 16px 0; +[part='track-wrapper'] { display: flex; + position: relative; + align-items: center; +} + +[part='track'] { position: relative; box-sizing: border-box; + display: flex; + align-items: center; + margin: var(--spacing-track); + width: 100%; + height: var(--height-track); + background: var(--background-track); + border-radius: var(--radius-track); +} + +/* Really holds the thumb, adding some spacing on the sides */ +[part='inner-track'] { + position: absolute; + display: flex; align-items: center; - border-radius: 100px; + left: var(--spacing-x-inner-track); + width: calc(100% - var(--spacing-x-inner-track) * 2); + height: 100%; } -.slider .slider__bar { - height: 6px; - z-index: -1; +[part='bar'] { + height: 100%; position: absolute; - border-radius: 100px; + z-index: 1; + border-radius: var(--radius-track); background-color: var(--background-bar); - z-index: var(--scl-z-index-10); } -.slider .slider__thumb-wrapper { - width: 32px; - height: 32px; - display: flex; - z-index: var(--scl-z-index-20); +[part~='thumb-wrapper'] { position: absolute; - text-align: center; + z-index: 3; + display: flex; align-items: center; - margin-left: -16px; justify-content: center; + width: 32px; + height: 32px; + margin-left: -16px; background-color: transparent; } -.slider .slider__thumb { - width: 16px; - border: 1px solid; - height: 16px; +[part~='thumb'] { + --_border: 0 0 0 var(--telekom-spacing-unit-x025) var(--border-color-thumb); + width: var(--size-thumb); + height: var(--size-thumb); outline: none; box-sizing: border-box; - border-color: var(--border-color-thumb); - border-radius: 50%; - background-color: #fff; - box-shadow: var(--telekom-shadow-resting-standard); + border-radius: var(--radius-thumb); + background-color: var(--background-thumb); + box-shadow: var(--_border), var(--telekom-shadow-raised-standard); } -.slider .slider__display-value { - color: var(--color-display-value); - margin-left: 24px; - font-weight: var(--font-weight-display-value); - font-size: var(--font-size-display-value); +[part~='thumb-wrapper']:hover [part~='thumb'] { + box-shadow: var(--_border), var(--telekom-shadow-raised-hover); } -.slider .slider__thumb:hover { - border-color: var(--border-color-thumb-hover); +[part~='thumb-wrapper']:active [part~='thumb'] { + box-shadow: var(--_border), var(--telekom-shadow-raised-pressed); } -.slider .slider__thumb:active { - border-color: var(--border-color-thumb-active); +[part~='thumb']:focus { + outline: var(--telekom-line-weight-highlight) solid var(--color-focus); + outline-offset: 1px; } -.slider .slider__thumb:focus { - box-shadow: var(--box-shadow-thumb-focus); +[part='step-marks'] { + width: 100%; + position: relative; + z-index: 2; + display: flex; + justify-content: space-between; + padding: 0 var(--spacing-x-step-marks); } -.slider .slider__thumb-wrapper:hover { - cursor: grab; +[part='step-mark'] { + width: var(--size-step-mark); + height: var(--size-step-mark); + background: var(--color-step-mark); + border-radius: var(--telekom-radius-circle); } -.slider .slider__thumb-wrapper:active { - cursor: grabbing; +[part='meta'] { + display: flex; + justify-content: space-between; } -.slider--track-small .slider__track { - border: none; - height: 1px; - border-top: 1px solid transparent; - background-color: var(--background-track); +[part='helper-text'] { + font: var(--font-helper-text); + color: var(--color-helper-text); } -.slider--track-small .slider__bar { - border: 1px solid transparent; - height: 3px; - z-index: 1; - box-sizing: border-box; +/* disabled */ + +/* TODO can this be overwritten? it should... */ +[part~='disabled'] [part='label-wrapper'], +[part~='disabled'] [part='helper-text'] { + color: var(--telekom-color-text-and-icon-disabled); } -.slider--thumb-large .slider__thumb { - width: 24px; - height: 24px; +[part~='disabled'] [part='bar'] { + background-color: var(--telekom-color-ui-border-disabled); } -.slider--disabled .slider__track-wrapper { - cursor: not-allowed; +[part~='disabled'] [part~='thumb-wrapper'] { + display: none; } -.slider--disabled .slider__bar { - background-color: var(--background-bar-disabled); - z-index: var(--scl-z-index-10); +/* cursor */ + +[part~='thumb-wrapper']:hover { + cursor: grab; } -.slider--disabled .slider__track { - border-color: var(--telekom-color-ui-border-disabled); +[part~='thumb-wrapper']:active { + cursor: grabbing; } -.slider--disabled .slider__thumb { - display: none; +[part~='disabled'] [part='track-wrapper'] { + cursor: not-allowed; } -.slider--disabled .slider__label { - color: var(--color-label-disabled); +/* PLATFORM iOS */ + +:host-context([data-platform='ios']) { + --height-track: 4px; + --size-thumb: 26px; + --size-step-mark: 2px; } -.slider--disabled .slider__thumb-wrapper:hover { - cursor: not-allowed; +/* PLATFORM Android */ + +:host-context([data-platform='android']) { + --background-thumb: var(--telekom-color-primary-standard); } diff --git a/packages/components/src/components/slider/slider.spec.ts b/packages/components/src/components/slider/slider.spec.ts index edd3536ce2..eabb430ce0 100644 --- a/packages/components/src/components/slider/slider.spec.ts +++ b/packages/components/src/components/slider/slider.spec.ts @@ -59,24 +59,6 @@ describe('Slider', () => { expect(page.root).toMatchSnapshot(); }); - describe('classes', () => { - it('should handle getCssClassMap() and getBasePartMap()', () => { - const element = new Slider(); - element.disabled = true; - element.trackSmall = true; - element.thumbLarge = true; - expect(element.getCssClassMap()).toContain('slider'); - expect(element.getCssClassMap()).toContain('slider--disabled'); - expect(element.getCssClassMap()).toContain('slider--track-small'); - expect(element.getCssClassMap()).toContain('slider--thumb-large'); - - expect(element.getBasePartMap()).toContain('slider'); - expect(element.getBasePartMap()).toContain('track-small'); - expect(element.getBasePartMap()).toContain('disabled'); - expect(element.getBasePartMap()).toContain('thumb-large'); - }); - }); - describe('props', () => { it('check default props', async () => { const page = await newSpecPage({ @@ -86,13 +68,10 @@ describe('Slider', () => { expect(page.rootInstance.min).toBe(0); expect(page.rootInstance.max).toBe(100); expect(page.rootInstance.step).toBe(1); - expect(page.rootInstance.unit).toBe('%'); + expect(page.rootInstance.unit).toBe(''); expect(page.rootInstance.decimals).toBe(0); expect(page.rootInstance.showValue).toBe(true); - expect(page.rootInstance.customColor).toBe(undefined); expect(page.rootInstance.disabled).toBe(false); - expect(page.rootInstance.trackSmall).toBe(false); - expect(page.rootInstance.thumbLarge).toBe(false); }); it('check props being set', async () => { @@ -107,11 +86,9 @@ describe('Slider', () => { page.root.unit = ''; page.root.decimals = 2; page.root.label = 'slider label'; + page.root.helperText = 'helper text'; page.root.showValue = false; - page.root.customColor = 'magenta'; page.root.disabled = 'true'; - page.root.trackSmall = 'true'; - page.root.thumbLarge = 'true'; page.root.sliderId = 'sliderID'; page.root.styles = 'background : red'; await page.waitForChanges(); @@ -122,23 +99,21 @@ describe('Slider', () => { expect(page.rootInstance.unit).toBe(''); expect(page.rootInstance.decimals).toBe(2); expect(page.rootInstance.label).toBe('slider label'); + expect(page.rootInstance.helperText).toBe('helper text'); expect(page.rootInstance.showValue).toBe(false); - expect(page.rootInstance.customColor).toBe('magenta'); expect(page.rootInstance.disabled).toBe(true); - expect(page.rootInstance.trackSmall).toBe(true); - expect(page.rootInstance.thumbLarge).toBe(true); expect(page.rootInstance.sliderId).toBe('sliderID'); expect(page.rootInstance.styles).toBe('background : red'); }); }); - it('keydown .slider__thumb with ArrowRight', async () => { + it('keydown [part="thumb"] with ArrowRight', async () => { const page = await newSpecPage({ components: [Slider], html: ``, }); page.root.value = 50; - simulateKeyboardEvent(page, 'keydown', '.slider__thumb', 'ArrowRight'); + simulateKeyboardEvent(page, 'keydown', '[part="thumb"]', 'ArrowRight'); expect(await page.rootInstance.value).toBe(51); }); @@ -151,31 +126,31 @@ describe('Slider', () => { const inputSpyLegacy = jest.fn(); page.doc.addEventListener('scale-input', inputSpy); page.doc.addEventListener('scaleInput', inputSpyLegacy); - const element = page.root.shadowRoot.querySelector('.slider__thumb'); + const element = page.root.shadowRoot.querySelector('[part="thumb"]'); element.dispatchEvent(new Event('keydown')); await page.waitForChanges(); expect(inputSpy).toHaveBeenCalled(); expect(inputSpyLegacy).toHaveBeenCalled(); }); - it('keydown .slider__thumb with ArrowUp', async () => { + it('keydown [part="thumb"] with ArrowUp', async () => { const page = await newSpecPage({ components: [Slider], html: ``, }); page.root.value = 50; - simulateKeyboardEvent(page, 'keydown', '.slider__thumb', 'ArrowUp'); + simulateKeyboardEvent(page, 'keydown', '[part="thumb"]', 'ArrowUp'); expect(await page.rootInstance.value).toBe(60); }); - it('mousedown .slider__thumb-wrapper', async () => { + it('mousedown [part="thumb-wrapper"]', async () => { const page = await newSpecPage({ components: [Slider], html: ``, }); page.root.dragging = false; expect(await page.rootInstance.dragging).toBe(undefined); - simulateMouseEvent(page, 'mousedown', '.slider__thumb-wrapper'); + simulateMouseEvent(page, 'mousedown', '[part="thumb-wrapper"]'); expect(await page.rootInstance.dragging).toBe(true); }); }); diff --git a/packages/components/src/components/slider/slider.tsx b/packages/components/src/components/slider/slider.tsx index 8c90129651..3a7cc9459a 100644 --- a/packages/components/src/components/slider/slider.tsx +++ b/packages/components/src/components/slider/slider.tsx @@ -9,8 +9,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -// ((input - min) * 100) / (max - min) - import { Component, h, @@ -21,6 +19,7 @@ import { Watch, EventEmitter, Element, + Fragment, } from '@stencil/core'; import classNames from 'classnames'; import { emitEvent } from '../../utils/utils'; @@ -35,41 +34,57 @@ let i = 0; }) export class Slider { sliderTrack?: HTMLDivElement; + /* Host HTML Element */ @Element() hostElement: HTMLElement; + /** (optional) the name of the slider */ @Prop() name?: string; - /** (optional) the display value of the slider */ - @Prop() value?: number; + /** (optional) the value of the slider */ + @Prop({ mutable: true, reflect: true }) value?: number = 0; + /** (optional) multi-thumb */ + @Prop() range?: boolean = false; + /** (optional) when `range` is true, the "from" value */ + @Prop({ mutable: true, reflect: true }) valueFrom?: number = 0; + /** (optional) when `range` is true, the "to" value */ + @Prop({ mutable: true, reflect: true }) valueTo?: number = 0; /** t(optional) he minimal value of the slider */ @Prop() min?: number = 0; /** (optional) the maximal value of the slider */ @Prop() max?: number = 100; /** (optional) the step size to increase or decrease when dragging slider */ @Prop() step?: number = 1; + /** (optional) show a mark for each step */ + @Prop() showStepMarks?: boolean = false; /** (optional) slider label */ @Prop() label?: string; + /** (optional) helper text */ + @Prop() helperText?: string; /** (optional) slider display value */ @Prop() showValue?: boolean = true; /** (optional) slider value unit */ - @Prop() unit?: string = '%'; + @Prop() unit?: string = ''; + /** (optional) unit position */ + @Prop() unitPosition?: 'before' | 'after' = 'after'; /** (optional) number of decimal places */ @Prop() decimals?: 0 | 1 | 2 = 0; - /** @deprecated - optional) slider custom color */ + /** @deprecated (optional) slider custom color */ @Prop() customColor?: string; /** (optional) disabled */ @Prop() disabled?: boolean = false; - /** (optional) smaller track */ - @Prop() trackSmall?: boolean = false; - /** (optional) larger thumb */ - @Prop() thumbLarge?: boolean = false; + /** @deprecated (optional) smaller track */ + @Prop() trackSmall?: boolean; + /** @deprecated (optional) larger thumb */ + @Prop() thumbLarge?: boolean; /** (optional) Slider id */ - @Prop() sliderId?: string; + @Prop({ mutable: true }) sliderId?: string; /** (optional) Injected CSS styles */ @Prop() styles?: string; // The actual position in % of the slider thumb - @State() position: number; + @State() position: number = 0; + @State() positionFrom: number = 25; + @State() positionTo: number = 75; @Event({ eventName: 'scale-change' }) scaleChange: EventEmitter; /** @deprecated in v3 in favor of kebab-case event names */ @@ -80,18 +95,35 @@ export class Slider { @Event({ eventName: 'scaleInput' }) scaleInputLegacy: EventEmitter; private dragging: boolean; - private offsetLeft: number; + // Don't know how to make TypeScript handle `this[offsetKey]` + // private offsetLeft: number; + // private offsetLeftFrom: number; + // private offsetLeftTo: number; + private activeRangeThumb: null | 'From' | 'To' = null; constructor() { this.onDragging = this.onDragging.bind(this); this.onDragEnd = this.onDragEnd.bind(this); } + @Watch('value') + @Watch('valueFrom') + @Watch('valueTo') + handleValueChange() { + this.setPosition(); + } + componentWillLoad() { if (this.sliderId == null) { this.sliderId = 'slider-' + i++; } - this.setPosition(); + // Set initial position + if (this.range) { + this.setPosition('From'); + this.setPosition('To'); + } else { + this.setPosition(); + } } disconnectedCallback() { @@ -109,49 +141,81 @@ export class Slider { source: this.hostElement, }); } + if (this.thumbLarge !== undefined) { + statusNote({ + tag: 'deprecated', + message: `Property "thumbLarge" is deprecated.`, + type: 'warn', + source: this.hostElement, + }); + } + if (this.trackSmall !== undefined) { + statusNote({ + tag: 'deprecated', + message: `Property "trackSmall" is deprecated.`, + type: 'warn', + source: this.hostElement, + }); + } } - onButtonDown = () => { + onButtonDown = (event) => { if (this.disabled) { return; } + this.setActiveRangeThumbFromEvent(event); this.onDragStart(); this.addGlobalListeners(); }; onKeyDown = (event: KeyboardEvent) => { let steps = 0; + this.setActiveRangeThumbFromEvent(event); if (['ArrowRight', 'ArrowLeft'].includes(event.key)) { steps = event.key === 'ArrowRight' ? this.step : -this.step; } if (['ArrowUp', 'ArrowDown'].includes(event.key)) { steps = event.key === 'ArrowUp' ? this.step * 10 : -this.step * 10; } - this.setValue(this.value + steps); + const valueKey = this.getKeyFor('value'); + this.setValue(this[valueKey] + steps, valueKey); + emitEvent( + this, + 'scaleChange', + this.range ? [this.valueFrom, this.valueTo] : this.value + ); }; onDragStart = () => { + const offsetKey = this.getKeyFor('offsetLeft'); this.dragging = true; - this.offsetLeft = this.sliderTrack.getBoundingClientRect().left; + this[offsetKey] = this.sliderTrack.getBoundingClientRect().left; }; onDragging = (event: any) => { - const { dragging, offsetLeft } = this; - - if (dragging) { - const currentX = this.handleTouchEvent(event).clientX; - const position: number = - ((currentX - offsetLeft) / this.sliderTrack.offsetWidth) * 100; - const nextValue = (position * (this.max - this.min)) / 100 + this.min; - // https://stackoverflow.com/q/14627566 - const roundedNextValue = Math.ceil(nextValue / this.step) * this.step; - this.setValue(roundedNextValue); + if (!this.dragging) { + return; } + const valueKey = this.getKeyFor('value'); + const offsetLeftKey = this.getKeyFor('offsetLeft'); + const offsetLeft = this[offsetLeftKey]; + + const currentX = this.handleTouchEvent(event).clientX; + const position: number = + ((currentX - offsetLeft) / this.sliderTrack.offsetWidth) * 100; + const nextValue = (position * (this.max - this.min)) / 100 + this.min; + // https://stackoverflow.com/q/14627566 + const roundedNextValue = Math.ceil(nextValue / this.step) * this.step; + this.setValue(roundedNextValue, valueKey); }; onDragEnd = () => { this.dragging = false; - emitEvent(this, 'scaleChange', this.value); + emitEvent( + this, + 'scaleChange', + this.range ? [this.valueFrom, this.valueTo] : this.value + ); this.removeGlobalListeners(); }; @@ -159,31 +223,91 @@ export class Slider { return event.type.indexOf('touch') === 0 ? event.touches[0] : event; } - setValue = (nextValue: number) => { - this.value = this.clamp(nextValue); - emitEvent(this, 'scaleInput', this.value); + setValue = ( + nextValue: number, + valueKey: string | 'value' | 'valueFrom' | 'valueTo' = 'value' + ) => { + this[valueKey] = this.clamp(nextValue); + emitEvent( + this, + 'scaleInput', + this.range ? [this.valueFrom, this.valueTo] : this.value + ); }; - @Watch('value') - handleValueChange() { - this.setPosition(); - } - - setPosition = () => { - if (!this.value) { - this.position = 0; + setActiveRangeThumbFromEvent = (event) => { + if (!this.range) { + this.activeRangeThumb = null; return; } - const clampedValue = this.clamp(this.value); + const part = (event.target as HTMLElement).part; + this.activeRangeThumb = part.contains('from') ? 'From' : 'To'; + }; + + setPosition = (thumb?: string) => { + const valueKey = this.getKeyFor('value', thumb); + const positionKey = this.getKeyFor('position', thumb); + const clampedValue = this.clamp(this[valueKey]); // https://stackoverflow.com/a/25835683 - this.position = ((clampedValue - this.min) * 100) / (this.max - this.min); + // ((input - min) * 100) / (max - min) + this[positionKey] = + ((clampedValue - this.min) * 100) / (this.max - this.min); + }; + + /** + * Utility function + * e.g. 'value' -> 'valueFrom' if `activeRangeThumb='From'` + * @param propName + * @returns {string} The prop name with the range suffix if needed + */ + getKeyFor = ( + propName: 'value' | 'offsetLeft' | 'position', + thumb?: string + ) => { + if (this.range) { + return `${propName}${this.activeRangeThumb ?? thumb}`; + } + return propName; + }; + + getTextValue = () => { + if (this.range) { + const from = this.valueFrom?.toFixed(this.decimals); + const to = this.valueTo?.toFixed(this.decimals); + return this.unitPosition === 'before' + ? `${this.unit}${from} - ${this.unit}${to}` + : `${from}${this.unit} - ${to}${this.unit}`; + } + return this.unitPosition === 'before' + ? `${this.unit}${this.value?.toFixed(this.decimals)}` + : `${this.value?.toFixed(this.decimals)}${this.unit}`; + }; + + getNumberOfSteps = () => { + const n = (this.max - this.min) / this.step + 1; + return [...Array(n).keys()]; + }; + + clamp = (val: number) => { + let min = this.min; + let max = this.max; + // Take into account the other thumb, when `range=true` + if (this.range) { + if (this.activeRangeThumb === 'From') { + max = Math.min(this.valueTo, this.max); + } else if (this.activeRangeThumb === 'To') { + min = Math.max(this.valueFrom, this.min); + } + } + // Regular generic clamp + return Math.min(Math.max(val, min), max); }; addGlobalListeners() { - window.addEventListener('mousemove', this.onDragging.bind(this)); - window.addEventListener('mouseup', this.onDragEnd.bind(this)); - window.addEventListener('touchmove', this.onDragging.bind(this)); - window.addEventListener('touchend', this.onDragEnd.bind(this)); + window.addEventListener('mousemove', this.onDragging); + window.addEventListener('mouseup', this.onDragEnd); + window.addEventListener('touchmove', this.onDragging); + window.addEventListener('touchend', this.onDragEnd); } removeGlobalListeners() { @@ -194,97 +318,141 @@ export class Slider { } render() { + const helperTextId = `slider-helper-message-${i}`; + const ariaDescribedByAttr = { 'aria-describedBy': helperTextId }; + return ( {this.styles && } -
- {!!this.label && ( - - )} -
+
+
+ {!!this.label && ( + + )} + {this.showValue && ( +
{this.getTextValue()}
+ )} +
+
(this.sliderTrack = el as HTMLDivElement)} >
-
-
+ {this.showStepMarks && ( +
+ {this.getNumberOfSteps().map(() => ( + + ))} +
+ )} +
+ {/* Two thumbs or one */} + {this.range ? ( + +
+
+
+
+
+
+ + ) : ( +
+
+
+ )}
- - {this.showValue && ( -
- {this.value != null && this.value.toFixed(this.decimals)} - {this.value != null && this.unit} -
- )}
+ {/* (a11y) Not sure about this being only one input, or its value, or useful at all… */} + + {this.helperText && ( +
+
{this.helperText}
+
+ )}
); } - - getBasePartMap() { - return this.getCssOrBasePartMap('basePart'); - } - - getCssClassMap() { - return this.getCssOrBasePartMap('css'); - } - - getCssOrBasePartMap(mode: 'basePart' | 'css') { - const component = 'slider'; - const prefix = mode === 'basePart' ? '' : `${component}--`; - - return classNames( - component, - this.disabled && `${prefix}disabled`, - this.trackSmall && `${prefix}track-small`, - this.thumbLarge && `${prefix}thumb-large` - ); - } - - private clamp = (val: number) => { - return Math.min(Math.max(val, this.min), this.max); - }; } diff --git a/packages/components/src/html/slider.html b/packages/components/src/html/slider.html index 6bd3ec62e0..5c55f358f8 100644 --- a/packages/components/src/html/slider.html +++ b/packages/components/src/html/slider.html @@ -24,6 +24,17 @@

Color thingy

+ +

+ +

+ Color thingy - +
+ +
+
+ +
diff --git a/packages/storybook-vue/stories/components/slider/Slider.stories.mdx b/packages/storybook-vue/stories/components/slider/Slider.stories.mdx index 069f5336bd..7e301f46c3 100644 --- a/packages/storybook-vue/stories/components/slider/Slider.stories.mdx +++ b/packages/storybook-vue/stories/components/slider/Slider.stories.mdx @@ -19,76 +19,36 @@ import { action } from '@storybook/addon-actions'; /> export const Template = (args, { argTypes }) => ({ - components: { ScaleSlider }, props: { ...ScaleSlider.props, }, template: ` + :label="label" + :value="value" + :range="range" + :value-from="valueFrom" + :value-to="valueTo" + :min="min" + :max="max" + :step="step" + :show-step-marks="showStepMarks" + :unit="unit" + :unit-position="unitPosition" + :decimals="decimals" + :disabled="disabled" + :show-value="String(showValue)" + :name="name" + :slider-id="sliderId" + :helper-text="helperText" + @scale-change="handleScaleChange" + @scale-input="handleScaleInput" + > - `, + `, methods: { - scaleButtonDown: action('scaleButtonDown'), - scaleDragStart: action('scaleDragStart'), - scaleDragging: action('scaleDragging'), - scaleDragEnd: action('scaleDragEnd'), - scaleSetPosition: action('scaleSetPosition'), - scaleCurrentPosition: action('scaleCurrentPosition'), - }, -}); - -export const TemplateCustomColor = (args, { argTypes }) => ({ - components: { ScaleSlider }, - props: { - ...ScaleSlider.props, - }, - template: ` - - - `, - methods: { - scaleButtonDown: action('scaleButtonDown'), - scaleDragStart: action('scaleDragStart'), - scaleDragging: action('scaleDragging'), - scaleDragEnd: action('scaleDragEnd'), - scaleSetPosition: action('scaleSetPosition'), - scaleCurrentPosition: action('scaleCurrentPosition'), + handleScaleChange: action('scale-change'), + handleScaleInput: action('scale-input'), }, }); @@ -111,7 +71,7 @@ export const TemplateCustomColor = (args, { argTypes }) => ({ name="Standard" args={{ label: 'Standard', - value: 20, + value: 42, }} > {Template.bind({})} @@ -124,35 +84,43 @@ export const TemplateCustomColor = (args, { argTypes }) => ({ ```css :host { - --border: 1px solid var(--telekom-color-ui-border-standard); - --background-bar: var(--telekom-color-primary-standard); - --border-color-thumb: var(--telekom-color-ui-border-standard); - --box-shadow-thumb: var(--telekom-shadow-resting-standard); - --border-color-thumb-hover: var(--telekom-color-ui-border-hovered); - --border-color-thumb-active: var(--telekom-color-ui-border-pressed); - --box-shadow-thumb-focus: 0 0 0 var(--telekom-line-weight-highlight) var( - --telekom-color-functional-focus - ); - --color-display-value: var(--telekom-color-text-and-icon-additional); - --font-weight-display-value: var(--telekom-typography-font-weight-bold); - --font-size-display-value: var(--telekom-typography-font-size-small); + --width: 368px; + --height-track: 6px; --background-track: var(--telekom-color-ui-faint); - --background-bar-disabled: var(--telekom-color-ui-disabled); - --color-label-disabled: var(--telekom-color-text-and-icon-disabled); + --radius-track: var(--telekom-radius-pill); + --spacing-track: var(--telekom-spacing-unit-x5) 0 + var(--telekom-spacing-unit-x4); + --spacing-x-inner-track: 10px; + --background-bar: var(--telekom-color-primary-standard); + --radius-thumb: var(--telekom-radius-circle); + --size-thumb: 24px; + --border-color-thumb: rgba(0, 0, 0, 0.05); + --background-thumb: var(--telekom-color-ui-white); + --color-focus: var(--telekom-color-functional-focus); + --spacing-x-step-marks: 8px; + --color-step-mark: var(--telekom-color-text-and-icon-additional); + --radius-step-mark: var(--telekom-radius-circle); + --size-step-mark: 4px; + --font-label: var(--telekom-text-style-ui); + --font-helper-text: var(--telekom-text-style-small-bold); + --color-helper-text: var(--telekom-color-text-and-icon-additional); } ``` For Shadow Parts, please inspect the element's #shadow. -## Slider track small +## Range {Template.bind({})} @@ -160,18 +128,27 @@ For Shadow Parts, please inspect the element's #shadow. ```html - + + ``` -## Slider thumb large +## Step Marks {Template.bind({})} @@ -179,39 +156,51 @@ For Shadow Parts, please inspect the element's #shadow. ```html - + + ``` -## Slider with custom color +## Helper Text - {TemplateCustomColor.bind({})} + {Template.bind({})} ```html + label="Alpha" + value="0.66" + step="0.01" + max="1" + helper-text="Between 0 and 1"> + ``` -## Disabled slider +## Disabled @@ -220,5 +209,49 @@ For Shadow Parts, please inspect the element's #shadow. ```html - + + +``` + +## Platform iOS + + + + {{ + template: ` +
+ +
+ ` + }} +
+
+ +```html +
+ +
+``` + +## Platform Android + + + + {{ + template: ` +
+ +
+ ` + }} +
+
+ +```html +
+ +
```