mirror of
https://github.com/pomber/git-history.git
synced 2024-08-16 09:50:32 +03:00
Merge pull request #118 from pomber/scroll-to-changes
Scroll to changes Fix #12 and #64
This commit is contained in:
commit
0537cb13fd
2
.storybook/addons.js
Normal file
2
.storybook/addons.js
Normal file
@ -0,0 +1,2 @@
|
||||
import "@storybook/addon-actions/register";
|
||||
import "@storybook/addon-links/register";
|
9
.storybook/config.js
Normal file
9
.storybook/config.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { configure } from "@storybook/react";
|
||||
|
||||
const req = require.context("../src", true, /\.story\.js$/);
|
||||
|
||||
function loadStories() {
|
||||
req.keys().forEach(filename => req(filename));
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
15
package.json
15
package.json
@ -9,11 +9,12 @@
|
||||
"js-base64": "^2.5.1",
|
||||
"netlify-auth-providers": "^1.0.0-alpha5",
|
||||
"prismjs": "^1.15.0",
|
||||
"react": "^16.8.1",
|
||||
"react-dom": "^16.8.1",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-scripts": "2.1.3",
|
||||
"react-swipeable": "^4.3.2",
|
||||
"react-use": "^5.2.2",
|
||||
"rebound": "^0.1.0",
|
||||
"workerize-loader": "^1.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
@ -23,7 +24,9 @@
|
||||
"test-prettier": "prettier --check \"**/*.{js,jsx,md,json,html,css,yml}\" --ignore-path .gitignore",
|
||||
"test-cra": "react-scripts test",
|
||||
"test": "run-s test-prettier test-cra",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"build-storybook": "build-storybook -s public"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
@ -35,6 +38,12 @@
|
||||
"not op_mini all"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.4",
|
||||
"@storybook/addon-actions": "^4.1.13",
|
||||
"@storybook/addon-links": "^4.1.13",
|
||||
"@storybook/addons": "^4.1.13",
|
||||
"@storybook/react": "^4.1.13",
|
||||
"babel-loader": "8.0.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.16.4"
|
||||
}
|
||||
|
@ -1,17 +1,26 @@
|
||||
import easing from "./easing";
|
||||
const MULTIPLY = "multiply";
|
||||
|
||||
/* eslint-disable */
|
||||
function mergeResults(results) {
|
||||
function mergeResults(results, composite) {
|
||||
const firstResult = results[0];
|
||||
if (results.length < 2) {
|
||||
return firstResult;
|
||||
}
|
||||
if (Array.isArray(firstResult)) {
|
||||
// console.log("merge", results);
|
||||
return firstResult.map((_, i) => {
|
||||
return mergeResults(results.map(result => result[i]));
|
||||
return mergeResults(results.map(result => result[i]), composite);
|
||||
});
|
||||
} else {
|
||||
return Object.assign({}, ...results);
|
||||
const merged = Object.assign({}, ...results);
|
||||
|
||||
if (composite === MULTIPLY) {
|
||||
const opacities = results.map(x => x.opacity).filter(x => x != null);
|
||||
if (opacities.length !== 0) {
|
||||
merged.opacity = opacities.reduce((a, b) => a * b);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +28,7 @@ const airframe = {
|
||||
parallel: ({ children: fns }) => {
|
||||
return (t, ...args) => {
|
||||
const styles = fns.map(fn => fn(t, ...args));
|
||||
const result = mergeResults(styles);
|
||||
const result = mergeResults(styles, MULTIPLY);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ import { createAnimation, Stagger } from "./airframe/airframe";
|
||||
import easing from "./airframe/easing";
|
||||
|
||||
const dx = 250;
|
||||
const offOpacity = 0.6;
|
||||
|
||||
/* @jsx createAnimation */
|
||||
|
||||
@ -43,7 +44,7 @@ function GrowHeight() {
|
||||
);
|
||||
}
|
||||
|
||||
function SwitchLines({ filterExit, filterEnter }) {
|
||||
function SwitchLines({ filterExit, filterEnter, filterFadeOut }) {
|
||||
return (
|
||||
<parallel>
|
||||
<Stagger interval={0.2} filter={filterExit}>
|
||||
@ -59,6 +60,19 @@ function SwitchLines({ filterExit, filterEnter }) {
|
||||
<SlideFromRight />
|
||||
</chain>
|
||||
</Stagger>
|
||||
<Stagger interval={0} filter={filterEnter}>
|
||||
<tween from={{ opacity: offOpacity }} to={{ opacity: 1 }} />
|
||||
</Stagger>
|
||||
<Stagger interval={0} filter={filterFadeOut}>
|
||||
<tween
|
||||
from={{ opacity: 1 }}
|
||||
to={{ opacity: offOpacity }}
|
||||
ease={easing.easeOutCubic}
|
||||
/>
|
||||
</Stagger>
|
||||
<Stagger interval={0} filter={l => !filterEnter(l) && !filterFadeOut(l)}>
|
||||
<tween from={{ opacity: offOpacity }} to={{ opacity: offOpacity }} />
|
||||
</Stagger>
|
||||
</parallel>
|
||||
);
|
||||
}
|
||||
@ -68,10 +82,12 @@ export default (
|
||||
<SwitchLines
|
||||
filterExit={line => line.left && !line.middle}
|
||||
filterEnter={line => !line.left && line.middle}
|
||||
filterFadeOut={line => false}
|
||||
/>
|
||||
<SwitchLines
|
||||
filterExit={line => line.middle && !line.right}
|
||||
filterEnter={line => !line.middle && line.right}
|
||||
filterFadeOut={line => !line.left && line.middle}
|
||||
/>
|
||||
</chain>
|
||||
);
|
||||
|
@ -37,12 +37,7 @@ function InnerApp({ gitProvider }) {
|
||||
return <Error error={{ status: 404 }} gitProvider={gitProvider} />;
|
||||
}
|
||||
|
||||
const commits = versions.map(v => v.commit);
|
||||
const slideLines = versions.map(v => v.lines);
|
||||
|
||||
return (
|
||||
<History commits={commits} slideLines={slideLines} loadMore={loadMore} />
|
||||
);
|
||||
return <History versions={versions} loadMore={loadMore} />;
|
||||
}
|
||||
|
||||
function useVersionsLoader(gitProvider) {
|
||||
|
@ -84,3 +84,26 @@ export function getSlides(codes, language) {
|
||||
.filter(line => line.middle || line.left || line.right);
|
||||
});
|
||||
}
|
||||
|
||||
export function getChanges(lines) {
|
||||
const changes = [];
|
||||
let currentChange = null;
|
||||
let i = 0;
|
||||
const isNewLine = i => !lines[i].left && lines[i].middle;
|
||||
while (i < lines.length) {
|
||||
if (isNewLine(i)) {
|
||||
if (!currentChange) {
|
||||
currentChange = { start: i };
|
||||
}
|
||||
} else {
|
||||
if (currentChange) {
|
||||
currentChange.end = i - 1;
|
||||
changes.push(currentChange);
|
||||
currentChange = null;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getLanguage, loadLanguage } from "./language-detector";
|
||||
import { getSlides } from "./differ";
|
||||
import { getSlides, getChanges } from "./differ";
|
||||
|
||||
import github from "./github-commit-fetcher";
|
||||
import gitlab from "./gitlab-commit-fetcher";
|
||||
@ -25,5 +25,9 @@ export async function getVersions(source, params) {
|
||||
|
||||
const codes = commits.map(commit => commit.content);
|
||||
const slides = getSlides(codes, lang);
|
||||
return commits.map((commit, i) => ({ commit, lines: slides[i] }));
|
||||
return commits.map((commit, i) => ({
|
||||
commit,
|
||||
lines: slides[i],
|
||||
changes: getChanges(slides[i])
|
||||
}));
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getLanguage, loadLanguage } from "./language-detector";
|
||||
import { getSlides } from "./differ";
|
||||
import { getSlides, getChanges } from "./differ";
|
||||
|
||||
const vscode = window.vscode;
|
||||
|
||||
@ -43,7 +43,11 @@ async function getVersions(last) {
|
||||
|
||||
const codes = commits.map(commit => commit.content);
|
||||
const slides = getSlides(codes, lang);
|
||||
return commits.map((commit, i) => ({ commit, lines: slides[i] }));
|
||||
return commits.map((commit, i) => ({
|
||||
commit,
|
||||
lines: slides[i],
|
||||
changes: getChanges(slides[i])
|
||||
}));
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -99,14 +99,13 @@ function CommitList({ commits, currentIndex, selectCommit }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function History({ commits, slideLines, loadMore }) {
|
||||
return (
|
||||
<Slides slideLines={slideLines} commits={commits} loadMore={loadMore} />
|
||||
);
|
||||
export default function History({ versions, loadMore }) {
|
||||
return <Slides versions={versions} loadMore={loadMore} />;
|
||||
}
|
||||
|
||||
function Slides({ commits, slideLines, loadMore }) {
|
||||
function Slides({ versions, loadMore }) {
|
||||
const [current, target, setTarget] = useSliderSpring(0);
|
||||
const commits = versions.map(v => v.commit);
|
||||
const setClampedTarget = newTarget => {
|
||||
setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget)));
|
||||
if (newTarget >= commits.length - 5) {
|
||||
@ -140,12 +139,13 @@ function Slides({ commits, slideLines, loadMore }) {
|
||||
onSwipedRight={prevSlide}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Slide time={index - current} lines={slideLines[index]} />
|
||||
<Slide time={index - current} version={versions[index]} />
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO use ./useSpring
|
||||
function useSliderSpring(initial) {
|
||||
const [target, setTarget] = useState(initial);
|
||||
const tension = 0;
|
||||
|
@ -3,4 +3,9 @@ import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
const root = document.getElementById("root");
|
||||
ReactDOM.render(<App />, root);
|
||||
ReactDOM.render(
|
||||
<React.unstable_ConcurrentMode>
|
||||
<App />
|
||||
</React.unstable_ConcurrentMode>,
|
||||
root
|
||||
);
|
||||
|
155
src/scroller.js
155
src/scroller.js
@ -1,18 +1,104 @@
|
||||
import React from "react";
|
||||
import useChildren from "./use-virtual-children";
|
||||
import "./scroller.css";
|
||||
import useSpring from "./use-spring";
|
||||
import { nextIndex, prevIndex, closestIndex, getScrollTop } from "./utils";
|
||||
|
||||
const initialState = {
|
||||
snap: false,
|
||||
targetTop: 0,
|
||||
currentTop: 0,
|
||||
areaIndex: 0
|
||||
};
|
||||
|
||||
export default function Scroller({
|
||||
items,
|
||||
getRow,
|
||||
getRowHeight,
|
||||
data,
|
||||
top,
|
||||
setTop
|
||||
snapAreas
|
||||
}) {
|
||||
const ref = React.useRef();
|
||||
const height = useHeight(ref);
|
||||
|
||||
const reducer = (prevState, action) => {
|
||||
switch (action.type) {
|
||||
case "change-area":
|
||||
if (snapAreas.length === 0) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const { changeIndex, recalculate } = action;
|
||||
const movingFromUnknownIndex = !prevState.snap || recalculate;
|
||||
|
||||
// TODO memo
|
||||
const heights = items.map((item, i) => getRowHeight(item, i, data));
|
||||
|
||||
let newIndex;
|
||||
if (movingFromUnknownIndex) {
|
||||
//todo memo
|
||||
const oldIndex = getAreaIndex(
|
||||
prevState.targetTop,
|
||||
snapAreas,
|
||||
heights,
|
||||
height
|
||||
);
|
||||
|
||||
newIndex = changeIndex(snapAreas, oldIndex);
|
||||
} else {
|
||||
newIndex = changeIndex(snapAreas, prevState.areaIndex);
|
||||
}
|
||||
|
||||
if (newIndex === prevState.areaIndex && !movingFromUnknownIndex) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
// TODO memo
|
||||
let contentHeight = heights.reduce((a, b) => a + b, 0);
|
||||
|
||||
const targetTop = getScrollTop(
|
||||
snapAreas[newIndex],
|
||||
contentHeight,
|
||||
height,
|
||||
heights
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
areaIndex: newIndex,
|
||||
snap: true,
|
||||
currentTop: null,
|
||||
targetTop
|
||||
};
|
||||
case "manual-scroll":
|
||||
const { newTop } = action;
|
||||
if (newTop === prevState.currentTop && !prevState.snap) {
|
||||
return prevState;
|
||||
}
|
||||
// console.log("manual scroll", newTop);
|
||||
return {
|
||||
...prevState,
|
||||
snap: false,
|
||||
currentTop: newTop,
|
||||
targetTop: newTop
|
||||
};
|
||||
default:
|
||||
throw Error();
|
||||
}
|
||||
};
|
||||
|
||||
const [{ snap, targetTop, currentTop }, dispatch] = React.useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
const top = useSpring({
|
||||
target: targetTop,
|
||||
current: currentTop,
|
||||
round: Math.round
|
||||
});
|
||||
// console.log("render", targetTop, top);
|
||||
|
||||
const children = useChildren({
|
||||
height,
|
||||
top,
|
||||
@ -22,21 +108,78 @@ export default function Scroller({
|
||||
data
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
document.body.addEventListener("keydown", e => {
|
||||
if (e.keyCode === 38) {
|
||||
dispatch({ type: "change-area", changeIndex: prevIndex });
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 40) {
|
||||
dispatch({ type: "change-area", changeIndex: nextIndex });
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to closest change when changing versions:
|
||||
// React.useLayoutEffect(() => {
|
||||
// dispatch({
|
||||
// type: "change-area",
|
||||
// recalculate: true,
|
||||
// changeIndex: closestIndex
|
||||
// });
|
||||
// }, [snapAreas]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
ref.current.scrollTop = top;
|
||||
}, [top]);
|
||||
if (snap) {
|
||||
ref.current.scrollTop = top;
|
||||
}
|
||||
}, [snap, top]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: "100%", overflowY: "auto", overflowX: "hidden" }}
|
||||
className="scroller"
|
||||
ref={ref}
|
||||
onScroll={e => setTop(e.target.scrollTop)}
|
||||
onScroll={e => {
|
||||
const newTop = e.target.scrollTop;
|
||||
if (newTop === top) {
|
||||
return;
|
||||
}
|
||||
dispatch({ type: "manual-scroll", newTop });
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getAreaIndex(scrollTop, areas, heights, containerHeight) {
|
||||
if (areas.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const scrollMiddle = scrollTop + containerHeight / 2;
|
||||
|
||||
let h = 0;
|
||||
let i = 0;
|
||||
while (scrollMiddle > h) {
|
||||
h += heights[i++];
|
||||
}
|
||||
const middleRow = i;
|
||||
|
||||
const areaCenters = areas.map(a => (a.start + a.end) / 2);
|
||||
areaCenters.unshift(0);
|
||||
for (let a = 0; a < areas.length; a++) {
|
||||
if (middleRow < areaCenters[a + 1]) {
|
||||
return (
|
||||
a -
|
||||
(areaCenters[a + 1] - middleRow) / (areaCenters[a + 1] - areaCenters[a])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return areas.length - 0.9;
|
||||
}
|
||||
|
||||
function useHeight(ref) {
|
||||
let [height, setHeight] = React.useState(null);
|
||||
|
||||
@ -50,7 +193,7 @@ function useHeight(ref) {
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref.current]);
|
||||
}, []);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
104
src/scroller.story.js
Normal file
104
src/scroller.story.js
Normal file
@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Scroller from "./scroller";
|
||||
|
||||
const snapAreas1 = [
|
||||
{ start: 1, end: 5 },
|
||||
{ start: 15, end: 26 },
|
||||
{ start: 50, end: 100 },
|
||||
{ start: 300, end: 302 }
|
||||
];
|
||||
|
||||
const snapAreas2 = [
|
||||
{ start: 8, end: 12 },
|
||||
{ start: 30, end: 32 },
|
||||
{ start: 550, end: 552 },
|
||||
{ start: 595, end: 599 }
|
||||
];
|
||||
|
||||
const items = Array(600)
|
||||
.fill(0)
|
||||
.map((_, i) => {
|
||||
const a1 = snapAreas1.find(a => a.start <= i && i <= a.end);
|
||||
const a2 = snapAreas2.find(a => a.start <= i && i <= a.end);
|
||||
return {
|
||||
content: `Row ${i}${
|
||||
a1
|
||||
? ` - Area1 [${a1.start}, ${a1.end}]`
|
||||
: a2
|
||||
? ` - Area2 [${a2.start}, ${a2.end}]`
|
||||
: ""
|
||||
}`,
|
||||
key: i,
|
||||
height: 22
|
||||
};
|
||||
});
|
||||
|
||||
function getRow(item) {
|
||||
return (
|
||||
<div key={item.key} style={{ height: item.height }}>
|
||||
{item.content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRowHeight(item) {
|
||||
return item.height;
|
||||
}
|
||||
|
||||
function BasicScroller({ areas }) {
|
||||
const [top, setTop] = React.useState(40);
|
||||
return (
|
||||
<Scroller
|
||||
items={items}
|
||||
snapAreas={areas}
|
||||
getRow={getRow}
|
||||
getRowHeight={getRowHeight}
|
||||
top={top}
|
||||
setTop={setTop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
storiesOf("Scroller", module).add("single", () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "90vh"
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "60%", height: "80vh", border: "1px solid black" }}>
|
||||
<BasicScroller areas={snapAreas1} />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
function DoubleScroller() {
|
||||
const [flag, setFlag] = React.useState(false);
|
||||
return (
|
||||
<div>
|
||||
<div style={{ height: "80vh", border: "1px solid black", width: "60vw" }}>
|
||||
<BasicScroller areas={flag ? snapAreas1 : snapAreas2} />
|
||||
</div>
|
||||
<div>
|
||||
{flag ? "Areas 1" : "Areas 2"}
|
||||
<button onClick={() => setFlag(flag => !flag)}>Toggle</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
storiesOf("Scroller", module).add("multiple", () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "90vh"
|
||||
}}
|
||||
>
|
||||
<DoubleScroller />
|
||||
</div>
|
||||
));
|
12
src/slide.js
12
src/slide.js
@ -36,8 +36,7 @@ function getLine(line, i, { styles }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Slide({ lines, styles }) {
|
||||
const [top, setTop] = React.useState(0);
|
||||
function Slide({ lines, styles, changes }) {
|
||||
return (
|
||||
<pre
|
||||
style={{
|
||||
@ -66,16 +65,15 @@ function Slide({ lines, styles }) {
|
||||
getRow={getLine}
|
||||
getRowHeight={getLineHeight}
|
||||
data={{ styles }}
|
||||
top={top}
|
||||
setTop={setTop}
|
||||
snapAreas={changes}
|
||||
/>
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SlideWrapper({ time, lines }) {
|
||||
export default function SlideWrapper({ time, version }) {
|
||||
const { lines, changes } = version;
|
||||
const styles = animation((time + 1) / 2, lines);
|
||||
|
||||
return <Slide lines={lines} styles={styles} />;
|
||||
return <Slide lines={lines} styles={styles} changes={changes} />;
|
||||
}
|
||||
|
47
src/use-spring.js
Normal file
47
src/use-spring.js
Normal file
@ -0,0 +1,47 @@
|
||||
// based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts
|
||||
import { SpringSystem } from "rebound";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function useSpring({
|
||||
target = 0,
|
||||
current = null,
|
||||
tension = 0,
|
||||
friction = 10,
|
||||
round = x => x
|
||||
}) {
|
||||
const [spring, setSpring] = useState(null);
|
||||
const [value, setValue] = useState(target);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = {
|
||||
onSpringUpdate: spring => {
|
||||
const value = spring.getCurrentValue();
|
||||
setValue(round(value));
|
||||
}
|
||||
};
|
||||
|
||||
if (!spring) {
|
||||
const newSpring = new SpringSystem().createSpring(tension, friction);
|
||||
newSpring.setCurrentValue(target);
|
||||
setSpring(newSpring);
|
||||
newSpring.addListener(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
return () => {
|
||||
spring.removeListener(listener);
|
||||
setSpring(null);
|
||||
};
|
||||
}, [tension, friction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spring) {
|
||||
spring.setEndValue(target);
|
||||
if (current != null) {
|
||||
spring.setCurrentValue(current);
|
||||
}
|
||||
}
|
||||
}, [target, current]);
|
||||
|
||||
return value;
|
||||
}
|
66
src/use-spring.story.js
Normal file
66
src/use-spring.story.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import useSpring from "./use-spring";
|
||||
|
||||
function Test() {
|
||||
const [{ target, current }, setState] = React.useState({
|
||||
target: 0,
|
||||
current: null
|
||||
});
|
||||
const value = useSpring({ target, current });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span style={{ flex: 0.3 }}>Target</span>
|
||||
<input
|
||||
value={target}
|
||||
onChange={e =>
|
||||
setState({
|
||||
target: +e.target.value,
|
||||
current: null
|
||||
})
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
type="range"
|
||||
/>
|
||||
<span style={{ flex: 0.3 }}>{target}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span style={{ flex: 0.3 }}>Current</span>
|
||||
<input
|
||||
value={current}
|
||||
onChange={e =>
|
||||
setState({
|
||||
target: +e.target.value,
|
||||
current: +e.target.value
|
||||
})
|
||||
}
|
||||
style={{ flex: 1 }}
|
||||
type="range"
|
||||
/>
|
||||
<span style={{ flex: 0.3 }}>{current}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span style={{ flex: 0.3 }}>Value</span>
|
||||
<input value={value} type="range" readOnly style={{ flex: 1 }} />
|
||||
<span style={{ flex: 0.3 }}>{Math.round(value * 1000) / 1000}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
storiesOf("useSpring", module).add("test", () => (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "90vh"
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "60%" }}>
|
||||
<Test />
|
||||
</div>
|
||||
</div>
|
||||
));
|
26
src/utils.js
Normal file
26
src/utils.js
Normal file
@ -0,0 +1,26 @@
|
||||
export function nextIndex(list, currentIndex) {
|
||||
return Math.min(list.length - 1, Math.floor(currentIndex + 1));
|
||||
}
|
||||
|
||||
export function prevIndex(list, currentIndex) {
|
||||
return Math.max(0, Math.ceil(currentIndex - 1));
|
||||
}
|
||||
|
||||
export function closestIndex(list, currentIndex) {
|
||||
return Math.min(Math.max(0, Math.round(currentIndex)), list.length - 1);
|
||||
}
|
||||
|
||||
export function getScrollTop(area, contentHeight, containerHeight, heights) {
|
||||
const start = heights.slice(0, area.start).reduce((a, b) => a + b, 0);
|
||||
const end =
|
||||
start + heights.slice(area.start, area.end + 1).reduce((a, b) => a + b, 0);
|
||||
const middle = (end + start) / 2;
|
||||
const halfContainer = containerHeight / 2;
|
||||
const bestTop =
|
||||
end - start > containerHeight ? start : middle - halfContainer;
|
||||
if (bestTop < 0) return 0;
|
||||
if (bestTop + containerHeight > contentHeight) {
|
||||
return contentHeight - containerHeight;
|
||||
}
|
||||
return bestTop;
|
||||
}
|
29
src/utils.test.js
Normal file
29
src/utils.test.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { nextIndex, prevIndex } from "./utils";
|
||||
|
||||
describe("nextIndex", () => {
|
||||
const fiveItems = [1, 2, 3, 4, 5];
|
||||
test("works with middle index", () => {
|
||||
expect(nextIndex(fiveItems, 2)).toBe(3);
|
||||
});
|
||||
test("works with last index", () => {
|
||||
expect(nextIndex(fiveItems, 4)).toBe(4);
|
||||
});
|
||||
test("works with fractions", () => {
|
||||
expect(nextIndex(fiveItems, 1.1)).toBe(2);
|
||||
expect(nextIndex(fiveItems, 1.9)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prevIndex", () => {
|
||||
const fiveItems = [1, 2, 3, 4, 5];
|
||||
test("works with middle index", () => {
|
||||
expect(prevIndex(fiveItems, 2)).toBe(1);
|
||||
});
|
||||
test("works with start index", () => {
|
||||
expect(prevIndex(fiveItems, 0)).toBe(0);
|
||||
});
|
||||
test("works with fractions", () => {
|
||||
expect(prevIndex(fiveItems, 1.1)).toBe(1);
|
||||
expect(prevIndex(fiveItems, 1.9)).toBe(1);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user