├── .gitignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ └── index.tsx ├── custom-typings ├── .gitkeep └── atlaskit.d.ts ├── demo.gif ├── example ├── app.tsx ├── index.html ├── index.tsx ├── styled.ts └── utils.ts ├── package.json ├── src ├── index.ts ├── scroll-shadow.tsx └── styled.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | dist 61 | publish_dist -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.5.0 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Hector Zarco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-scroll-shadow [![Build Status](https://travis-ci.org/zzarcon/react-scroll-shadow.svg?branch=master)](https://travis-ci.org/zzarcon/react-scroll-shadow) 2 | > Pure CSS shadow to indicate more content in scrollable area 3 | 4 |
5 | demo 6 |

7 |
8 | 9 | # Demo 🍿 10 | 11 | [https://zzarcon.github.io/react-scroll-shadow](https://zzarcon.github.io/react-scroll-shadow) 12 | 13 | # Install 🚀 14 | 15 | ``` 16 | $ yarn add react-scroll-shadow 17 | ``` 18 | 19 | # Usage ⛏ 20 | 21 | **Basic** 22 | 23 | ```tsx 24 | import ScrollShadow from 'react-scroll-shadow'; 25 | 26 | 27 | Content 28 | 29 | ``` 30 | 31 | **Custom** 32 | 33 | ```tsx 34 | import ScrollShadow from 'react-scroll-shadow'; 35 | 36 | 47 | Content 48 | 49 | ``` 50 | 51 | # Api 📚 52 | 53 | ```ts 54 | interface ShadowColors { 55 | inactive: string; 56 | active: string; 57 | } 58 | 59 | interface Props { 60 | height?: string; 61 | bottomShadowColors?: ShadowColors; 62 | topShadowColors?: ShadowColors; 63 | shadowSize?: number; 64 | } 65 | ``` 66 | 67 | See [example/](https://github.com/zzarcon/react-scroll-shadow/tree/master/example) for full example. -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | import ScrollShadow, {ScrollShadowProps} from '../src'; 4 | import { ScrollableWrapper, ScrollableContent } from '../src/styled'; 5 | 6 | describe('ScrollShadow', () => { 7 | const setup = (props?: Partial) => { 8 | const component = shallow(); 9 | 10 | return { 11 | component 12 | }; 13 | }; 14 | 15 | it('should render children', () => { 16 | const component = shallow( 17 | 18 |
    19 |
      20 |
      21 | ); 22 | 23 | expect(component.find('ul')).toHaveLength(2); 24 | }); 25 | 26 | it('should use the given colors', () => { 27 | const {component} = setup({ 28 | bottomShadowColors: { 29 | inactive: 'blue', 30 | active: 'green' 31 | }, 32 | topShadowColors: { 33 | inactive: '#ccc', 34 | active: 'pink' 35 | } 36 | }); 37 | 38 | expect(component.find(ScrollableWrapper).prop('topShadowActiveColor')).toEqual('pink'); 39 | expect(component.find(ScrollableWrapper).prop('bottomShadowActiveColor')).toEqual('green'); 40 | expect(component.find(ScrollableContent).prop('topShadowInactiveColor')).toEqual('#ccc'); 41 | expect(component.find(ScrollableContent).prop('bottomShadowInactiveColor')).toEqual('blue'); 42 | }); 43 | 44 | it('should set the given height', () => { 45 | const {component} = setup(); 46 | 47 | expect(component.find(ScrollableContent).prop('style')).toEqual({}); 48 | component.setProps({height: '100px'}); 49 | expect(component.find(ScrollableContent).prop('style')).toEqual({height: '100px'}); 50 | }); 51 | 52 | it('should use the given shadow size', () => { 53 | const {component} = setup(); 54 | 55 | expect(component.find(ScrollableWrapper).prop('size')).toEqual(2); 56 | expect(component.find(ScrollableContent).prop('size')).toEqual(2); 57 | component.setProps({shadowSize: 5}); 58 | expect(component.find(ScrollableWrapper).prop('size')).toEqual(5); 59 | expect(component.find(ScrollableContent).prop('size')).toEqual(5); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /custom-typings/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzarcon/react-scroll-shadow/a020d7f2e1a2b42816d404aad0fb9c5c99d0d8fc/custom-typings/.gitkeep -------------------------------------------------------------------------------- /custom-typings/atlaskit.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@atlaskit/*'; -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzarcon/react-scroll-shadow/a020d7f2e1a2b42816d404aad0fb9c5c99d0d8fc/demo.gif -------------------------------------------------------------------------------- /example/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Component, ReactNode, ChangeEvent} from 'react'; 3 | import GHCorner from 'react-gh-corner'; 4 | import ScrollShadow, { ShadowColors } from '../src'; 5 | import {ShadowSize, Title, ScrollWrapper, Item, AppWrapper, AppHeader, AppFooter, ColorWrapper, ColorsWrapper} from './styled'; 6 | 7 | interface AppProps { 8 | 9 | } 10 | 11 | interface ShadowPosition { 12 | bottomShadowColors: ShadowColors; 13 | topShadowColors: ShadowColors; 14 | } 15 | 16 | type AppState = ShadowPosition & { 17 | shadowSize: number; 18 | } 19 | 20 | const items: ReactNode[] = []; 21 | 22 | for (let i = 0; i < 20; i++) { 23 | items.push({i}); 24 | } 25 | 26 | const repoUrl = 'https://github.com/zzarcon/react-scroll-shadow'; 27 | 28 | export default class App extends Component { 29 | state: AppState = { 30 | bottomShadowColors: { 31 | active: '#cccccc', 32 | inactive: '#ffffff' 33 | }, 34 | topShadowColors: { 35 | active: '#cccccc', 36 | inactive: '#ffffff' 37 | }, 38 | shadowSize: 2 39 | }; 40 | 41 | onColorChange = (position: keyof ShadowPosition, property: keyof ShadowColors) => (e: ChangeEvent) => { 42 | const color = e.target.value; 43 | const shadowColors: ShadowColors = { 44 | ...this.state[position], 45 | [property]: color 46 | }; 47 | 48 | this.setState({ 49 | [position]: shadowColors 50 | } as any); 51 | } 52 | 53 | onShadowSizeChange = (e: any) => { 54 | const shadowSize = e.target.value; 55 | this.setState({shadowSize}); 56 | } 57 | 58 | render() { 59 | const {bottomShadowColors, topShadowColors, shadowSize} = this.state; 60 | 61 | return ( 62 | 63 | 64 | 65 | 66 | 🕸 react-scroll-shadow 🕸 67 | 68 | 69 | Top shadow active 70 | 71 | 72 | Top shadow inactive 73 | 74 | 75 | Bottom shadow active 76 | 77 | 78 | Bottom shadow inactive 79 | 80 | Shadow size 81 | 82 | 83 | 84 | Header 85 | 90 | {items} 91 | 92 | Footer 93 | 94 | 95 | ); 96 | } 97 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
      9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | const app = document.getElementById('app'); 6 | 7 | ReactDOM.render(, app); -------------------------------------------------------------------------------- /example/styled.ts: -------------------------------------------------------------------------------- 1 | import styled, {injectGlobal} from 'styled-components'; 2 | 3 | injectGlobal` 4 | body, html { 5 | height: 100%; 6 | margin: 0; 7 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; 8 | font-size: 14px; 9 | } 10 | 11 | body { 12 | background: #69b7eb; 13 | } 14 | 15 | #app { 16 | height: 100%; 17 | } 18 | `; 19 | 20 | export const Item = styled.div` 21 | border-bottom: 1px solid #ccc; 22 | padding: 15px; 23 | margin: 0 20px; 24 | `; 25 | 26 | export const AppWrapper = styled.div` 27 | height: 100%; 28 | width: 800px; 29 | margin: 0 auto; 30 | display: flex; 31 | max-height: 80vh; 32 | max-width: 90vw; 33 | width: 100%; 34 | height: 100%; 35 | position: absolute; 36 | top: 50%; 37 | left: 50%; 38 | transform: translate(-50%, -50%); 39 | border-radius: 3px; 40 | overflow: hidden; 41 | background-color: white; 42 | box-shadow: 1px 5px 5px 1px rgba(0,0,0,.3); 43 | `; 44 | 45 | export const AppHeader = styled.div` 46 | flex-shrink: 0; 47 | height: 40px; 48 | width: 100%; 49 | text-align: center; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | font-size: 20px; 54 | `; 55 | 56 | export const AppFooter = styled.div` 57 | flex-shrink: 0; 58 | height: 40px; 59 | width: 100%; 60 | text-align: center; 61 | display: flex; 62 | align-items: center; 63 | justify-content: center; 64 | font-size: 20px; 65 | `; 66 | 67 | export const ColorWrapper = styled.div` 68 | display: flex; 69 | justify-content: space-between; 70 | height: 50px; 71 | border-bottom: 1px solid #ccc; 72 | align-items: center; 73 | 74 | input { 75 | margin-left: 10px; 76 | } 77 | `; 78 | 79 | export const ColorsWrapper = styled.div` 80 | overflow: auto; 81 | padding: 20px; 82 | border-right: 1px solid #e2e2e2; 83 | `; 84 | 85 | export const ScrollWrapper = styled.div` 86 | flex: 1; 87 | display: flex; 88 | flex-direction: column; 89 | margin: 0 30px; 90 | `; 91 | 92 | export const Title = styled.a` 93 | border-bottom: 2px dashed; 94 | color: #f4d6db; 95 | margin: 0; 96 | font-size: 23px; 97 | margin-bottom: 10px; 98 | text-decoration: none; 99 | display: block; 100 | 101 | &:hover { 102 | border-bottom-style: solid; 103 | } 104 | `; 105 | 106 | export const ShadowSize = styled.input` 107 | outline: none; 108 | margin: 15px; 109 | width: 50px; 110 | text-align: center; 111 | font-size: 15px; 112 | border-radius: 5px; 113 | border: 1px solid #ccc; 114 | `; -------------------------------------------------------------------------------- /example/utils.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | export class ToolboxApp extends Component { 4 | onCheckboxChange = (propName: any) => () => { 5 | const currentValue = (this.state as any)[propName]; 6 | this.setState({ [propName]: !currentValue } as any); 7 | }; 8 | 9 | onFieldTextChange = (propName: any) => (e: any) => { 10 | const value = e.target.value; 11 | 12 | (this as any).setState({ 13 | [propName]: value 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-shadow", 3 | "version": "2.0.2", 4 | "description": "Pure CSS shadow to indicate more content in scrollable area", 5 | "main": "dist/es5/index.js", 6 | "scripts": { 7 | "bootstrap": "ts-react-toolbox init", 8 | "dev": "ts-react-toolbox dev", 9 | "test": "ts-react-toolbox test", 10 | "test:ci": "ts-react-toolbox test --runInBand --coverage", 11 | "build": "ts-react-toolbox build", 12 | "release": "ts-react-toolbox release", 13 | "lint": "ts-react-toolbox lint", 14 | "static": "ts-react-toolbox publish", 15 | "format": "ts-react-toolbox format", 16 | "analyze": "ts-react-toolbox analyze" 17 | }, 18 | "repository": "git@github.com:zzarcon/react-scroll-shadow.git", 19 | "author": "Hector Leon Zarco Garcia ", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "ts-react-toolbox": "^0.1.22" 23 | }, 24 | "dependencies": {}, 25 | "engines": { 26 | "node": ">=8.5.0" 27 | }, 28 | "peerDependencies": { 29 | "react": "^16.3.0", 30 | "styled-components": "^3.4.10" 31 | }, 32 | "types": "dist/es5/index.d.ts", 33 | "files": [ 34 | "dist" 35 | ], 36 | "keywords": [ 37 | "react", 38 | "scroll", 39 | "shadow", 40 | "overflow", 41 | "height", 42 | "css", 43 | "pure-css", 44 | "scrollable", 45 | "scrollbar" 46 | ], 47 | "jsnext:main": "dist/es2015/index.js", 48 | "module": "dist/es2015/index.js" 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scroll-shadow'; 2 | export {ScrollShadow as default} from './scroll-shadow'; -------------------------------------------------------------------------------- /src/scroll-shadow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Component} from 'react'; 3 | import {ScrollableContent, ScrollableWrapper} from './styled'; 4 | 5 | export interface ShadowColors { 6 | inactive: string; 7 | active: string; 8 | } 9 | 10 | export interface ScrollShadowProps { 11 | height?: string; 12 | bottomShadowColors?: ShadowColors; 13 | topShadowColors?: ShadowColors; 14 | shadowSize?: number; 15 | } 16 | 17 | export class ScrollShadow extends Component { 18 | static defaultProps = { 19 | shadowSize: 2, 20 | bottomShadowColors: { 21 | inactive: 'white', 22 | active: 'gray' 23 | }, 24 | topShadowColors: { 25 | inactive: 'white', 26 | active: 'gray' 27 | } 28 | }; 29 | render() { 30 | const { 31 | children, 32 | height, 33 | bottomShadowColors, 34 | topShadowColors, 35 | shadowSize 36 | } = this.props; 37 | const style = {height}; 38 | 39 | return ( 40 | 45 | 51 | {children} 52 | 53 | 54 | ); 55 | } 56 | } 57 | 58 | export default ScrollShadow; -------------------------------------------------------------------------------- /src/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | // @ts-ignore: unused variable 3 | // prettier-ignore 4 | import { HTMLAttributes, ClassAttributes } from 'react'; 5 | 6 | export interface ScrollableWrapperProps { 7 | size: number; 8 | topShadowActiveColor: string; 9 | bottomShadowActiveColor: string; 10 | } 11 | 12 | export interface ScrollableContentProps { 13 | size: number; 14 | topShadowInactiveColor: string; 15 | bottomShadowInactiveColor: string; 16 | } 17 | 18 | export const ScrollableWrapper = styled.div` 19 | display: flex; 20 | flex: 1 1 auto; 21 | width: 100%; 22 | position: relative; 23 | // Next line is important hack/fix for Firefox 24 | // https://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox 25 | min-height:0; 26 | 27 | &::before{ 28 | content: ''; 29 | position: absolute; 30 | top:0; 31 | left:0; 32 | right:0; 33 | height: ${({size}: ScrollableWrapperProps) => size}px; 34 | background: ${({topShadowActiveColor}: ScrollableWrapperProps) => topShadowActiveColor}; 35 | z-index: 10; 36 | } 37 | &::after{ 38 | content: ''; 39 | position: absolute; 40 | bottom:0; 41 | left:0; 42 | right:0; 43 | height: ${({size}: ScrollableWrapperProps) => size}px; 44 | background: ${({bottomShadowActiveColor}: ScrollableWrapperProps) => bottomShadowActiveColor}; 45 | z-index: 10; 46 | } 47 | `; 48 | 49 | export const ScrollableContent = styled.div` 50 | flex: 1 1 auto; 51 | overflow-y: auto; 52 | overflow-x: hidden; 53 | 54 | display: flex; 55 | flex-direction: column; 56 | 57 | &::before{ 58 | content: ''; 59 | height: ${({size}: ScrollableContentProps) => size}px; 60 | width: 100%; 61 | background: ${({topShadowInactiveColor}: ScrollableContentProps) => topShadowInactiveColor}; 62 | flex-shrink: 0; 63 | 64 | z-index: 11; 65 | // Next line is important hack/fix for Safari 66 | // https://stackoverflow.com/questions/40895387/z-index-not-working-on-safari-fine-on-firefox-and-chrome 67 | transform: translate3d(0,0,0); 68 | } 69 | &::after{ 70 | content: ''; 71 | height: ${({size}: ScrollableContentProps) => size}px;; 72 | width: 100%; 73 | background: ${({bottomShadowInactiveColor}: ScrollableContentProps) => bottomShadowInactiveColor}; 74 | flex-grow: 1; 75 | flex-shrink: 0; 76 | 77 | z-index: 11; 78 | // Next line is important hack/fix for Safari 79 | // https://stackoverflow.com/questions/40895387/z-index-not-working-on-safari-fine-on-firefox-and-chrome 80 | transform: translate3d(0,0,0); 81 | } 82 | `; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "removeComments": true, 5 | "target": "es5", 6 | "lib": [ 7 | "dom", 8 | "es5", 9 | "scripthost", 10 | "es2015.collection", 11 | "es2015.symbol", 12 | "es2015.iterable", 13 | "es2015.promise" 14 | ], 15 | "jsx": "react" 16 | } 17 | } --------------------------------------------------------------------------------