├── .gitignore ├── .npmignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── Readme.md ├── babel.config.js ├── demo.gif ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── ChevronLeft.tsx ├── Stack.tsx ├── StackContext.ts ├── StackItem.tsx ├── StackTitle.tsx ├── index.test.ts ├── index.ts ├── styles.css └── use-measure.ts ├── stories └── intro.stories.tsx ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | build 4 | dist 5 | .rpt2_cache 6 | .DS_Store 7 | .env 8 | .env.local 9 | .env.development.local 10 | .env.test.local 11 | .env.production.local 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | esm 16 | cjs -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmcmahen/react-gesture-stack/fe0039f811100babdcfa2ff5c5836784e13b91de/.npmignore -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmcmahen/react-gesture-stack/fe0039f811100babdcfa2ff5c5836784e13b91de/.storybook/addons.js -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | const req = require.context("../stories", true, /.stories.tsx$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | use: [ 5 | { 6 | loader: require.resolve("babel-loader") 7 | }, 8 | { 9 | loader: require.resolve("awesome-typescript-loader") 10 | } 11 | ] 12 | }); 13 | 14 | config.resolve.extensions.push(".ts", ".tsx", ".json"); 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 |
2 | A demo showing views being swiped left and right. 6 |
7 | 8 | # react-gesture-stack 9 | 10 | [![npm package](https://img.shields.io/npm/v/react-gesture-stack/latest.svg)](https://www.npmjs.com/package/react-gesture-stack) 11 | [![Follow on Twitter](https://img.shields.io/twitter/follow/benmcmahen.svg?style=social&logo=twitter)](https://twitter.com/intent/follow?screen_name=benmcmahen) 12 | 13 | React-gesture-stack provides an iOS stack-like interface for use on the web. It supports gestures to "go back" in the stack. View the above example [on CodeSandbox](https://codesandbox.io/embed/damp-monad-ukvcu). 14 | 15 | This was originally built for use in [Sancho-UI](https://github.com/bmcmahen/sancho). 16 | 17 | ## Install 18 | 19 | Install `react-gesture-stack` and its peer dependency `react-gesture-responder` using yarn or npm. 20 | 21 | ``` 22 | yarn add react-gesture-stack react-gesture-responder 23 | ``` 24 | 25 | ## Basic usage 26 | 27 | ```jsx 28 | import { Stack, StackItem, StackTitle } from "react-gesture-stack"; 29 | // optional styles 30 | import "react-gesture-stack/src/styles.css"; 31 | 32 | function Simple() { 33 | const [index, setIndex] = React.useState(0); 34 | 35 | return ( 36 | setIndex(i)} 38 | index={index} 39 | style={{ width: "400px", height: "600px" }} 40 | items={[ 41 | { 42 | title: , 43 | content: ( 44 | 45 | 46 | 47 | ) 48 | }, 49 | { 50 | title: , 51 | content: ( 52 | 53 | 54 | 55 | ) 56 | }, 57 | { 58 | title: , 59 | content: ( 60 | 61 |
No more!
62 |
63 | ) 64 | } 65 | ]} 66 | /> 67 | ); 68 | } 69 | ``` 70 | 71 | ## API 72 | 73 | ### Stack 74 | 75 | | Name | Type | Default Value | Description | 76 | | ---------------- | -------------------- | ------------- | ------------------------------------------------------- | 77 | | index \* | number | | The index of stack item to show | 78 | | onIndexChange \* | (i: number) => void; | | A callback requesting the active stack item change | 79 | | items \* | StackItemList[] | | A list of stack items to render (see the above example) | 80 | | disableNav | boolean | false | Hide the top navigation pane | 81 | | navHeight | number | 50 | The height of the navigation pane (in px) | 82 | 83 | ### StackItem 84 | 85 | | Name | Type | Default Value | Description | 86 | | -------- | ---------- | ------------- | ------------------------- | 87 | | style | object | | Optional style attributes | 88 | | children | React.Node | | Content of the stack item | 89 | 90 | ### StackTitle 91 | 92 | | Name | Type | Default Value | Description | 93 | | ------------- | ---------- | ------------- | ---------------------------------------------------------------------------- | 94 | | title | React.Node | | The title of the stack item | 95 | | backTitle | string | "Back" | The title of the back button | 96 | | contentAfter | React.Node | | Content that appears to the right of the title | 97 | | contentBefore | React.Node | | Content that appears to the left of the title (and replaces the back button) | 98 | | backButton | React.Node | | Render a custom back button. You're responsible for listening to click handlers and updating the current index| 99 | 100 | ## License 101 | 102 | MIT 103 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | useBuiltIns: "usage", 7 | corejs: 2, 8 | targets: { node: "6" } 9 | } 10 | ], 11 | "@babel/preset-react", 12 | "@babel/preset-typescript" 13 | ], 14 | env: { 15 | test: { 16 | plugins: ["require-context-hook"] 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmcmahen/react-gesture-stack/fe0039f811100babdcfa2ff5c5836784e13b91de/demo.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | transform: { 4 | "^.+\\.tsx?$": "ts-jest" 5 | }, 6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 7 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"] 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gesture-stack", 3 | "version": "1.3.1", 4 | "description": "iOS-style stacking views built for the web", 5 | "main": "cjs/index.js", 6 | "module": "esm/index.js", 7 | "typings": "esm/index.d.ts", 8 | "author": "Ben McMahen", 9 | "license": "MIT", 10 | "private": false, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/bmcmahen/react-gesture-stack.git" 14 | }, 15 | "scripts": { 16 | "test": "jest", 17 | "test-watch": "jest -w", 18 | "storybook": "start-storybook -p 6006", 19 | "build-esm": "rimraf esm && tsc", 20 | "build-other": "rimraf umd && rimraf cjs && rollup -c", 21 | "build": "yarn run build-esm && yarn run build-other", 22 | "prepublishOnly": "yarn run build" 23 | }, 24 | "peerDependencies": { 25 | "react": "^16.8.6", 26 | "react-dom": "^16.8.6", 27 | "react-gesture-responder": "^2.1.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.4.0", 31 | "@babel/preset-env": "^7.4.2", 32 | "@babel/preset-react": "^7.0.0", 33 | "@babel/preset-typescript": "^7.3.3", 34 | "@storybook/react": "^5.0.5", 35 | "@types/faker": "^4.1.5", 36 | "@types/jest": "^24.0.11", 37 | "@types/storybook__react": "^4.0.1", 38 | "awesome-typescript-loader": "^5.2.1", 39 | "babel-core": "^6.26.3", 40 | "babel-jest": "^24.5.0", 41 | "babel-loader": "^8.0.5", 42 | "babel-plugin-require-context-hook": "^1.0.0", 43 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 44 | "faker": "^4.1.0", 45 | "jest": "^24.5.0", 46 | "react": "^16.8.6", 47 | "react-dom": "^16.8.6", 48 | "react-gesture-responder": "^2.1.0", 49 | "rimraf": "^2.6.3", 50 | "rollup": "^1.7.4", 51 | "rollup-plugin-babel": "^4.3.2", 52 | "rollup-plugin-cleanup": "^3.1.1", 53 | "rollup-plugin-commonjs": "^9.2.2", 54 | "rollup-plugin-filesize": "^6.0.1", 55 | "rollup-plugin-json": "^4.0.0", 56 | "rollup-plugin-node-resolve": "^4.0.1", 57 | "rollup-plugin-sourcemaps": "^0.4.2", 58 | "rollup-plugin-typescript2": "^0.20.1", 59 | "rollup-plugin-uglify": "^6.0.2", 60 | "sancho": "^3.2.1", 61 | "ts-jest": "^24.0.1", 62 | "typescript": "^3.4.1", 63 | "webpack": "^4.29.6" 64 | }, 65 | "dependencies": { 66 | "@types/react": "^16.8.10", 67 | "@types/react-dom": "^16.8.3", 68 | "react-spring": "^9.0.0-beta.31", 69 | "resize-observer-polyfill": "^1.5.1", 70 | "tslib": "^1.9.3", 71 | "use-scroll-lock": "^1.0.0" 72 | }, 73 | "sideEffects": false 74 | } 75 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import filesize from "rollup-plugin-filesize"; 3 | import pkg from "./package.json"; 4 | import commonjs from "rollup-plugin-commonjs"; 5 | import cleanup from "rollup-plugin-cleanup"; 6 | import json from "rollup-plugin-json"; 7 | import typescript from "rollup-plugin-typescript2"; 8 | 9 | const input = "src/index.ts"; 10 | 11 | const plugins = [ 12 | resolve(), 13 | typescript({ 14 | typescript: require("typescript") 15 | }), 16 | commonjs(), 17 | json(), 18 | cleanup(), 19 | filesize() 20 | ]; 21 | 22 | const externals = [ 23 | ...Object.keys(pkg.dependencies || {}), 24 | ...Object.keys(pkg.peerDependencies || {}) 25 | ]; 26 | 27 | export default [ 28 | { 29 | input, 30 | output: [ 31 | { 32 | file: pkg.main, 33 | format: "cjs", 34 | sourcemap: true 35 | } 36 | ], 37 | external: externals, 38 | plugins 39 | } 40 | ]; 41 | -------------------------------------------------------------------------------- /src/ChevronLeft.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface ChevronIconProps { 4 | color: string; 5 | size: number; 6 | } 7 | export const IconChevronLeft: React.FunctionComponent = ({ 8 | color, 9 | size, 10 | ...props 11 | }) => { 12 | return ( 13 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/Stack.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useGestureResponder, StateType } from "react-gesture-responder"; 3 | import { StackContext } from "./StackContext"; 4 | import { useSprings } from "react-spring"; 5 | import { useMeasure } from "./use-measure"; 6 | import useScrollLock from "use-scroll-lock"; 7 | 8 | /** 9 | * Get position of stack items 10 | */ 11 | 12 | function getAnimationValues(i: number, currentIndex: number) { 13 | // current 14 | if (i === currentIndex) { 15 | return { left: 0, immediate: false, opacity: 1, overlay: 0 }; 16 | } 17 | 18 | // next 19 | else if (i > currentIndex) { 20 | return { left: 100, immediate: false, opacity: 0, overlay: 0 }; 21 | } 22 | 23 | // previous 24 | return { left: -50, immediate: false, opacity: 0, overlay: 1 }; 25 | } 26 | 27 | /** 28 | * Stack manager 29 | */ 30 | 31 | interface StackItemList { 32 | title?: React.ReactNode; 33 | content: React.ReactNode; 34 | } 35 | 36 | export interface StackProps extends React.HTMLAttributes { 37 | index: number; 38 | onIndexChange: (index: number) => void; 39 | items: StackItemList[]; 40 | disableNav?: boolean; 41 | navHeight?: number; 42 | disableScroll?: boolean; 43 | } 44 | 45 | export const Stack: React.FunctionComponent = ({ 46 | style, 47 | children, 48 | index, 49 | disableNav, 50 | disableScroll = true, 51 | navHeight = 50, 52 | items, 53 | onIndexChange, 54 | ...other 55 | }) => { 56 | const ref = React.useRef(null); 57 | const count = items.length; 58 | const bounds = useMeasure(ref); 59 | const [dragging, setDragging] = React.useState(false); 60 | 61 | useScrollLock(dragging && disableScroll); 62 | 63 | // set default positions 64 | const [springs, set] = useSprings(count, i => { 65 | return getAnimationValues(i, index); 66 | }); 67 | 68 | React.useEffect(() => { 69 | set(i => getAnimationValues(i, index)); 70 | }, [index, set]); 71 | 72 | // handle termination / gesture end 73 | // either return to current position or 74 | // animate to the previous index. 75 | function onEnd({ delta, velocity, direction }: StateType) { 76 | const { width } = bounds; 77 | const [x] = delta; 78 | const xp = (x / width) * 100; 79 | 80 | setDragging(false); 81 | 82 | // go back if force is great enouggh 83 | if (direction[0] === 1 && velocity > 0.25) { 84 | return onIndexChange(index - 1); 85 | } 86 | 87 | // go back if distance > 50% 88 | if (xp > 50) { 89 | return onIndexChange(index - 1); 90 | } 91 | 92 | // otherwise, reset indexes 93 | set(i => getAnimationValues(i, index)); 94 | } 95 | 96 | const { bind } = useGestureResponder({ 97 | onStartShouldSet: () => false, 98 | onMoveShouldSet: ({ initialDirection }) => { 99 | // only engage on a swipe back and if there is something 100 | // to swipe back towards 101 | 102 | const isHorizontal = 103 | Math.abs(initialDirection[0]) > Math.abs(initialDirection[1]); 104 | 105 | if (isHorizontal && initialDirection[0] > 0 && index > 0) { 106 | setDragging(true); 107 | return true; 108 | } 109 | 110 | return false; 111 | }, 112 | onRelease: onEnd, 113 | onTerminate: onEnd, 114 | onMove: ({ delta }) => { 115 | const { width } = bounds; 116 | const [x] = delta; 117 | const xp = (x / width) * 100; 118 | 119 | // prevent over dragging to the left 120 | if (x < 0) return; 121 | 122 | set(i => { 123 | // animate our current pane 124 | if (i === index) { 125 | return { 126 | immediate: true, 127 | left: xp, 128 | opacity: (100 - xp) / 100, 129 | overlay: 0 130 | }; 131 | } 132 | 133 | // animate our previous pane 134 | if (i === index - 1) { 135 | const dx = 100 - xp; 136 | return { 137 | immediate: true, 138 | left: (dx / 2) * -1, 139 | opacity: xp / 100, 140 | overlay: (100 - xp) / 100 141 | }; 142 | } 143 | 144 | return getAnimationValues(i, index); 145 | }); 146 | } 147 | }); 148 | 149 | return ( 150 | 151 |
162 | {!disableNav && ( 163 |
171 | {springs.map((props, i) => { 172 | return ( 173 | clamp(x)), 184 | changeIndex: onIndexChange 185 | }} 186 | > 187 | {items[i].title} 188 | 189 | ); 190 | })} 191 |
192 | )} 193 |
201 | {springs.map((props, i) => { 202 | return ( 203 | clamp(x)), 214 | changeIndex: onIndexChange 215 | }} 216 | > 217 | {items[i].content} 218 | 219 | ); 220 | })} 221 |
222 |
223 |
224 | ); 225 | }; 226 | 227 | // ensure values don't exceed our bounds 228 | // when dragging 229 | function clamp(x: number) { 230 | if (x > 100) { 231 | return 100; 232 | } 233 | 234 | if (x < -50) { 235 | return -50; 236 | } 237 | 238 | return x; 239 | } 240 | -------------------------------------------------------------------------------- /src/StackContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SpringValue } from "react-spring"; 3 | 4 | interface StackContextType { 5 | index: number; 6 | activeIndex: number; 7 | active: boolean; 8 | dragging: boolean; 9 | navHeight: number; 10 | overlay?: SpringValue; 11 | opacity?: SpringValue; 12 | transform?: SpringValue; 13 | changeIndex: (index: number) => void; 14 | } 15 | 16 | export const StackContext = React.createContext({ 17 | index: 0, 18 | activeIndex: 0, 19 | dragging: false, 20 | navHeight: 50, 21 | active: false, 22 | changeIndex: () => {} 23 | }); 24 | -------------------------------------------------------------------------------- /src/StackItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StackContext } from "./StackContext"; 3 | import { animated } from "react-spring"; 4 | 5 | export interface StackItemProps extends React.HTMLAttributes { 6 | generateShadow?: (x: number) => string; 7 | } 8 | 9 | export function StackItem({ 10 | style, 11 | generateShadow, 12 | children, 13 | className = "", 14 | ...other 15 | }: StackItemProps) { 16 | const { index, opacity, overlay, active, transform } = React.useContext( 17 | StackContext 18 | ); 19 | 20 | if (!transform || !opacity || !overlay) { 21 | throw new Error("Stack must be used as a child of StackManager"); 22 | } 23 | 24 | const cx = `StackItem StackItem-${index} ${ 25 | active ? "StackItem-active" : "" 26 | } ${className}`; 27 | 28 | return ( 29 | 40 | generateShadow 41 | ? generateShadow(x) 42 | : `0 0 12px -2px rgba(160,160,160,${x})` 43 | ), 44 | transform: transform.to(x => `translateX(${x}%)`), 45 | ...style 46 | }} 47 | {...other} 48 | > 49 | {children} 50 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/StackTitle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StackContext } from "./StackContext"; 3 | import { animated } from "react-spring"; 4 | import { IconChevronLeft } from "./ChevronLeft"; 5 | 6 | export interface StackTitleProps { 7 | title?: React.ReactNode; 8 | style?: React.CSSProperties; 9 | backButton?: React.ReactNode; 10 | backTitle?: React.ReactNode; 11 | contentBefore?: React.ReactNode; 12 | contentAfter?: React.ReactNode; 13 | } 14 | 15 | const ellipsis = { 16 | textOverflow: "ellipsis", 17 | whiteSpace: "nowrap", 18 | overflow: "hidden" 19 | } as any; // eh? 20 | 21 | export function StackTitle({ 22 | title, 23 | backTitle = "Back", 24 | contentAfter, 25 | contentBefore, 26 | backButton, 27 | style 28 | }: StackTitleProps) { 29 | const { 30 | navHeight, 31 | index, 32 | active, 33 | changeIndex, 34 | opacity, 35 | transform 36 | } = React.useContext(StackContext); 37 | 38 | if (!transform || !opacity) { 39 | throw new Error("StackTitle must be used within a Stack component"); 40 | } 41 | 42 | return ( 43 |
55 |
66 | `translateX(${x * 0.7}%)`) 73 | }} 74 | > 75 | {index > 0 && 76 | !contentBefore && 77 | (backButton || ( 78 | 94 | ))} 95 | {contentBefore} 96 | 97 | `translateX(${x * 0.85}%)`) 106 | }} 107 | > 108 | {title} 109 | 110 | 120 | {contentAfter} 121 | 122 |
123 |
124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | test("hello", () => { 2 | expect("hello").toBeTruthy(); 3 | }); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./StackItem"; 2 | export * from "./Stack"; 3 | export * from "./StackContext"; 4 | export * from "./StackTitle"; 5 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .StackTitle { 2 | margin: 0; 3 | font-weight: 400; 4 | line-height: 1.5; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 6 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 7 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 8 | font-size: 1rem; 9 | color: #212529; 10 | box-sizing: border-box; 11 | 12 | -webkit-font-smoothing: antialiased; 13 | -webkit-text-size-adjust: none; 14 | } 15 | 16 | .StackTitle__heading { 17 | font-weight: 500; 18 | } 19 | 20 | .Stack__nav { 21 | box-shadow: rgba(52, 58, 64, 0.1) 0px 0px 1px, 22 | rgba(52, 58, 64, 0.12) 0px 0px 1px 1px; 23 | background: white; 24 | } 25 | 26 | .StackTitle__button-back { 27 | white-space: nowrap; 28 | -webkit-appearance: none; 29 | box-sizing: border-box; 30 | text-align: center; 31 | -webkit-font-smoothing: antialiased; 32 | text-rendering: optimizelegibility; 33 | user-select: none; 34 | -webkit-tap-highlight-color: transparent; 35 | cursor: pointer; 36 | font-weight: 500; 37 | position: relative; 38 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 39 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 40 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 41 | font-size: 1rem; 42 | height: 40px; 43 | display: inline-flex; 44 | -webkit-box-align: center; 45 | padding-right: 0.25rem; 46 | align-items: center; 47 | -webkit-box-pack: center; 48 | justify-content: center; 49 | color: rgb(25, 113, 194); 50 | box-shadow: none; 51 | text-decoration: none; 52 | border-radius: 0.25rem; 53 | padding: 0; 54 | border: none; 55 | background: none; 56 | } 57 | -------------------------------------------------------------------------------- /src/use-measure.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ResizeObserver from "resize-observer-polyfill"; 3 | 4 | export interface Bounds { 5 | left: number; 6 | height: number; 7 | top: number; 8 | width: number; 9 | } 10 | 11 | export function useMeasure(ref: React.RefObject) { 12 | const [bounds, setBounds] = React.useState({ 13 | left: 0, 14 | top: 0, 15 | width: 0, 16 | height: 0 17 | }); 18 | 19 | const [observer] = React.useState( 20 | () => 21 | new ResizeObserver(([entry]) => { 22 | setBounds(entry.contentRect); 23 | }) 24 | ); 25 | 26 | React.useEffect(() => { 27 | if (ref.current) { 28 | // i don't know why, but sometimes our resize observer callback isn't 29 | // called upon initial creation. So I'm doing this just for safety. 30 | const { left, top, width, height } = ref.current.getBoundingClientRect(); 31 | setBounds({ left, top, width, height }); 32 | observer.observe(ref.current); 33 | } 34 | return () => observer.disconnect(); 35 | }, [observer, ref, setBounds]); 36 | 37 | return bounds; 38 | } 39 | -------------------------------------------------------------------------------- /stories/intro.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import * as faker from "faker"; 4 | import "../src/styles.css"; 5 | import { 6 | ScrollView, 7 | List, 8 | ListItem, 9 | Avatar, 10 | IconChevronRight, 11 | useTheme, 12 | Layer, 13 | IconPlus, 14 | IconButton 15 | } from "sancho"; 16 | 17 | import { StackItem, Stack, StackContext } from "../src"; 18 | import { StackTitle } from "../src/StackTitle"; 19 | 20 | function Simple() { 21 | const [index, setIndex] = React.useState(0); 22 | const { changeIndex } = React.useContext(StackContext); 23 | 24 | function goback() { 25 | return setIndex(index - 1); 26 | } 27 | 28 | return ( 29 | setIndex(i)} 31 | index={index} 32 | style={{ width: "400px", height: "600px" }} 33 | items={[ 34 | { 35 | title: , 36 | content: ( 37 | 38 | 39 | 40 | ) 41 | }, 42 | { 43 | title: ( 44 | BACK} 46 | title="Second title" 47 | /> 48 | ), 49 | content: ( 50 | `-8px -8px 8px rgba(0,0,0,${x})`}> 51 | 52 | 53 | ) 54 | }, 55 | { 56 | title: , 57 | content: ( 58 | 59 |
No more!
60 |
61 | ) 62 | } 63 | ]} 64 | /> 65 | ); 66 | } 67 | 68 | function getUser() { 69 | faker.seed(0); 70 | 71 | return { 72 | name: faker.name.firstName() + " " + faker.name.lastName(), 73 | uid: faker.random.uuid(), 74 | description: faker.lorem.sentence() 75 | }; 76 | } 77 | 78 | function ListDetail() { 79 | const theme = useTheme(); 80 | const [index, setIndex] = React.useState(0); 81 | const [items, setItems] = React.useState( 82 | Array.from(new Array(10)).map(() => getUser()) 83 | ); 84 | 85 | function next() { 86 | setIndex(index + 1); 87 | } 88 | 89 | function onChange(i: number) { 90 | setIndex(i); 91 | } 92 | 93 | return ( 94 |
101 | 109 | , 118 | content: ( 119 | 120 |
121 | 122 | setIndex(index + 1)} 124 | primary="All" 125 | contentAfter={ 126 | 127 | } 128 | /> 129 | setIndex(index + 1)} 131 | primary="Family" 132 | contentAfter={ 133 | 134 | } 135 | /> 136 | setIndex(index + 1)} 138 | primary="Friends" 139 | contentAfter={ 140 | 141 | } 142 | /> 143 | 144 |
145 |
146 | ) 147 | }, 148 | { 149 | title: ( 150 | } 158 | /> 159 | } 160 | /> 161 | ), 162 | content: ( 163 | 164 |
171 |
172 | 173 | {items.map(item => ( 174 | setIndex(index + 1)} 177 | contentBefore={} 178 | primary={item.name} 179 | secondary={item.description} 180 | contentAfter={ 181 | 184 | } 185 | /> 186 | ))} 187 | 188 |
189 |
190 |
191 | ) 192 | }, 193 | { 194 | title: , 195 | content: ( 196 | 197 |
207 | 208 | ) 209 | } 210 | ]} 211 | onIndexChange={onChange} 212 | index={index} 213 | /> 214 | 215 |
216 | ); 217 | } 218 | 219 | storiesOf("Hello", module) 220 | .add("List detail", () => ) 221 | .add("simple", () => ); 222 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "declaration": true, 6 | "declarationDir": "esm", 7 | "esModuleInterop": true, 8 | "importHelpers": true, 9 | "jsx": "react", 10 | "lib": ["es2015", "esnext.asynciterable", "dom"], 11 | "module": "esnext", 12 | "target": "es5", 13 | "outDir": "esm", 14 | "skipLibCheck": true, 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": false, 18 | "strict": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "allowJs": false 21 | }, 22 | "includes": ["src"], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "stories", 27 | "tests", 28 | "esm", 29 | "src/**/__tests__" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------