diff --git a/packages/cdk/utils/src/dom.ts b/packages/cdk/utils/src/dom.ts index b615ff4e2..1504b72dc 100644 --- a/packages/cdk/utils/src/dom.ts +++ b/packages/cdk/utils/src/dom.ts @@ -178,3 +178,20 @@ export function isVisibleElement(element: HTMLElement | SVGElement | undefined): return isStyleVisible(element) && isAttributeVisible(element) } + +export function getMouseClientXY(ev: MouseEvent | TouchEvent): { clientX: number; clientY: number } { + let clientX: number + let clientY: number + if (ev.type.startsWith('touch')) { + clientY = (ev as TouchEvent).touches[0].clientY + clientX = (ev as TouchEvent).touches[0].clientX + } else { + clientY = (ev as MouseEvent).clientY + clientX = (ev as MouseEvent).clientX + } + + return { + clientX, + clientY, + } +} diff --git a/packages/components/components.less b/packages/components/components.less new file mode 100644 index 000000000..9f7ae937c --- /dev/null +++ b/packages/components/components.less @@ -0,0 +1,53 @@ +@import './affix/style/index.less'; +@import './alert/style/index.less'; +@import './anchor/style/index.less'; +@import './avatar/style/index.less'; +@import './back-top/style/index.less'; +@import './badge/style/index.less'; +@import './button/style/index.less'; +@import './card/style/index.less'; +@import './checkbox/style/index.less'; +@import './collapse/style/index.less'; +@import './date-picker/style/index.less'; +@import './divider/style/index.less'; +@import './drawer/style/index.less'; +@import './dropdown/style/index.less'; +@import './empty/style/index.less'; +@import './form/style/index.less'; +@import './grid/style/index.less'; +@import './header/style/index.less'; +@import './icon/style/index.less'; +@import './image/style/index.less'; +@import './input/style/index.less'; +@import './input-number/style/index.less'; +@import './layout/style/index.less'; +@import './list/style/index.less'; +@import './menu/style/index.less'; +@import './message/style/index.less'; +@import './modal/style/index.less'; +@import './notification/style/index.less'; +@import './pagination/style/index.less'; +@import './popconfirm/style/index.less'; +@import './popover/style/index.less'; +@import './progress/style/index.less'; +@import './radio/style/index.less'; +@import './rate/style/index.less'; +@import './result/style/index.less'; +@import './select/style/index.less'; +@import './skeleton/style/index.less'; +@import './space/style/index.less'; +@import './spin/style/index.less'; +@import './statistic/style/index.less'; +@import './stepper/style/index.less'; +@import './switch/style/index.less'; +@import './table/style/index.less'; +@import './tabs/style/index.less'; +@import './tag/style/index.less'; +@import './textarea/style/index.less'; +@import './time-picker/style/index.less'; +@import './timeline/style/index.less'; +@import './tooltip/style/index.less'; +@import './tree/style/index.less'; +@import './typography/style/index.less'; + +@import './slider/style/index.less'; \ No newline at end of file diff --git a/packages/components/default.less b/packages/components/default.less index f8a737160..e1cb437ba 100644 --- a/packages/components/default.less +++ b/packages/components/default.less @@ -41,6 +41,7 @@ @import './result/style/themes/default.less'; @import './select/style/themes/default.less'; @import './skeleton/style/themes/default.less'; +@import './slider/style/themes/default.less'; @import './space/style/themes/default.less'; @import './spin/style/themes/default.less'; @import './statistic/style/themes/default.less'; diff --git a/packages/components/index.ts b/packages/components/index.ts index faaffd455..5bf89e823 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -44,6 +44,7 @@ import { IxRate } from '@idux/components/rate' import { IxResult } from '@idux/components/result' import { IxSelect, IxSelectOption, IxSelectOptionGroup } from '@idux/components/select' import { IxSkeleton } from '@idux/components/skeleton' +import { IxSlider } from '@idux/components/slider' import { IxSpace } from '@idux/components/space' import { IxSpin } from '@idux/components/spin' import { IxStatistic } from '@idux/components/statistic' @@ -122,6 +123,7 @@ const components = [ IxSelectOption, IxSelectOptionGroup, IxSkeleton, + IxSlider, IxSpace, IxSpin, IxStatistic, diff --git a/packages/components/input-number/src/types.ts b/packages/components/input-number/src/types.ts index e0fda97ec..402b54bda 100644 --- a/packages/components/input-number/src/types.ts +++ b/packages/components/input-number/src/types.ts @@ -44,4 +44,4 @@ export type InputNumberComponent = DefineComponent< Omit & InputNumberPublicProps, InputNumberBindings > -export type InputNumberInstance = InstanceType> +export type InputNumberInstance = InstanceType> diff --git a/packages/components/input-number/src/useInputNumber.ts b/packages/components/input-number/src/useInputNumber.ts index 4456024da..c25a5e901 100644 --- a/packages/components/input-number/src/useInputNumber.ts +++ b/packages/components/input-number/src/useInputNumber.ts @@ -207,6 +207,6 @@ export function getPrecision(value: number | undefined | null): number { return 0 } - const fraction = String(value).split('.')[1] - return fraction ? fraction.length : 0 + const decimal = String(value).split('.')[1] + return decimal ? decimal.length : 0 } diff --git a/packages/components/slider/__tests__/__snapshots__/slider.spec.ts.snap b/packages/components/slider/__tests__/__snapshots__/slider.spec.ts.snap new file mode 100644 index 000000000..0dbfdd7b4 --- /dev/null +++ b/packages/components/slider/__tests__/__snapshots__/slider.spec.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Slider render work 1`] = ` +"
+
+
+
+
+ +
+ +
+
" +`; diff --git a/packages/components/slider/__tests__/slider.spec.ts b/packages/components/slider/__tests__/slider.spec.ts new file mode 100644 index 000000000..d05680ef8 --- /dev/null +++ b/packages/components/slider/__tests__/slider.spec.ts @@ -0,0 +1,84 @@ +import { mount } from '@vue/test-utils' +import { ref } from 'vue' + +import { renderWork } from '@tests' + +import Slider from '../src/Slider' +import { SliderProps } from '../src/types' + +describe('Slider', () => { + // const SliderMount = (options?: MountingOptions>) => mount(Slider, { ...options }) + + renderWork(Slider, { + props: { value: 30 }, + }) + + test('v-model work', async () => { + const valueRef = ref(1) + const wrapper = mount({ + components: { Slider }, + template: ``, + setup() { + return { valueRef } + }, + }) + const thumb = wrapper.find('.ix-slider-thumb') + + expect(valueRef.value).toBe(1) + expect(getComputedStyle(thumb.element).left).toBe('1%') + + await thumb.trigger('focus') + await thumb.trigger('keydown', { code: 'ArrowUp' }) + + expect(valueRef.value).toBe(2) + expect(getComputedStyle(thumb.element).left).toBe('2%') + }) + + test('range work', () => { + const valueRef = ref([20, 50]) + const wrapper = mount({ + components: { Slider }, + template: ``, + setup() { + return { valueRef } + }, + }) + const thumbs = wrapper.findAll('.ix-slider-thumb') + + expect(valueRef.value).toEqual([20, 50]) + expect(getComputedStyle(thumbs[0].element).left).toBe('20%') + expect(getComputedStyle(thumbs[1].element).left).toBe('50%') + }) + + test('vertical work', () => { + const valueRef = ref(30) + const wrapper = mount({ + components: { Slider }, + template: ``, + setup() { + return { valueRef } + }, + }) + const thumb = wrapper.find('.ix-slider-thumb') + + expect(valueRef.value).toBe(30) + expect(getComputedStyle(thumb.element).left).toBe('') + expect(getComputedStyle(thumb.element).bottom).toBe('30%') + }) + + test('reverse work', () => { + const valueRef = ref(30) + const wrapper = mount({ + components: { Slider }, + template: ``, + setup() { + return { valueRef } + }, + }) + const thumb = wrapper.find('.ix-slider-thumb') + + expect(valueRef.value).toBe(30) + expect(getComputedStyle(thumb.element).left).toBe('') + expect(getComputedStyle(thumb.element).right).toBe('30%') + }) +}) diff --git a/packages/components/slider/demo/Basic.md b/packages/components/slider/demo/Basic.md new file mode 100644 index 000000000..817a7ee7c --- /dev/null +++ b/packages/components/slider/demo/Basic.md @@ -0,0 +1,14 @@ +--- +title: + zh: 基本使用 + en: Basic usage +order: 0 +--- + +## zh + +最简单的用法。 + +## en + +The simplest usage. diff --git a/packages/components/slider/demo/Basic.vue b/packages/components/slider/demo/Basic.vue new file mode 100644 index 000000000..8e0cad6ff --- /dev/null +++ b/packages/components/slider/demo/Basic.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/components/slider/demo/Event.md b/packages/components/slider/demo/Event.md new file mode 100644 index 000000000..81bdf8457 --- /dev/null +++ b/packages/components/slider/demo/Event.md @@ -0,0 +1,14 @@ +--- +title: + zh: 事件 + en: Event +order: 0 +--- + +## zh + +在拖动滑块的过程中会触发 `onInput` 事件,并把改变后的值作为参数传入。 + +当 `Slider` 的值发生改变后,会触发 `onChange` 事件,并把改变后的值作为参数传入,一般情况下,`onChange` 都在 `mouseup` 阶段触发。 + +## en diff --git a/packages/components/slider/demo/Event.vue b/packages/components/slider/demo/Event.vue new file mode 100644 index 000000000..ea65ac1a0 --- /dev/null +++ b/packages/components/slider/demo/Event.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/components/slider/demo/InputNumber.md b/packages/components/slider/demo/InputNumber.md new file mode 100644 index 000000000..9bb59831b --- /dev/null +++ b/packages/components/slider/demo/InputNumber.md @@ -0,0 +1,14 @@ +--- +title: + zh: 显示输入框 + en: show input number +order: 0 +--- + +## zh + +和 [数字输入框](components/input-number/zh) 组件保持同步。 + +## en + +Synchronize with [InputNumber](components/input-number/en) component. diff --git a/packages/components/slider/demo/InputNumber.vue b/packages/components/slider/demo/InputNumber.vue new file mode 100644 index 000000000..3dbe3cae3 --- /dev/null +++ b/packages/components/slider/demo/InputNumber.vue @@ -0,0 +1,33 @@ + + + diff --git a/packages/components/slider/demo/Keyboard.md b/packages/components/slider/demo/Keyboard.md new file mode 100644 index 000000000..b6f6ac590 --- /dev/null +++ b/packages/components/slider/demo/Keyboard.md @@ -0,0 +1,14 @@ +--- +title: + zh: 键盘行为 + en: Keyboard +order: 0 +--- + +## zh + +使用 `keyboard` 属性可以控制键盘行为,当滑块获得焦点后,使用方向键可以控制滑块进行移动。 + +## en + +Control keyboard behavior by `keyboard`. diff --git a/packages/components/slider/demo/Keyboard.vue b/packages/components/slider/demo/Keyboard.vue new file mode 100644 index 000000000..534c5bd1c --- /dev/null +++ b/packages/components/slider/demo/Keyboard.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/components/slider/demo/Marks.md b/packages/components/slider/demo/Marks.md new file mode 100644 index 000000000..943415073 --- /dev/null +++ b/packages/components/slider/demo/Marks.md @@ -0,0 +1,12 @@ +--- +title: + zh: 标签 + en: Marks +order: 0 +--- + +## zh + +使用 `marks` 属性标注分段式滑块,使用 `value` 指定滑块位置。当 `step=null` 时,`Slider` 的可选值仅有 `marks` 标出来的部分。 + +## en diff --git a/packages/components/slider/demo/Marks.vue b/packages/components/slider/demo/Marks.vue new file mode 100644 index 000000000..e6a479aea --- /dev/null +++ b/packages/components/slider/demo/Marks.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/components/slider/demo/Reverse.md b/packages/components/slider/demo/Reverse.md new file mode 100644 index 000000000..68312b951 --- /dev/null +++ b/packages/components/slider/demo/Reverse.md @@ -0,0 +1,12 @@ +--- +title: + zh: 反向 + en: Reverse +order: 0 +--- + +## zh + +使用 `reverse` 将 `Slider` 置反。 + +## en diff --git a/packages/components/slider/demo/Reverse.vue b/packages/components/slider/demo/Reverse.vue new file mode 100644 index 000000000..7b6c18fd4 --- /dev/null +++ b/packages/components/slider/demo/Reverse.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/components/slider/demo/TooltipFormatter.md b/packages/components/slider/demo/TooltipFormatter.md new file mode 100644 index 000000000..f40e33439 --- /dev/null +++ b/packages/components/slider/demo/TooltipFormatter.md @@ -0,0 +1,14 @@ +--- +title: + zh: 自定义提示 + en: Customize tooltip +order: 0 +--- + +## zh + +使用 `tooltipFormatter` 可以格式化 `Tooltip` 的内容。 + +## en + +Use `tooltipFormatter` to format content of `Tooltip`. diff --git a/packages/components/slider/demo/TooltipFormatter.vue b/packages/components/slider/demo/TooltipFormatter.vue new file mode 100644 index 000000000..4650403f3 --- /dev/null +++ b/packages/components/slider/demo/TooltipFormatter.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/components/slider/demo/TooltipVisible.md b/packages/components/slider/demo/TooltipVisible.md new file mode 100644 index 000000000..852e8085e --- /dev/null +++ b/packages/components/slider/demo/TooltipVisible.md @@ -0,0 +1,12 @@ +--- +title: + zh: 控制 ToolTip 的显示 + en: Control visible of ToolTip +order: 0 +--- + +## zh + +当 `tooltipVisible` 为 `true` 时,将始终显示 `ToolTip`;反之则始终不显示,即使在拖动、移入时也是如此。 + +## en diff --git a/packages/components/slider/demo/TooltipVisible.vue b/packages/components/slider/demo/TooltipVisible.vue new file mode 100644 index 000000000..60b397dd5 --- /dev/null +++ b/packages/components/slider/demo/TooltipVisible.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/components/slider/demo/Vertical.md b/packages/components/slider/demo/Vertical.md new file mode 100644 index 000000000..f73700e8b --- /dev/null +++ b/packages/components/slider/demo/Vertical.md @@ -0,0 +1,12 @@ +--- +title: + zh: 垂直 + en: Vertical +order: 0 +--- + +## zh + +使用 `vertical` 设置 `Slider` 为垂直方向。 + +## en diff --git a/packages/components/slider/demo/Vertical.vue b/packages/components/slider/demo/Vertical.vue new file mode 100644 index 000000000..92781dea6 --- /dev/null +++ b/packages/components/slider/demo/Vertical.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/components/slider/docs/Index.en.md b/packages/components/slider/docs/Index.en.md new file mode 100644 index 000000000..e3cb9615c --- /dev/null +++ b/packages/components/slider/docs/Index.en.md @@ -0,0 +1,31 @@ +--- +category: components +type: Data Entry +title: Slider +subtitle: +order: 0 +--- + + + +## API + +### IxSlider + +#### SliderProps + +| Name | Description | Type | Default | Global Config | Remark | +| --- | --- | --- | --- | --- | --- | +| - | - | - | - | ✅ | - | + +#### SliderSlots + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | + +#### SliderMethods + +| Name | Description | Parameter Type | Remark | +| --- | --- | --- | --- | +| - | - | - | - | diff --git a/packages/components/slider/docs/Index.zh.md b/packages/components/slider/docs/Index.zh.md new file mode 100644 index 000000000..6a9857a55 --- /dev/null +++ b/packages/components/slider/docs/Index.zh.md @@ -0,0 +1,39 @@ +--- +category: components +type: 数据录入 +title: Slider +subtitle: 滑动输入条 +order: 0 +--- + +滑动型输入器,展示当前值和可选范围。 + +## 何时使用 + +当用户需要在数值区间/自定义区间内进行选择时,可为连续或离散值。 + +## API + +### IxSlider + +#### SliderProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `v-model:value` | 绑定值 | `number \| [number, number]` | `0 \| [0, 0]` | - | - | +| `control` | 控件控制器 | `string \| number \| AbstractControl` | - | - | 配合 `@idux/cdk/forms` 使用, 参考 [Form](/components/form/zh) | +| `disabled` | 设置禁用状态 | `boolean` | `false` | - | - | +| `dots` | 显示间断点 | `boolean` | `false` | - | `marks` 间断点会始终显示 | +|`marks`|刻度标记,`key` 的类型必须为 `number` 且取值在闭区间 `[min, max]` 内,每个标签可以单独设置样式|`object`|-|-|`{ number: string \| VNode } or { number: { style: object, label: string \| VNode } } or { number: () => VNode }` | +| `keyboard` | 启用键盘行为 | `boolean` | `true` | - | - | +| `max` | 最大值 | `number` | `100` | - | - | +| `min` | 最小值 | `number` | `0` | - | - | +| `range` | 设置范围选择模式 | `boolean` | `false` | - | 双滑块模式 | +| `reverse` | 设置反向坐标轴 | `boolean` | `false` | - | - | +| `step` | 步长 | `number` | `1` | - | 要大于0 | +| `tooltipFormatter` | 格式化 `tooltip` 内容 | `(value: number) => VNode \| string \| number` | - | - | - | +| `tooltipPlacement` | 设置 `tooltip` 显示位置 | `string` | `auto` | - | 参考 [Tooltip](/components/tooltip/) | +| `tooltipVisible` | 设置 `tooltip` 显示状态 | `boolean` | - | - | `tooltip` 默认为 `hover` 和拖拽时显示,`tooltipVisible` 设置为 `true` 则始终显示,反之则始终不显示 | +| `vertical` | 设置垂直状态 | `boolean` | `false` | - | - | +| `onInput` | 拖动滑块时触发 | `(value: number) => void` | - | - | - | +| `onChange` | `Slider` 的值改变后触发(`mouseup` 阶段触发) | `(value: number) => void` | - | - | - | diff --git a/packages/components/slider/index.ts b/packages/components/slider/index.ts new file mode 100644 index 000000000..806f06c7a --- /dev/null +++ b/packages/components/slider/index.ts @@ -0,0 +1,16 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderComponent } from './src/types' + +import Slider from './src/Slider' + +const IxSlider = Slider as unknown as SliderComponent + +export { IxSlider } + +export type { SliderInstance, SliderPublicProps as SliderProps } from './src/types' diff --git a/packages/components/slider/src/Marks.tsx b/packages/components/slider/src/Marks.tsx new file mode 100644 index 000000000..5486f2505 --- /dev/null +++ b/packages/components/slider/src/Marks.tsx @@ -0,0 +1,115 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +/* eslint-disable indent */ + +import type { SliderContext } from './token' +import type { SliderMarksProps, SliderProps } from './types' +import type { CSSProperties, VNode } from 'vue' + +import { computed, defineComponent, inject, isVNode, normalizeStyle } from 'vue' + +import { isFunction, isPlainObject, isString, isUndefined } from 'lodash-es' + +import { callEmit } from '@idux/cdk/utils' + +import { sliderStartDirection, sliderToken } from './token' +import { sliderMarksProps } from './types' + +export default defineComponent({ + name: 'IxSliderMarks', + props: sliderMarksProps, + setup(props) { + const { values, marks, max, min, prefixCls, range, reverse, vertical } = inject(sliderToken)! + const mergedPrefixCls = computed(() => `${prefixCls.value}-mark`) + return () => { + return ( +
+ {renderMarks( + props, + values.value, + marks.value, + max.value, + min.value, + mergedPrefixCls.value, + range.value, + reverse.value, + vertical.value, + )} +
+ ) + } + }, +}) + +function renderMarks( + props: SliderMarksProps, + values: number[], + marks: SliderProps['marks'], + max: number, + min: number, + prefixCls: string, + range: boolean, + reverse: boolean, + vertical: boolean, +) { + if (isUndefined(marks)) { + return + } + + const width = max - min + return Object.keys(marks) + .map(parseFloat) + .sort((a, b) => a - b) + .map(offset => { + const markValue = marks[offset] + const isObj = isPlainObject(markValue) + let markLabel: string | VNode | undefined = undefined + if (isString(markValue) || isVNode(markValue)) { + markLabel = markValue + } else if (isFunction(markValue)) { + markLabel = markValue() + } else if (isObj) { + markLabel = markValue.label! + } + + if (isUndefined(markLabel)) { + return null + } + + const isActived = range ? !(offset < values[0] || offset > values[1]) : offset <= values[0] + + const classes = { + [`${prefixCls}-label`]: true, + [`${prefixCls}-label-active`]: isActived, + } + + const style = vertical + ? { + marginBottom: '-50%', + [reverse ? sliderStartDirection.ttb : sliderStartDirection.btt]: `${((offset - min) / width) * 100}%`, + } + : { + transform: `translateX(${reverse ? `50%` : `-50%`})`, + [reverse ? sliderStartDirection.rtl : sliderStartDirection.ltr]: `${((offset - min) / width) * 100}%`, + } + + const markStyle = isObj ? normalizeStyle([style, (markValue as { style?: CSSProperties })?.style]) : style + const handleMarkClick = (evt: MouseEvent | TouchEvent) => callEmit(props.onClickMark, evt, offset) + return ( + + {markLabel} + + ) + }) +} diff --git a/packages/components/slider/src/Slider.tsx b/packages/components/slider/src/Slider.tsx new file mode 100644 index 000000000..9362bf177 --- /dev/null +++ b/packages/components/slider/src/Slider.tsx @@ -0,0 +1,132 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderContext } from './token' +import type { SliderProps } from './types' +import type { Ref } from 'vue' + +import { computed, defineComponent, provide, toRefs } from 'vue' + +import { isUndefined } from 'lodash-es' + +import { NoopFunction } from '@idux/cdk/utils' +import { useGlobalConfig } from '@idux/components/config' + +import IxSliderMarks from './Marks' +import IxSliderSteps from './Steps' +import IxSliderThumb from './Thumb' +import { sliderToken } from './token' +import { sliderProps } from './types' +import { useSlider } from './useSlider' + +export default defineComponent({ + name: 'IxSlider', + props: sliderProps, + setup(props, { expose }) { + const common = useGlobalConfig('common') + const { + direction, + isDisabled, + isDragging, + valuesRef, + railRef, + setThumbRefs, + handleMouseDown, + handleMouseUp, + handleKeyDown, + handleMarkClick, + focus, + blur, + } = useSlider(props) + const { trackStyle } = useTrack(props, valuesRef, direction) + + const mergedPrefixCls = computed(() => `${common.prefixCls}-slider`) + const thumbs = computed(() => valuesRef.value.slice(0, props.range ? 2 : 1)) + const thumbTransformOfStyle = computed(() => { + if (props.vertical) { + return props.reverse ? `translateY(-50%)` : `translateY(50%)` + } + return props.reverse ? `translateX(50%)` : `translateX(-50%)` + }) + + const classes = computed(() => { + const prefixCls = mergedPrefixCls.value + return { + [prefixCls]: true, + [`${prefixCls}-disabled`]: isDisabled.value, + [`${prefixCls}-vertical`]: props.vertical, + [`${prefixCls}-with-marks`]: !isUndefined(props.marks), + } + }) + + provide(sliderToken, { + ...toRefs(props), + direction, + dragging: isDragging, + values: valuesRef, + disabled: isDisabled, + prefixCls: mergedPrefixCls, + }) + + expose({ focus, blur }) + + return () => { + return ( +
+
+
+ + {thumbs.value.map((val, index) => { + const position = ((val - props.min) / (props.max - props.min)) * 100 + return ( + + ) + })} + +
+ ) + } + }, +}) + +function useTrack(props: SliderProps, values: Ref, direction: Ref) { + const maxValue = computed(() => Math.max(...values.value)) + const minValue = computed(() => Math.min(...values.value)) + const trackStyle = computed(() => { + return { + [direction.value]: props.range ? `${((minValue.value - props.min) / (props.max - props.min)) * 100}%` : '0%', + [props.vertical ? 'height' : 'width']: props.range + ? `${((maxValue.value - minValue.value) / (props.max - props.min)) * 100}%` + : `${((+values.value[0] - props.min) / (props.max - props.min)) * 100}%`, + } + }) + + return { trackStyle } +} diff --git a/packages/components/slider/src/Steps.tsx b/packages/components/slider/src/Steps.tsx new file mode 100644 index 000000000..2804a1e14 --- /dev/null +++ b/packages/components/slider/src/Steps.tsx @@ -0,0 +1,86 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderContext } from './token' +import type { SliderProps } from './types' + +import { defineComponent, inject } from 'vue' + +import { sliderStartDirection, sliderToken } from './token' + +export default defineComponent({ + name: 'IxSliderSteps', + setup() { + const { values, dots, marks, max, min, prefixCls, range, reverse, step, vertical } = + inject(sliderToken)! + + return () => { + return ( +
+ {renderDots( + values.value, + dots.value, + marks.value, + max.value, + min.value, + prefixCls.value, + range.value, + reverse.value, + step.value, + vertical.value, + )} +
+ ) + } + }, +}) + +function renderDots( + values: number[], + dots: boolean, + marks: SliderProps['marks'], + max: number, + min: number, + prefixCls: string, + range: boolean, + reverse: boolean, + step: number | null, + vertical: boolean, +) { + const width = max - min + return getOffsets(dots, marks, max, min, step).map(offset => { + const pos = `${(Math.abs(offset - min) / width) * 100}%` + const isActived = range ? !(offset < values[0] || offset > values[1]) : offset <= values[0] + const style = vertical + ? { [reverse ? sliderStartDirection.ttb : sliderStartDirection.btt]: pos } + : { [reverse ? sliderStartDirection.rtl : sliderStartDirection.ltr]: pos } + + const classes = { + [`${prefixCls}-dot`]: true, + [`${prefixCls}-dot-active`]: isActived, + [`${prefixCls}-dot-reverse`]: reverse, + } + + return + }) +} + +function getOffsets(dots: boolean, marks: SliderProps['marks'], max: number, min: number, step: number | null) { + const points = Object.keys(marks ?? {}) + .map(parseFloat) + .sort((a, b) => a - b) + + if (dots && step) { + for (let i = min; i <= max; i += step) { + if (points.indexOf(i) === -1) { + points.push(i) + } + } + } + + return points +} diff --git a/packages/components/slider/src/Thumb.tsx b/packages/components/slider/src/Thumb.tsx new file mode 100644 index 000000000..44fa2313a --- /dev/null +++ b/packages/components/slider/src/Thumb.tsx @@ -0,0 +1,99 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderContext } from './token' +import type { TooltipInstance } from '@idux/components/tooltip' + +import { computed, defineComponent, inject, onUpdated, ref } from 'vue' + +import { isFunction } from 'lodash-es' + +import { NoopFunction, callEmit } from '@idux/cdk/utils' +import { IxTooltip } from '@idux/components/tooltip' + +import { sliderToken } from './token' +import { sliderThumbProps } from './types' + +export default defineComponent({ + name: 'IxSliderThumb', + inheritAttrs: false, + props: sliderThumbProps, + setup(props, { attrs, expose }) { + const { disabled, dragging, prefixCls, tooltipVisible, tooltipPlacement, tooltipFormatter } = + inject(sliderToken)! + + const mergedTooltipVisible = ref(tooltipVisible.value ?? false) + const isHovering = ref(false) + const tooltipRef = ref(null) + const thumbRef = ref(null) + const mergedPrefixCls = computed(() => `${prefixCls.value}-thumb`) + + const showTooltip = () => tooltipVisible.value !== false && (mergedTooltipVisible.value = true) + const hideTooltip = () => tooltipVisible.value !== true && (mergedTooltipVisible.value = false) + + onUpdated(() => mergedTooltipVisible.value && tooltipRef.value?.updatePopper()) + + function handleMouseEnter() { + isHovering.value = true + showTooltip() + } + + function handleMouseLeave() { + isHovering.value = false + if (!dragging.value) { + hideTooltip() + } + } + + function handleFocus(evt: FocusEvent) { + showTooltip() + if (!dragging.value) { + callEmit(props.onFocus, evt) + } + } + + function handleBlur(evt: FocusEvent) { + hideTooltip() + if (!dragging.value) { + callEmit(props.onBlur, evt) + } + } + + expose({ + tooltipRef, + thumbRef, + isHovering, + showTooltip, + hideTooltip, + }) + + return () => { + return ( + + isFunction(tooltipFormatter.value) ? tooltipFormatter.value(props.value!) : {props.value}, + }} + > +
+
+ ) + } + }, +}) diff --git a/packages/components/slider/src/marks.tsx b/packages/components/slider/src/marks.tsx new file mode 100644 index 000000000..5486f2505 --- /dev/null +++ b/packages/components/slider/src/marks.tsx @@ -0,0 +1,115 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +/* eslint-disable indent */ + +import type { SliderContext } from './token' +import type { SliderMarksProps, SliderProps } from './types' +import type { CSSProperties, VNode } from 'vue' + +import { computed, defineComponent, inject, isVNode, normalizeStyle } from 'vue' + +import { isFunction, isPlainObject, isString, isUndefined } from 'lodash-es' + +import { callEmit } from '@idux/cdk/utils' + +import { sliderStartDirection, sliderToken } from './token' +import { sliderMarksProps } from './types' + +export default defineComponent({ + name: 'IxSliderMarks', + props: sliderMarksProps, + setup(props) { + const { values, marks, max, min, prefixCls, range, reverse, vertical } = inject(sliderToken)! + const mergedPrefixCls = computed(() => `${prefixCls.value}-mark`) + return () => { + return ( +
+ {renderMarks( + props, + values.value, + marks.value, + max.value, + min.value, + mergedPrefixCls.value, + range.value, + reverse.value, + vertical.value, + )} +
+ ) + } + }, +}) + +function renderMarks( + props: SliderMarksProps, + values: number[], + marks: SliderProps['marks'], + max: number, + min: number, + prefixCls: string, + range: boolean, + reverse: boolean, + vertical: boolean, +) { + if (isUndefined(marks)) { + return + } + + const width = max - min + return Object.keys(marks) + .map(parseFloat) + .sort((a, b) => a - b) + .map(offset => { + const markValue = marks[offset] + const isObj = isPlainObject(markValue) + let markLabel: string | VNode | undefined = undefined + if (isString(markValue) || isVNode(markValue)) { + markLabel = markValue + } else if (isFunction(markValue)) { + markLabel = markValue() + } else if (isObj) { + markLabel = markValue.label! + } + + if (isUndefined(markLabel)) { + return null + } + + const isActived = range ? !(offset < values[0] || offset > values[1]) : offset <= values[0] + + const classes = { + [`${prefixCls}-label`]: true, + [`${prefixCls}-label-active`]: isActived, + } + + const style = vertical + ? { + marginBottom: '-50%', + [reverse ? sliderStartDirection.ttb : sliderStartDirection.btt]: `${((offset - min) / width) * 100}%`, + } + : { + transform: `translateX(${reverse ? `50%` : `-50%`})`, + [reverse ? sliderStartDirection.rtl : sliderStartDirection.ltr]: `${((offset - min) / width) * 100}%`, + } + + const markStyle = isObj ? normalizeStyle([style, (markValue as { style?: CSSProperties })?.style]) : style + const handleMarkClick = (evt: MouseEvent | TouchEvent) => callEmit(props.onClickMark, evt, offset) + return ( + + {markLabel} + + ) + }) +} diff --git a/packages/components/slider/src/token.ts b/packages/components/slider/src/token.ts new file mode 100644 index 000000000..e5434e72b --- /dev/null +++ b/packages/components/slider/src/token.ts @@ -0,0 +1,39 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderProps } from './types' +import type { ComputedRef, InjectionKey, Ref } from 'vue' + +export const sliderStartDirection = { + ltr: 'left', + rtl: 'right', + btt: 'bottom', + ttb: 'top', +} as const + +export type ValueOf = T[keyof T] + +export interface SliderContext { + values: Ref + direction: ComputedRef> + disabled: ComputedRef + dragging: Ref + dots: Ref + marks: Ref + max: Ref + min: Ref + prefixCls: ComputedRef + range: Ref + reverse: Ref + step: Ref + tooltipFormatter: Ref + tooltipPlacement: Ref + tooltipVisible: Ref + vertical: Ref +} + +export const sliderToken: InjectionKey = Symbol('sliderToken') diff --git a/packages/components/slider/src/types.ts b/packages/components/slider/src/types.ts new file mode 100644 index 000000000..209ccca1d --- /dev/null +++ b/packages/components/slider/src/types.ts @@ -0,0 +1,91 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { IxInnerPropTypes, IxPublicPropTypes } from '@idux/cdk/utils' +import type { TooltipInstance } from '@idux/components/tooltip' +import type { CSSProperties, DefineComponent, HTMLAttributes, VNode } from 'vue' + +import { IxPropTypes } from '@idux/cdk/utils' +import { ɵOverlayPlacementDef } from '@idux/components/_private' + +// slider +export const sliderProps = { + value: IxPropTypes.oneOfType([IxPropTypes.number, IxPropTypes.arrayOf(IxPropTypes.number)]).def(0), + disabled: IxPropTypes.bool.def(false), + dots: IxPropTypes.bool.def(false), + keyboard: IxPropTypes.bool.def(true), + marks: + IxPropTypes.object< + Record string | VNode)> + >(), + max: IxPropTypes.number.def(100), + min: IxPropTypes.number.def(0), + range: IxPropTypes.bool.def(false), + reverse: IxPropTypes.bool.def(false), + step: IxPropTypes.oneOfType([IxPropTypes.number]).def(1), + tooltipFormatter: IxPropTypes.func<(value: number) => VNode | string | number>(), + tooltipPlacement: ɵOverlayPlacementDef, + tooltipVisible: IxPropTypes.bool, + vertical: IxPropTypes.bool.def(false), + + // events + 'onUpdate:value': IxPropTypes.emit<(value: number | number[]) => void>(), + onChange: IxPropTypes.emit<(value: number | number[]) => void>(), + onInput: IxPropTypes.emit<(value: number | number[]) => void>(), + onFocus: IxPropTypes.emit<(evt: FocusEvent) => void>(), + onBlur: IxPropTypes.emit<(evt: FocusEvent) => void>(), +} + +export interface SliderBindings { + focus: (options?: FocusOptions) => void + blur: () => void +} + +export type SliderProps = IxInnerPropTypes +export type SliderPublicProps = IxPublicPropTypes +export type SliderComponent = DefineComponent< + Omit & SliderPublicProps, + SliderBindings +> +export type SliderInstance = InstanceType> + +// slider thumb +export const sliderThumbProps = { + value: IxPropTypes.number, + + // events + onFocus: IxPropTypes.emit<(evt: FocusEvent) => void>(), + onBlur: IxPropTypes.emit<(evt: FocusEvent) => void>(), +} + +export interface SliderThumbBindings { + tooltipRef: TooltipInstance + thumbRef: HTMLElement + isHovering: boolean + showTooltip: () => void + hideTooltip: () => void +} + +export type SliderThumbProps = IxInnerPropTypes +export type SliderThumbPublicProps = IxPublicPropTypes +export type SliderThumbComponent = DefineComponent< + Omit & SliderThumbPublicProps, + SliderThumbBindings +> +export type SliderThumbInstance = InstanceType> + +// slider marks +export const sliderMarksProps = { + onClickMark: IxPropTypes.emit<(evt: MouseEvent | TouchEvent, markValue: number) => void>(), +} + +export type SliderMarksProps = IxInnerPropTypes +export type SliderMarksPublicProps = IxPublicPropTypes +export type SliderMarksComponent = DefineComponent< + Omit & SliderMarksPublicProps +> +export type SliderMarksInstance = InstanceType> diff --git a/packages/components/slider/src/useSlider.ts b/packages/components/slider/src/useSlider.ts new file mode 100644 index 000000000..b9d05d25e --- /dev/null +++ b/packages/components/slider/src/useSlider.ts @@ -0,0 +1,349 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { SliderContext } from './token' +import type { SliderProps, SliderThumbInstance } from './types' +import type { ComputedRef, Ref } from 'vue' + +import { computed, nextTick, ref, toRaw, watch } from 'vue' + +import { useValueAccessor } from '@idux/cdk/forms' +import { Logger, callEmit, getMouseClientXY, isNumeric, off, on } from '@idux/cdk/utils' +import { useFormItemRegister } from '@idux/components/form' +import { useFormElement } from '@idux/components/utils' + +import { sliderStartDirection } from './token' + +export type Nullable = T | null + +export interface SliderBindings { + valuesRef: Ref + thumbListRef: Ref + railRef: Ref> + direction: SliderContext['direction'] + isDisabled: ComputedRef + isDragging: Ref + focus: (options?: FocusOptions | undefined) => void + blur: () => void + setThumbRefs: (index: number) => (el: unknown) => void + handleMouseDown: (evt: MouseEvent | TouchEvent) => void + handleMouseUp: () => void + handleKeyDown: (evt: KeyboardEvent) => void + handleMarkClick: (evt: MouseEvent | TouchEvent, newValue: number) => void +} + +export function useSlider(props: SliderProps): SliderBindings { + const { elementRef, focus, blur } = useFormElement() + const { accessor, control } = useValueAccessor() + useFormItemRegister(control) + + const valuesRef = ref([props.min, props.min]) + const thumbListRef = ref([]) + const activeIndex = ref(-1) + const railRef = ref>(null) + const isDragging = ref(false) + const isDisabled = computed(() => accessor?.disabled.value) + + const precision = computed(() => { + const precisions = [props.min, props.max, props.step].map(num => { + const decimal = `${num}`.split('.')[1] + return decimal ? decimal.length : 0 + }) + return Math.max(...precisions) + }) + + const stepPrecision = computed(() => { + const decimal = `${props.step}`.split('.')[1] + return decimal ? decimal.length : 0 + }) + + const direction = computed(() => { + if (props.vertical) { + return props.reverse ? sliderStartDirection.ttb : sliderStartDirection.btt + } + return props.reverse ? sliderStartDirection.rtl : sliderStartDirection.ltr + }) + + function setThumbRefs(index: number) { + return (el: unknown) => { + if (index === 0) { + elementRef.value = (el as SliderThumbInstance)?.thumbRef + } + + thumbListRef.value[index] = el as SliderThumbInstance + } + } + + function handleMouseDown(evt: MouseEvent | TouchEvent) { + const index = thumbListRef.value.findIndex(v => v?.thumbRef === evt.target) + if (index !== -1) { + // avoid triggering scrolling on touch + evt.preventDefault() + activeIndex.value = index + thumbListRef.value[index]?.thumbRef?.focus() + startDragging() + } else { + const newValue = calcValueByMouseEvent(evt) + setActiveIndex(newValue) + updateModelValue(newValue) + } + } + + function handleKeyDown(evt: KeyboardEvent) { + if (props.keyboard) { + const index = thumbListRef.value.findIndex(v => v?.thumbRef === evt.target) + if (index !== -1) { + let value: Nullable = null + if (evt.code === 'ArrowUp' || evt.code === 'ArrowRight') { + activeIndex.value = index + value = incValueByActiveIndex(+1) + } else if (evt.code === 'ArrowDown' || evt.code === 'ArrowLeft') { + activeIndex.value = index + value = incValueByActiveIndex(-1) + } + + if (value !== null) { + evt.preventDefault() + updateModelValue(value) + checkAcross(value) + } + } + } + } + + function handleMarkClick(evt: MouseEvent | TouchEvent, newValue: number) { + evt.stopPropagation() + setActiveIndex(newValue) + updateModelValue(newValue) + } + + function startDragging() { + if (!isDragging.value) { + isDragging.value = true + + on(window, 'mousemove', handleMouseMove) + on(window, 'touchmove', handleMouseMove) + on(window, 'mouseup', handleMouseUpOnDocument) + on(window, 'touchend', handleMouseUpOnDocument) + on(window, 'contextmenu', handleMouseUpOnDocument) + } + } + + function stopDragging() { + if (isDragging.value) { + const activeThumbRef = thumbListRef.value[activeIndex.value] + isDragging.value = false + activeIndex.value = -1 + if (!activeThumbRef?.isHovering) { + nextTick(() => activeThumbRef?.hideTooltip()) + } + + off(window, 'mousemove', handleMouseMove) + off(window, 'touchmove', handleMouseMove) + off(window, 'mouseup', handleMouseUpOnDocument) + off(window, 'touchend', handleMouseUpOnDocument) + off(window, 'contextmenu', handleMouseUpOnDocument) + } + } + + function handleMouseMove(evt: MouseEvent | TouchEvent) { + if (!isDragging.value) { + stopDragging() + return + } + + const newValue = calcValueByMouseEvent(evt) + updateModelValue(newValue) + checkAcross(newValue) + } + + function handleMouseUpOnDocument() { + stopDragging() + callEmit(props.onChange, toRaw(valuesRef.value)) + } + + function handleMouseUp() { + const { value: index } = activeIndex + if (index !== -1) { + thumbListRef.value[index]?.thumbRef?.focus() + } + } + + function updateModelValue(newValue: number) { + const { value: index } = activeIndex + if (valuesRef.value[index] !== newValue) { + const newValues = valuesRef.value.slice() + newValues[index] = newValue + const modelValue = props.range ? newValues : newValues[0] + accessor.setValue(modelValue) + isDragging.value ? callEmit(props.onInput, modelValue) : callEmit(props.onChange, modelValue) + + // sync position of thumb + nextTick(() => setValues()) + } + } + + function setActiveIndex(newValue: number) { + const { value: oldValues } = valuesRef + activeIndex.value = props.range + ? Math.abs(oldValues[0] - newValue) < Math.abs(oldValues[1] - newValue) + ? 0 + : 1 + : 0 + } + + function checkAcross(value: number) { + if (props.range) { + const { value: thumbValues } = valuesRef + let newIndex = activeIndex.value + if (value > thumbValues[1]) { + newIndex = 1 + } else if (value < thumbValues[0]) { + newIndex = 0 + } + + if (newIndex !== activeIndex.value) { + activeIndex.value = newIndex + thumbListRef.value[activeIndex.value]?.thumbRef?.focus() + } + } + } + + function calcValueByMouseEvent(evt: MouseEvent | TouchEvent) { + const client = getMouseClientXY(evt) + const railRect = railRef.value!.getBoundingClientRect() + let percentage: number + if (props.vertical) { + percentage = (railRect.bottom - client.clientY) / railRect.height + } else { + percentage = (client.clientX - railRect.left) / railRect.width + } + + if (props.reverse) { + percentage = 1 - percentage + } + + return calcValueByStep(percentage * (props.max - props.min)) + } + + function calcValueByStep(value: number) { + value = Math.max(props.min, Math.min(props.max, value)) + const marks = props.marks ?? {} + const points = Object.keys(marks).map(parseFloat) + + if (props.step !== null) { + // convert to integer for calculation to ensure decimal accuracy + const convertNum = Math.pow(10, precision.value) + const totalSteps = Math.floor((props.max * convertNum - props.min * convertNum) / (props.step * convertNum)) + const steps = Math.min((value - props.min) / props.step, totalSteps) + const closestStepValue = Math.round(steps) * props.step + props.min + points.push(parseFloat(closestStepValue.toFixed(precision.value))) + } + + const diffs = points.map(point => Math.abs(value - point)) + const index = diffs.indexOf(Math.min(...diffs)) + return points[index] ?? props.min + } + + function incValueByActiveIndex(flag: number) { + if (props.step !== null) { + const step = flag < 0 ? -props.step : +props.step + const currValue = valuesRef.value[activeIndex.value] + return Math.max(props.min, Math.min(props.max, parseFloat((currValue + step).toFixed(stepPrecision.value)))) + } + + return props.min + } + + watch( + accessor.valueRef, + (val, oldVal) => { + if (isDragging.value || (Array.isArray(val) && Array.isArray(oldVal) && val.every((v, i) => v === oldVal[i]))) { + return + } + setValues() + }, + { immediate: true }, + ) + + watch( + () => [props.max, props.min, props.range, props.step], + () => { + setValues() + }, + ) + + function setValues() { + if (props.min > props.max) { + if (__DEV__) { + Logger.error('components/slider', 'min should not be greater than max.') + } + return + } + + if (__DEV__ && props.step !== null && props.step <= 0) { + Logger.error('components/slider', `step(${props.step}) should be greater than 0.`) + } + + const { value: modelValue } = accessor.valueRef + let val: number[] + if (props.range) { + if (!Array.isArray(modelValue)) { + if (__DEV__) { + Logger.error('components/slider', 'value should be [number, number] in range mode.') + } + return + } + + val = [modelValue[0] ?? props.min, modelValue[1] ?? props.min] + } else { + if (!isNumeric(modelValue)) { + if (__DEV__) { + Logger.error('components/slider', 'value should be a number.') + } + return + } + + val = [modelValue as number] + } + + const newVal = val + .map(num => { + if (!isNumeric(num)) { + return props.min + } + + return calcValueByStep(num) + }) + .sort((a, b) => a - b) // order + + // When the legal value is not equal to the modelValue, update modelValue + if (val.every((v, i) => v !== newVal[i])) { + const modelValue = props.range ? newVal : newVal[0] + callEmit(props.onChange, modelValue) + accessor.setValue(modelValue) + } + + valuesRef.value = newVal + } + + return { + valuesRef, + thumbListRef, + railRef, + direction, + isDisabled, + isDragging, + focus, + blur, + setThumbRefs, + handleMouseDown, + handleMouseUp, + handleKeyDown, + handleMarkClick, + } +} diff --git a/packages/components/slider/style/index.less b/packages/components/slider/style/index.less new file mode 100644 index 000000000..e6a2b6f40 --- /dev/null +++ b/packages/components/slider/style/index.less @@ -0,0 +1,187 @@ +@import './themes/default.less'; + +.slider-rail() { + position: absolute; + height: 4px; + border-radius: 2px; + transition: background-color @transition-duration-slow; +} + +.@{slider-prefix} { + position: relative; + box-sizing: border-box; + height: 12px; + margin: 10px 6px; + padding: 4px 0; + cursor: pointer; + touch-action: none; + + &:hover &-rail { + background-color: @slider-rail-hover-bg; + } + + &:hover &-track { + background-color: @slider-track-hover-bg; + } + + &-rail { + .slider-rail(); + + width: 100%; + background-color: @slider-rail-bg; + } + + &-track { + .slider-rail(); + + background-color: @slider-track-bg; + } + + &-step { + position: absolute; + width: 100%; + height: 4px; + background: 0 0; + } + + &-dot { + position: absolute; + top: -2px; + width: 8px; + height: 8px; + margin-left: -4px; + background-color: @slider-dot-bg; + border: @slider-dot-border; + border-radius: 50%; + cursor: pointer; + + &:first-child, + &:last-child { + margin-left: -4px; + } + + &-active { + border-color: @slider-dot-active-border-color; + } + } + + &-thumb { + position: absolute; + box-sizing: border-box; + width: 14px; + height: 14px; + margin-top: -5px; + background-color: @slider-thumb-bg; + border: @slider-thumb-border; + border-radius: 50%; + cursor: pointer; + transition: @slider-thumb-transition; + + &:hover { + border-color: @slider-thumb-hover-border-color; + } + + &:focus { + outline: none; + border-color: @slider-thumb-focus-border-color; + box-shadow: @slider-thumb-focus-box-shadow; + } + } + + &-mark { + position: absolute; + top: 14px; + left: 0; + width: 100%; + font-size: 14px; + + &-label { + position: absolute; + display: inline-block; + color: @slider-marks-label-color; + text-align: center; + word-break: keep-all; + cursor: pointer; + user-select: none; + + &-active { + color: @slider-marks-label-active-color; + } + } + } + + &-with-marks { + margin-bottom: 44px; + } + + &-vertical { + width: 12px; + height: 100%; + margin: 6px 10px; + padding: 0 4px; + + & .@{slider-prefix}-rail { + width: 4px; + height: 100%; + } + + & .@{slider-prefix}-track { + width: 4px; + } + + & .@{slider-prefix}-step { + width: 4px; + height: 100%; + } + + & .@{slider-prefix}-thumb { + margin-top: -6px; + margin-left: -5px; + } + + & .@{slider-prefix}-dot { + top: auto; + left: 2px; + margin-bottom: -4px; + } + + & .@{slider-prefix}-mark { + top: 0; + left: 12px; + width: 18px; + height: 100%; + + &-label { + left: 4px; + white-space: nowrap; + } + } + } + + &-disabled { + cursor: not-allowed; + + & .@{slider-prefix}-track { + background-color: @slider-track-disabled-bg !important; + } + + & .@{slider-prefix}-thumb { + background-color: @slider-thumb-disabled-bg; + border-color: @slider-thumb-disabled-border-color !important; + box-shadow: none; + cursor: not-allowed; + } + + & .@{slider-prefix}-dot { + cursor: not-allowed; + + &-active { + border-color: @slider-dot-disabled-active-border-color !important; + } + } + + & .@{slider-prefix}-mark { + cursor: not-allowed; + } + } +} diff --git a/packages/components/slider/style/index.ts b/packages/components/slider/style/index.ts new file mode 100644 index 000000000..9b1835dc1 --- /dev/null +++ b/packages/components/slider/style/index.ts @@ -0,0 +1,4 @@ +import '../../style/index.less' +import './index.less' + +// style dependencies diff --git a/packages/components/slider/style/themes/default.less b/packages/components/slider/style/themes/default.less new file mode 100644 index 000000000..e42cdd1e8 --- /dev/null +++ b/packages/components/slider/style/themes/default.less @@ -0,0 +1,31 @@ +@import '../../../style/themes/default.less'; + +@slider-marks-label-color: rgba(0, 0, 0, 0.451); + +@slider-rail-bg: @background-color-base; +@slider-track-bg: #91d5ff; +@slider-thumb-bg: @color-white; +@slider-dot-bg: @color-white; + +@slider-dot-border: 2px solid #f0f0f0; +@slider-thumb-border: 2px solid #91d5ff; + +@slider-rail-hover-bg: #e1e1e1; +@slider-track-hover-bg: #69c0ff; +@slider-thumb-hover-border-color: #46a6ff; + +@slider-thumb-focus-border-color: #46a6ff; +@slider-thumb-focus-box-shadow: 0 0 0 5px fade(#46a6ff, 12%); + +@slider-dot-active-border-color: #8cc8ff; +@slider-marks-label-active-color: rgba(0, 0, 0, 0.851); + +@slider-track-disabled-bg: rgba(0, 0, 0, 0.251); +@slider-thumb-disabled-bg: @color-white; +@slider-thumb-disabled-border-color: rgba(0, 0, 0, 0.251); +@slider-dot-disabled-active-border-color: rgba(0, 0, 0, 0.251); + +@slider-thumb-transition: border-color @transition-duration-base, + box-shadow @transition-duration-base * 2, + transform @transition-duration-base + cubic-bezier(.18,.89,.32,1.28); diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less index 8da53b83a..7d01aa6ca 100644 --- a/packages/components/style/variable/prefix.less +++ b/packages/components/style/variable/prefix.less @@ -55,6 +55,7 @@ @rate-prefix: ~'@{idux-prefix}-rate'; @select-prefix: ~'@{idux-prefix}-select'; @select-option-prefix: ~'@{idux-prefix}-select-option'; +@slider-prefix: ~'@{idux-prefix}-slider'; @switch-prefix: ~'@{idux-prefix}-switch'; @textarea-prefix: ~'@{idux-prefix}-textarea'; @time-picker-prefix: ~'@{idux-prefix}-time-picker';