@nami-ui/use-slider
TypeScript icon, indicating that this package has built-in type declarations

0.0.5 • Public • Published

id: use-slider title: useSlider subtitle: 滑动交互

提供一套滑动交互逻辑,可以用于实现大多数的滑动组件,类似 滑块 或 旋纽 等。

一个完整的 slider 应至少包含如下三个元素:

  • 容器(root):包含其它所有元素,响应指针、滚轮及键盘事件,可以获取焦点;
  • 滑轨(rail):用于限定滑块的滑动区域;
  • 滑块(thumb):指示当前滑动位置及滑动状态。

具体可查看下面示例的代码。

示例

横向滑动选择器

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    maxWidth: 300,
    height: 40,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    left: 20,
    right: 20,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(-20px, 0)',
    position: 'absolute',
    top: 0,

    // TODO: 不知道这样性能怎么样,有时间可以测试一下
    left: (props) => props.value * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([0])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ px }) => px,
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

垂直滑动选择器

将横向滑动选择器改为垂直方向非常简单,只需要在 moving 中将 px 改为 py,剩下的的就都是样式上的调整:

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    width: 40,
    height: 300,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 20,
    bottom: 20,
    left: 0,
    right: 0,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(0, -20px)',
    position: 'absolute',
    left: 0,

    top: (props) => props.value * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([0])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ py }) => py,
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

平面滑动选择器

而同时使用 pxpy,就可以实现平面滑动选择器了:

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles({
  root: {
    maxWidth: 300,
    height: 200,
    background: '#ddd',
    borderRadius: 20,
    position: 'relative',
  },
  rail: {
    position: 'absolute',
    top: 20,
    bottom: 20,
    left: 20,
    right: 20,
  },
  thumb: {
    width: 40,
    height: 40,
    borderRadius: 20,
    transform: 'translate(-20px, -20px)',
    position: 'absolute',

    top: (props) => props.value.y * 100 + '%',
    left: (props) => props.value.x * 100 + '%',
    background: (props) => (props.sliding ? '#000' : '#777'),
  },
})

export default () => {
  const [value, setValue] = useState([{ x: 0, y: 0 }])

  const slider = useSlider({
    value,
    setValue,
    moving: ({ px, py }) => ({ x: px, y: py }),
  })

  const classes = useStyles({
    value: value[0],
    sliding: slider.sliding,
  })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <div className={classes.rail} {...slider.railProps}>
        <div
          className={classes.thumb}
          {...slider.thumbProps}
        />
      </div>
    </div>
  )
}

表格行列数选择器(常用于富文本编辑器)

import { useSlider } from '@nami-ui/use-slider'
import { createUseStyles } from 'react-jss'
import clsx from 'clsx'

const COL_SIZE = 26
const COL_SPACE = 1
const MIN_LEN = 3
const MAX_LEN = 10

const useStyles = createUseStyles({
  root: {
    position: 'relative',
    width: (props) =>
      props.collen * COL_SIZE +
      (props.collen - 1) * COL_SPACE,
  },
  rail: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
  },
})

export default () => {
  const [value, setValue] = useState([{ row: 2, col: 2 }])

  const slider = useSlider({
    value,
    setValue,
    axis: { step: 1, min: 1, max: MAX_LEN },
    moving: ({ breakX: x, breakY: y }) => ({
      row: Math.ceil(y / (COL_SIZE + COL_SPACE)),
      col: Math.ceil(x / (COL_SIZE + COL_SPACE)),
    }),
  })

  const { row, col } = value[0]
  const rowlen = Math.max(MIN_LEN, row)
  const collen = Math.max(MIN_LEN, col)

  const classes = useStyles({ rowlen, collen })

  return (
    <div className={classes.root} {...slider.rootProps}>
      <TableMock
        row={row}
        col={col}
        rowlen={rowlen}
        collen={collen}
      />
      <div className={classes.rail} {...slider.railProps}>
        <div {...slider.thumbProps} />
      </div>
    </div>
  )
}

const useTableMockStyles = createUseStyles({
  row: {
    display: 'flex',
    marginTop: COL_SPACE,
  },
  col: {
    width: COL_SIZE,
    height: COL_SIZE,
    border: '1px solid #ddd',
    marginLeft: COL_SPACE,
  },
  firstRow: {
    marginTop: 0,
  },
  firstCol: {
    marginLeft: 0,
  },
  colInFirstRow: {
    background: '#f0f0f0',
  },
  selectedCol: {
    position: 'relative',
    '&:after': {
      content: '""',
      position: 'absolute',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
      background: 'rgb(11 142 208 / 15%)',
    },
  },
})

function TableMock({ row, col, rowlen, collen }) {
  const classes = useTableMockStyles()

  const rows = []
  for (let i = 0; i < rowlen; i++) {
    const isFirstRow = i === 0

    const cols = []
    for (let j = 0; j < collen; j++) {
      const isFirstCol = j === 0

      cols.push(
        <div
          key={j}
          className={clsx(classes.col, {
            [classes.firstCol]: isFirstCol,
            [classes.colInFirstRow]: isFirstRow,
            [classes.selectedCol]: i < row && j < col,
          })}
        />,
      )
    }

    rows.push(
      <div
        key={i}
        className={clsx(classes.row, {
          [classes.firstRow]: isFirstRow,
        })}
      >
        {cols}
      </div>,
    )
  }

  return rows
}

类型定义

useSlider 所接收参数及返回值类型定义如下:

interface useSlider {
    (props: UseSliderProps): Slider
}

interface UseSliderProps {
    // 一组值,分别对应每个滑块
    value: Value
    // 设置值
    setValue: React.Dispatch<React.SetStateAction<Value>>

    // 数轴
    axis: Axis | { [prop: string]: Axis }

    // 是否禁用
    disabled: boolean

    // 处理指针(鼠标、手指)拖拽事件
    moving: (event: MovingEvent) => ValuePatch

    // 处理滚轮事件
    wheel?: (event: WheelEvent) => ValuePatch

    // 快捷键
    hotkeys?: Hotkey[]
}

interface Slider {
    // 需要分别注入到对应元素上的属性
    rootProps: HTMLAttributes<HTMLElement>
    railProps: HTMLAttributes<HTMLElement>
    thumbProps: HTMLAttributes<HTMLElement>

    // 是否在滑动中
    sliding: boolean

    // 当前正在滑动的滑块索引
    thumb: number | undefined
}

// 每个滑块的值;可以是单个数值,如:`[0, 1]`,
// 也可以是包含一组数值的对象,如:`[ { x: 0, y: 0 }, { x: 1, y: 1 } ]`
type Value = number[] | { [prop: string]: number }[]

// 数轴,用于限定滑块在某个方向上的数值;若滑块值为单个数值,则只能指定一个数轴,
// 而若是包含一组数值的对象,则既可以指定一个通用数轴,也可以分别为每个数值属性指定对应的数轴,
// 如:{ x: { min: 0, max: 100 }, y: { min: 0, max: 60 }  }
interface Axis {
    // 最小值
    min: number

    // 最大值
    max: number

    // 步长
    step?: number

    // 额外数值点
    points?: number[]
}

// 用于更新滑块值,需要在事件处理器中返回,如:
// - 更新到指定数值:`0.2`,或 `{ x: 0.2 }`;
// - 更新到下一个或上一个值:`'next'` 或 `{ x: 'prev' }`;
// - 在当前值的基础上加减指定值:`'+0.2'`,或 `{ x: '-0.2' }`;
// - 自定义更新,如:`prevValue => prevValue / 2` 或 `prevValue => ( { x: prevValue.x / 2 } )`
type ValuePatch =
    | number
    | 'prev' | 'next' | string
    | { [prop: string]: number | 'prev' | 'next' | string }
    | (value: Value) => Value

// 指针拖拽事件
interface MovingEvent {
    // 指针相对于轨道元素的坐标位置
    x: number
    y: number
    breakX: number
    breakY: number

    // 指针相对于轨道元素的位置百分比
    px: number
    py: number
    breakPX: number
    breakPY: number

    // 指针移动方向
    dirX: 'left' | 'right'
    dirY: 'up' | 'down'

    // 指针移动速度
    velocity: number
}

// 滚轮事件
interface WheelEvent {
    // 滚轮滚动值
    deltaX: number
    deltaY: number
    deltaZ: number

    // 修饰键
    altKey: boolean
    ctrlKey: boolean
    metaKey: boolean
    shiftKey: boolean
}

// 快捷键事件
interface Hotkey {
    // 需要监听的快捷键,如: `a, ctrl-a`(特殊值 `ANY`,用于监听任意快捷键)
    keys: string

    // 可选修饰键,如 `{ key: 'ctrl-a', shift: true }` 表示监听快捷键 `ctrl-a` 及 `ctrl-shift-a`。
    shift?: boolean
    ctrl?: boolean
    meta?: boolean
    alt?: boolean

    // 事件处理函数
    handle: (eventData: HotkeyEvent) => ValuePatch | void
}

interface HotkeyEvent {
    // 用户所按下的快捷键,对应在 Hotkey.keys 中配置的某个快捷键
    readonly key: string

    /** 修饰键 */
    readonly ctrl: boolean
    readonly shift: boolean
    readonly meta: boolean
    readonly alt: boolean

    // 停止事件传播
    readonly stopPropagation: () => void

    // 阻止事件默认行为
    readonly preventDefault: () => void

    // 是否已阻止事件默认行为
    readonly defaultPrevented: boolean

    // 是否已匹配并触发某个快捷键
    readonly dispatched: boolean
}

基础样式

这三者的基础样式可如下所示:

.container {
  position: relative;
}

.rail {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.thumb {
  width: 40px;
  height: 40px;

  position: absolute;
  left: 0;
  top: 0;

  transform: translate(-20px, 0);
}

Readme

Keywords

Package Sidebar

Install

npm i @nami-ui/use-slider

Weekly Downloads

0

Version

0.0.5

License

MIT

Unpacked Size

28 kB

Total Files

9

Last publish

Collaborators

  • biossun
  • biossun-by-nami-ui