-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
29fdbe8
commit 44a6233
Showing
14 changed files
with
819 additions
and
367 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { type RefObject, useEffect } from 'react'; | ||
|
||
const KEYDOWN_DURATION = 300; | ||
const KEYDOWN_IGNORE_TAGNAMES = ['INPUT', 'TEXTAREA']; | ||
let keydownTimestamp = 0; | ||
|
||
interface Trigger { | ||
key: string; | ||
callback: () => void; | ||
} | ||
|
||
export const useEventKeydown = ( | ||
triggers: Trigger[], | ||
ref?: RefObject<HTMLElement>, | ||
) => { | ||
// | ||
const handleKeydown = (e: KeyboardEvent) => { | ||
if (e.isComposing) return; // 変換中ならキャンセル | ||
const tagName = (e.target as HTMLElement).tagName; // 入力対象のタグ名を取得 | ||
if (KEYDOWN_IGNORE_TAGNAMES.includes(tagName)) return; // タグ名が無視リストにあるならキャンセル | ||
if (triggers.map(t => t.key).includes(e.key)) e.preventDefault(); // デフォルトの動作を無効化 | ||
const sub = e.timeStamp - keydownTimestamp; // 時間間隔を取得 | ||
if (sub < KEYDOWN_DURATION) return; // 間隔が一定以下ならキャンセル | ||
keydownTimestamp = e.timeStamp; // 最終実行時間を更新 | ||
// 入力に応じたコールバックを実行 | ||
for (const trigger of triggers) { | ||
if (trigger.key === e.key) { | ||
trigger.callback(); | ||
} | ||
} | ||
}; | ||
// | ||
useEffect(() => { | ||
if (ref?.current) { | ||
ref.current.addEventListener('keydown', handleKeydown, true); | ||
} else { | ||
window.addEventListener('keydown', handleKeydown, true); | ||
} | ||
return () => { | ||
if (ref?.current) { | ||
ref.current.removeEventListener('keydown', handleKeydown, true); | ||
} else { | ||
window.removeEventListener('keydown', handleKeydown, true); | ||
} | ||
}; | ||
}, [ref, handleKeydown]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { type RefObject, useEffect } from 'react'; | ||
|
||
const WHEEL_DURATION = 300; | ||
let wheelTimeoutid: number | null = null; | ||
|
||
export const useEventWheel = ( | ||
ref: RefObject<HTMLElement>, | ||
callbackWheel: () => void, | ||
callbackEnd: () => void, | ||
) => { | ||
// | ||
const handleWheel = (e: WheelEvent) => { | ||
// 要素がないならキャンセル | ||
if (!ref.current) return; | ||
// 実行中の処理を実行 | ||
callbackWheel(); | ||
// デフォルトのスクロールを無効化 | ||
e.preventDefault(); | ||
// | ||
ref.current.scrollLeft += e.deltaX; | ||
ref.current.scrollTop += e.deltaY; | ||
// タイムアウトを設定済みならクリア | ||
if (wheelTimeoutid) clearTimeout(wheelTimeoutid); | ||
// タイムアウトを設定 | ||
wheelTimeoutid = setTimeout(callbackEnd, WHEEL_DURATION); | ||
}; | ||
// | ||
useEffect(() => { | ||
ref.current?.addEventListener('wheel', handleWheel, { | ||
passive: false, | ||
}); | ||
return () => { | ||
ref.current?.removeEventListener('wheel', handleWheel); | ||
}; | ||
}, [ref, handleWheel]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import stylex from '@stylexjs/stylex'; | ||
import { type FC, memo } from 'react'; | ||
import { DivCustom } from '../../components/div/DivCustom'; | ||
import { DivScrollable } from '../../components/div/DivScrollable'; | ||
import { H2 } from '../../components/heading/H2'; | ||
import { stylesCommon } from './styles'; | ||
|
||
const styles = stylex.create({ | ||
wrapper: { | ||
width: '100dvw', | ||
height: '100dvh', | ||
position: 'relative', | ||
}, | ||
parent: { | ||
position: 'absolute', | ||
left: '50%', | ||
top: '50%', | ||
transform: 'translate(-50%, -50%)', | ||
width: '80dvw', | ||
height: '80dvh', | ||
}, | ||
scroll: { | ||
display: 'grid', | ||
gridTemplateColumns: 'repeat(3, 1fr)', | ||
gridTemplateRows: 'repeat(3, 1fr)', | ||
'::-webkit-scrollbar': { | ||
display: 'none', | ||
}, | ||
}, | ||
child: { | ||
width: '80dvw', | ||
height: '80dvh', | ||
background: 'radial-gradient(#ff000033, #0000ff33)', | ||
}, | ||
}); | ||
|
||
const Component: FC = () => { | ||
return ( | ||
<DivCustom styles={styles.wrapper}> | ||
<H2 propsStyles={stylesCommon.h2}>Ultimate</H2> | ||
{/* */} | ||
<DivScrollable | ||
isSnap | ||
isAnimate | ||
stylesParent={styles.parent} | ||
stylesScroll={styles.scroll} | ||
> | ||
<div {...stylex.props(styles.child)}>1</div> | ||
<div {...stylex.props(styles.child)}>2</div> | ||
<div {...stylex.props(styles.child)}>3</div> | ||
<div {...stylex.props(styles.child)}>4</div> | ||
<div {...stylex.props(styles.child)}>5</div> | ||
<div {...stylex.props(styles.child)}>6</div> | ||
<div {...stylex.props(styles.child)}>7</div> | ||
<div {...stylex.props(styles.child)}>8</div> | ||
<div {...stylex.props(styles.child)}>9</div> | ||
</DivScrollable> | ||
</DivCustom> | ||
); | ||
}; | ||
|
||
export const Ultimate = memo(Component); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
type Type = (t: number) => number; | ||
export const easeInOutQuad: Type = t => { | ||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const getChildWidthes = (elem: HTMLDivElement) => { | ||
const children = Array.from(elem.children); | ||
return { | ||
childWidthes: children.map(child => child.clientWidth), | ||
childHeights: children.map(child => child.clientHeight), | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import type { Coordinate } from '../@types'; | ||
|
||
type Type = (elem: HTMLDivElement) => Coordinate[]; | ||
|
||
export const getChildOffsets: Type = (elem: HTMLDivElement) => { | ||
const rectParent = elem.getBoundingClientRect(); | ||
const children = Array.from(elem.children); | ||
const rectChildren = children.map(c => c.getBoundingClientRect()); | ||
return rectChildren.map(c => ({ | ||
x: c.left - rectParent.left + elem.scrollLeft, | ||
y: c.top - rectParent.top + +elem.scrollTop, | ||
})); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { Coordinate } from '../@types'; | ||
|
||
export const getClosestCoordinate = ( | ||
target: Coordinate, | ||
samples: Coordinate[], | ||
) => | ||
samples.reduce((acc, curr) => { | ||
const currDistance = Math.sqrt( | ||
(curr.x - target.x) ** 2 + (curr.y - target.y) ** 2, | ||
); | ||
const accDistance = Math.sqrt( | ||
(acc.x - target.x) ** 2 + (acc.y - target.y) ** 2, | ||
); | ||
return currDistance < accDistance ? curr : acc; | ||
}, samples[0]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import type { Coordinate } from '../@types'; | ||
import { easeInOutQuad } from './easeInOutQuad'; | ||
|
||
const SCROLL_ANIMATE_DURATION = 250; | ||
|
||
type Callback = () => void; | ||
|
||
export const scrollAnimate = () => { | ||
// アニメーションのフレームID | ||
let animationFrameId: number | null = null; | ||
|
||
const cancelAnimateScroll = (callback?: Callback) => { | ||
if (animationFrameId === null) return; | ||
cancelAnimationFrame(animationFrameId); | ||
animationFrameId === null; | ||
callback?.(); | ||
}; | ||
|
||
const animateScroll = ( | ||
elem: HTMLElement, | ||
target: Coordinate, | ||
callback?: Callback, | ||
) => { | ||
const start = { x: elem.scrollLeft, y: elem.scrollTop }; | ||
const distance = { x: target.x - start.x, y: target.y - start.y }; | ||
if (distance.x === 0 && distance.y === 0) return callback?.(); | ||
// タイムスタンプを定義 | ||
let t: number | null = null; | ||
// アニメーションを定義 | ||
const step = (timestamp: number) => { | ||
if (!t) t = timestamp; | ||
const progress = timestamp - t; | ||
const percentage = Math.min(progress / SCROLL_ANIMATE_DURATION, 1); | ||
const easePercentage = easeInOutQuad(percentage); | ||
elem.scrollLeft = start.x + distance.x * easePercentage; | ||
elem.scrollTop = start.y + distance.y * easePercentage; | ||
// 進捗率で分岐 | ||
if (progress < SCROLL_ANIMATE_DURATION) { | ||
animationFrameId = window.requestAnimationFrame(step); | ||
} else { | ||
cancelAnimateScroll(callback); | ||
} | ||
}; | ||
// アニメーションを実行 | ||
window.requestAnimationFrame(step); | ||
}; | ||
|
||
return { cancelAnimateScroll, animateScroll }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type { PointerEvent } from 'react'; | ||
import type { Coordinate } from '../@types'; | ||
import { getChildOffsets } from './getChildOffsets'; | ||
import { getClosestCoordinate } from './getClosestCoordinate'; | ||
import { scrollAnimate } from './scrollAnimate'; | ||
|
||
export const snapPointer = ( | ||
e: PointerEvent<HTMLDivElement>, | ||
dragScroll: Coordinate, | ||
dragTimestamp: number, | ||
isAnimate: boolean | undefined, | ||
) => { | ||
// | ||
const target = e.target as HTMLDivElement; | ||
// | ||
const { scrollLeft, scrollTop, clientWidth, clientHeight } = target; | ||
const subTimestamp = e.timeStamp - dragTimestamp; | ||
const subCoordinate: Coordinate = { | ||
x: scrollLeft - dragScroll.x, | ||
y: scrollTop - dragScroll.y, | ||
}; | ||
const adjuster: Coordinate = { | ||
x: ((subCoordinate.x / subTimestamp) * clientWidth) / 5000, | ||
y: ((subCoordinate.y / subTimestamp) * clientHeight) / 5000, | ||
}; | ||
const adjusted: Coordinate = { | ||
x: scrollLeft + adjuster.x, | ||
y: scrollTop + adjuster.y, | ||
}; | ||
const offsets: Coordinate[] = getChildOffsets(target); | ||
const closest = getClosestCoordinate(adjusted, offsets); | ||
// フラグによりアニメーションか瞬間移動か分岐 | ||
if (isAnimate) { | ||
const { animateScroll } = scrollAnimate(); | ||
animateScroll(target, closest); | ||
} else { | ||
target.scrollLeft = closest.x; | ||
target.scrollTop = closest.y; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { Coordinate } from '../@types'; | ||
import { getChildOffsets } from './getChildOffsets'; | ||
import { getClosestCoordinate } from './getClosestCoordinate'; | ||
import { scrollAnimate } from './scrollAnimate'; | ||
|
||
export const snapWheel = ( | ||
target: HTMLDivElement, | ||
isAnimate: boolean | undefined, | ||
) => { | ||
// | ||
const { scrollLeft: x, scrollTop: y } = target; | ||
const offsets: Coordinate[] = getChildOffsets(target); | ||
const closest = getClosestCoordinate({ x, y }, offsets); | ||
// フラグによりアニメーションか瞬間移動か分岐 | ||
if (isAnimate) { | ||
const { animateScroll } = scrollAnimate(); | ||
animateScroll(target, closest); | ||
} else { | ||
target.scrollLeft = closest.x; | ||
target.scrollTop = closest.y; | ||
} | ||
}; |