Skip to content

Commit d77b6b0

Browse files
committed
feat: add draft scrollabe
1 parent 052e9fb commit d77b6b0

File tree

5 files changed

+347
-36
lines changed

5 files changed

+347
-36
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "dist/index.js",
66
"scripts": {
77
"test": "jest --coverage",
8-
"dev": "start-storybook -s ./public -p 6006",
8+
"dev": "start-storybook -s ./dist -p 6006",
99
"build-storybook": "build-storybook",
1010
"check-types": "tsc --skipLibCheck",
1111
"build": "tsc --project tsconfig.build.json",

src/components/ scrollable/index.tsx

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import './style.scss';
2+
import * as React from 'react';
3+
import { prefixClaName, classNames } from 'mo/common/className';
4+
5+
const getIndicatorMovement = (scrollerSize: number, containerSize: number) => {
6+
const width = (scrollerSize / containerSize) * scrollerSize
7+
return {
8+
width,
9+
movement: scrollerSize - width
10+
}
11+
}
12+
13+
const getIndicatorStyle = (
14+
direction: 'horizontal' | 'vertical',
15+
scrollerSize: number,
16+
containerSize: number
17+
) => {
18+
if (scrollerSize >= containerSize) {
19+
return {}
20+
}
21+
let size = (scrollerSize / containerSize) * 100
22+
if (direction === 'horizontal') {
23+
return {
24+
width: `${size}%`,
25+
height: '100%'
26+
}
27+
}
28+
return {
29+
width: '100%',
30+
height: `${size}%`
31+
}
32+
}
33+
34+
export interface Props {
35+
children: any
36+
direction: 'horizontal' | 'vertical'
37+
activeIndex: number
38+
}
39+
40+
export default class Scrollable extends React.Component<Props> {
41+
static defaultProps = {
42+
direction: 'horizontal'
43+
}
44+
45+
translate: number = 0
46+
maxTranslate: number = 0
47+
indicatorActive: boolean = false
48+
indicatorPrevPos: number = 0
49+
indicatorMovementSize = { width: 0, movement: 0 }
50+
refScroller: React.RefObject<HTMLDivElement> = React.createRef()
51+
refContainer: React.RefObject<HTMLDivElement> = React.createRef()
52+
refIndicator: React.RefObject<HTMLDivElement> = React.createRef()
53+
state: {
54+
movementStyle: any
55+
indicatorMovementStyle: any
56+
scrollerWidth: number
57+
containerWidth: number
58+
barActive: boolean
59+
indicatorActive: boolean
60+
}
61+
62+
constructor(props: Props) {
63+
super(props)
64+
this.state = {
65+
movementStyle: {},
66+
indicatorMovementStyle: {},
67+
scrollerWidth: 0,
68+
containerWidth: 0,
69+
barActive: false,
70+
indicatorActive: false
71+
}
72+
}
73+
74+
componentDidMount() {
75+
this.attachIndicatorEvent()
76+
}
77+
78+
componentDidUpdate(prevProps: Props) {
79+
const { children, activeIndex } = this.props
80+
if (prevProps.children.length !== children.length) {
81+
this.resize()
82+
this.scrollToActive()
83+
} else if (prevProps.activeIndex !== activeIndex) {
84+
this.scrollToActive()
85+
}
86+
}
87+
88+
handleMouseWheel = (event: React.WheelEvent) => {
89+
const useX = Math.abs(event.deltaX) > Math.abs(event.deltaY)
90+
this.move(useX ? -event.deltaX : event.deltaY)
91+
}
92+
93+
handleIndicatorDown = (event: MouseEvent) => {
94+
this.indicatorActive = true
95+
this.indicatorPrevPos = event.clientX
96+
}
97+
98+
handleIndicatorMove = (event: MouseEvent) => {
99+
if (!this.indicatorActive) return
100+
const movement = event.clientX - this.indicatorPrevPos
101+
this.indicatorPrevPos = event.clientX
102+
this.move((-movement / this.indicatorMovementSize.movement) * this.maxTranslate )
103+
this.setState({
104+
indicatorActive: true
105+
})
106+
}
107+
108+
handleIndicatorUp = () => {
109+
this.indicatorActive = false
110+
this.setState({
111+
indicatorActive: false
112+
})
113+
}
114+
115+
handleBarMouseOver = () => {
116+
this.setState({
117+
barActive: true
118+
})
119+
}
120+
handleBarMouseOut = () => {
121+
this.setState({
122+
barActive: false
123+
})
124+
}
125+
126+
handleIndicatorMouseOver = () => {
127+
this.setState({
128+
indicatorActive: true
129+
})
130+
}
131+
handleIndicatorMouseOut = () => {
132+
this.setState({
133+
indicatorActive: false
134+
})
135+
}
136+
137+
attachIndicatorEvent() {
138+
if (this.refIndicator.current) {
139+
this.refIndicator.current.addEventListener(
140+
'mousedown',
141+
this.handleIndicatorDown
142+
)
143+
document.addEventListener('mousemove', this.handleIndicatorMove)
144+
document.addEventListener('mouseup', this.handleIndicatorUp)
145+
}
146+
}
147+
148+
scrollToActive() {
149+
if (!this.refContainer.current) return
150+
const { activeIndex } = this.props
151+
const { scrollerWidth } = this.state
152+
const $children = this.refContainer.current.children
153+
const $activeChild = $children[activeIndex] as HTMLElement
154+
if (!$activeChild) return
155+
let movement = -(
156+
$activeChild.offsetLeft +
157+
$activeChild.offsetWidth -
158+
scrollerWidth
159+
)
160+
// 一开始就处于可见范围内的元素
161+
if (movement > 0 && movement < scrollerWidth) {
162+
movement = 0
163+
}
164+
this.translate = 0
165+
this.move(movement)
166+
}
167+
168+
resize() {
169+
let scrollerWidth = 0
170+
let containerWidth = 0
171+
if (this.refScroller.current) {
172+
scrollerWidth = this.refScroller.current.clientWidth
173+
}
174+
if (this.refContainer.current) {
175+
containerWidth = this.refContainer.current.clientWidth
176+
}
177+
this.maxTranslate = containerWidth - scrollerWidth
178+
this.indicatorMovementSize = getIndicatorMovement(
179+
scrollerWidth,
180+
containerWidth
181+
)
182+
this.setState({
183+
scrollerWidth,
184+
containerWidth
185+
})
186+
this.move(0)
187+
}
188+
189+
move(delta: number) {
190+
if (this.maxTranslate <= 0) {
191+
this.setState({
192+
indicatorMovementStyle: { transform: `translate(0, 0)` },
193+
movementStyle: { transform: `translate(0, 0)` }
194+
})
195+
return
196+
}
197+
this.translate -= delta
198+
if (this.translate <= 0) {
199+
this.translate = 0
200+
} else if (this.translate > this.maxTranslate) {
201+
this.translate = this.maxTranslate
202+
}
203+
this.setState({
204+
indicatorMovementStyle: {
205+
transform: `translate(${(this.translate / this.maxTranslate) *
206+
this.indicatorMovementSize.movement}px, 0px)`
207+
},
208+
movementStyle: {
209+
transform: `translate(${-this.translate}px, 0px)`
210+
}
211+
})
212+
}
213+
214+
render() {
215+
const { direction, children } = this.props
216+
const {
217+
movementStyle,
218+
scrollerWidth,
219+
containerWidth,
220+
indicatorMovementStyle,
221+
barActive,
222+
indicatorActive
223+
} = this.state
224+
return (
225+
<div
226+
className={classNames('spui-scroller', `spui-scroller--${direction}`)}
227+
onWheel={this.handleMouseWheel}
228+
ref={this.refScroller}
229+
onMouseOver={this.handleBarMouseOver}
230+
onMouseOut={this.handleBarMouseOut}
231+
>
232+
<div
233+
className="spui-scroller__container"
234+
ref={this.refContainer}
235+
style={movementStyle}
236+
>
237+
{children}
238+
</div>
239+
<div
240+
className={classNames('spui-scroller__bar', {
241+
'spui-scroller__bar--active':
242+
scrollerWidth < containerWidth && (barActive || indicatorActive)
243+
})}
244+
style={{
245+
...getIndicatorStyle(direction, scrollerWidth, containerWidth),
246+
...indicatorMovementStyle
247+
}}
248+
ref={this.refIndicator}
249+
onMouseOver={this.handleIndicatorMouseOver}
250+
onMouseOut={this.handleIndicatorMouseOut}
251+
>
252+
</div>
253+
</div>
254+
)
255+
}
256+
}

src/components/ scrollable/style.scss

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.spui-scroller {
2+
height: 100%;
3+
overflow: hidden;
4+
position: relative;
5+
width: 100%;
6+
7+
&__container {
8+
display: inline-block;
9+
position: relative;
10+
}
11+
12+
&__bar {
13+
display: none;
14+
position: absolute;
15+
}
16+
17+
&__bar--active {
18+
display: block;
19+
}
20+
21+
&__indicator {
22+
background-color: rgba(255, 255, 255, 0.1);
23+
border-radius: 6px;
24+
cursor: pointer;
25+
}
26+
27+
&__indicator--active {
28+
background-color: rgba(255, 255, 255, 0.3);
29+
}
30+
31+
&&--horizontal &__container {
32+
height: 100%;
33+
}
34+
35+
&&--horizontal &__bar {
36+
bottom: 0;
37+
height: 6px;
38+
left: 0;
39+
width: 100%;
40+
}
41+
42+
&&--vertical &__container {
43+
width: 100%;
44+
}
45+
46+
&&--vertical &__bar {
47+
bottom: 0;
48+
height: 100%;
49+
right: 0;
50+
width: 6px;
51+
}
52+
}

src/components/scroller/style.scss

-4
This file was deleted.

0 commit comments

Comments
 (0)