diff --git a/packages/carousel/.babelrc b/packages/carousel/.babelrc new file mode 100644 index 0000000000..b1c4d85635 --- /dev/null +++ b/packages/carousel/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + "react", + "stage-2", + [ + "env", + { + "targets": { + "browsers": ["last 2 versions", "ie >= 11"] + } + } + ] + ] +} diff --git a/packages/carousel/.npmignore b/packages/carousel/.npmignore new file mode 100644 index 0000000000..8ff9f4ada2 --- /dev/null +++ b/packages/carousel/.npmignore @@ -0,0 +1,3 @@ +__specs__ +src +.babelrc \ No newline at end of file diff --git a/packages/carousel/.storybook/addons.js b/packages/carousel/.storybook/addons.js new file mode 100644 index 0000000000..a68dac4af3 --- /dev/null +++ b/packages/carousel/.storybook/addons.js @@ -0,0 +1,6 @@ +import '@storybook/addon-actions/register' + +import addons from '@storybook/addons' +import register from '@pluralsight/ps-design-system-storybook-addon-theme/register' + +register(addons) diff --git a/packages/carousel/.storybook/config.js b/packages/carousel/.storybook/config.js new file mode 100644 index 0000000000..8e76337f51 --- /dev/null +++ b/packages/carousel/.storybook/config.js @@ -0,0 +1,14 @@ +import addons from '@storybook/addons' +import { addDecorator, configure } from '@storybook/react' + +import centerDecorator from '@pluralsight/ps-design-system-storybook-addon-center' +import themeDecorator from '@pluralsight/ps-design-system-storybook-addon-theme' + +addDecorator(centerDecorator) +addDecorator(themeDecorator(addons)) + +function loadStory() { + require('../src/react/__stories__/index.story.js') +} + +configure(loadStory, module) diff --git a/packages/carousel/.storybook/preview-head.html b/packages/carousel/.storybook/preview-head.html new file mode 100644 index 0000000000..c59650daf2 --- /dev/null +++ b/packages/carousel/.storybook/preview-head.html @@ -0,0 +1,2 @@ + + diff --git a/packages/carousel/css.js b/packages/carousel/css.js new file mode 100644 index 0000000000..7b24a76a0a --- /dev/null +++ b/packages/carousel/css.js @@ -0,0 +1 @@ +module.exports = require('./dist/css/index.js') diff --git a/packages/carousel/index.js b/packages/carousel/index.js new file mode 100644 index 0000000000..60ceb3c12e --- /dev/null +++ b/packages/carousel/index.js @@ -0,0 +1,5 @@ +const css = require('./css/index.js') +const react = require('./react/index.js') +const vars = require('./vars/index.js') + +module.exports = { css: css, react: react, vars: vars } diff --git a/packages/carousel/jest.config.js b/packages/carousel/jest.config.js new file mode 100644 index 0000000000..9404904389 --- /dev/null +++ b/packages/carousel/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('../../jest/base.config.js') +const { name } = require('./package.json') + +module.exports = { + ...baseConfig, + displayName: name, + name: name, + testMatch: [`${__dirname}/**/*/?(*.)+(spec|test).js`] +} diff --git a/packages/carousel/package.json b/packages/carousel/package.json new file mode 100644 index 0000000000..a87fd4da25 --- /dev/null +++ b/packages/carousel/package.json @@ -0,0 +1,47 @@ +{ + "name": "@pluralsight/ps-design-system-carousel", + "version": "1.0.0", + "description": "Carousel UI Component for the Pluralsight Design System", + "license": "Apache-2.0", + "repository": "pluralsight/design-system", + "main": "index.js", + "module": "index.js", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "run-s build:js build:css", + "build:css": "build-css --useGlamor", + "build:js": "babel src --out-dir dist --ignore spec.js --copy-files", + "build:watch": "npm run build:js -- --watch", + "prepublish": "npm run build", + "storybook": "start-storybook -p 6006", + "test": "../../node_modules/.bin/jest", + "test:updateSnapshot": "npm run test -- --updateSnapshot", + "test:watch": "npm run test -- --watchAll" + }, + "dependencies": { + "@pluralsight/ps-design-system-core": "*", + "@pluralsight/ps-design-system-filter-react-props": "*", + "@pluralsight/ps-design-system-theme": "*" + }, + "peerDependencies": { + "@pluralsight/ps-design-system-normalize": "*", + "glamor": "^2.x.x", + "react": ">=16.8.6 < 17.0.0" + }, + "devDependencies": { + "@pluralsight/ps-design-system-build": "*", + "@pluralsight/ps-design-system-storybook-addon-center": "*", + "@pluralsight/ps-design-system-storybook-addon-theme": "*", + "@storybook/addon-actions": "^4.0.0", + "@storybook/addon-storyshots": "^4.0.0", + "@storybook/addons": "^4.0.0", + "@storybook/react": "^4.0.0", + "babel-cli": "^6.24.1", + "babel-preset-env": "^1.6.0", + "babel-preset-react": "^6.24.1", + "babel-preset-stage-2": "^6.24.1", + "npm-run-all": "^4.1.2" + } +} diff --git a/packages/carousel/react.js b/packages/carousel/react.js new file mode 100644 index 0000000000..091a641f3b --- /dev/null +++ b/packages/carousel/react.js @@ -0,0 +1 @@ +module.exports = require('./dist/react/index.js') diff --git a/packages/carousel/src/css/index.js b/packages/carousel/src/css/index.js new file mode 100644 index 0000000000..3e2f88a279 --- /dev/null +++ b/packages/carousel/src/css/index.js @@ -0,0 +1,79 @@ +import core from '@pluralsight/ps-design-system-core' + +export const resetButton = { + background: 'transparent', + border: 'none', + color: 'inherit', + font: 'inherit', + lineHeight: 'normal', + margin: 0, + overflow: 'visible', + padding: 0, + width: 'auto', + + MozOsxFontSmoothing: 'inherit', + WebkitAppearance: 'none', + WebkitFontSmoothing: 'inherit', + + '&::-moz-focus-inner': { + border: 0, + padding: 0 + } +} + +export default { + '.psds-carousel': { + position: 'relative', + outline: '1px solid yellow' + }, + + '.psds-carousel__controls': {}, + '.psds-carousel__controls__control': { + ...resetButton, + background: 'white', + borderRadius: '100%', + height: '36px', + position: 'absolute', + top: '50%', + width: '36px', + + '&:hover': { + cursor: 'pointer' + } + }, + '.psds-carousel__controls__control--prev': { + left: 0, + transform: 'translate(-50%, -50%)' + }, + '.psds-carousel__controls__control--next': { + right: 0, + transform: 'translate(50%, -50%)' + }, + + '.psds-carousel__pages': { + display: 'flex', + overflow: 'hidden', + width: '100%' + }, + + '.psds-carousel__page': { + alignItems: 'flex-start', + display: 'flex', + flex: '1 0 100%', + margin: `0 calc(${core.layout.spacingSmall}/2)`, + transition: `transform ${core.motion.speedXSlow} ease-in-out`, + + outline: '3px solid red', + + '&:first-child': { marginLeft: 0 }, + '&:last-child': { marginRight: 0 } + }, + + '.psds-carousel__item': { + margin: `0 calc(${core.layout.spacingSmall}/2)`, + flex: 1, + + '&:first-child': { marginLeft: 0 }, + '&:last-child': { marginRight: 0 } + } +} diff --git a/packages/carousel/src/react/__specs__/__snapshots__/storyshots.spec.js.snap b/packages/carousel/src/react/__specs__/__snapshots__/storyshots.spec.js.snap new file mode 100644 index 0000000000..38560e8813 --- /dev/null +++ b/packages/carousel/src/react/__specs__/__snapshots__/storyshots.spec.js.snap @@ -0,0 +1,358 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Carousel working example 1`] = ` +Array [ +
+
+
+
+
+
+
+ item: + 0 +
+
+
+
+ item: + 1 +
+
+
+
+ item: + 2 +
+
+
+
+
+
+ item: + 3 +
+
+
+
+ item: + 4 +
+
+
+
+ item: + 5 +
+
+
+
+
+
+ item: + 6 +
+
+
+
+ item: + 7 +
+
+
+
+ item: + 8 +
+
+
+
+
+
+ item: + 9 +
+
+
+
+ item: + 10 +
+
+
+
+
, +
, +
+
+
+
+
+
+
+ item: + 0 +
+
+
+
+ item: + 1 +
+
+
+
+ item: + 2 +
+
+
+
+
+
+ item: + 3 +
+
+
+
+ item: + 4 +
+
+
+
+ item: + 5 +
+
+
+
+
+
+ item: + 6 +
+
+
+
+ item: + 7 +
+
+
+
+ item: + 8 +
+
+
+
+
+
+ item: + 9 +
+
+
+
+ item: + 10 +
+
+
+
+ item: + 11 +
+
+
+
+
+
+ item: + 12 +
+
+
+
+ item: + 13 +
+
+
+
+ item: + 14 +
+
+
+
+
, +] +`; diff --git a/packages/carousel/src/react/__specs__/index.spec.js b/packages/carousel/src/react/__specs__/index.spec.js new file mode 100644 index 0000000000..a20a80170c --- /dev/null +++ b/packages/carousel/src/react/__specs__/index.spec.js @@ -0,0 +1,17 @@ +import React from 'react' +import { render } from 'react-testing-library' + +import Carousel from '../index.js' + +describe('Carousel', () => { + it('renders', () => { + render( + +
+
+
+
+ + ) + }) +}) diff --git a/packages/carousel/src/react/__specs__/storyshots.spec.js b/packages/carousel/src/react/__specs__/storyshots.spec.js new file mode 100644 index 0000000000..11ad5fa021 --- /dev/null +++ b/packages/carousel/src/react/__specs__/storyshots.spec.js @@ -0,0 +1,14 @@ +import path from 'path' +import initStoryshots, { + snapshotWithOptions +} from '@storybook/addon-storyshots' + +jest.mock('@pluralsight/ps-design-system-storybook-addon-center') +jest.mock('@pluralsight/ps-design-system-storybook-addon-theme') + +const createNodeMock = el => document.createElement('div') + +initStoryshots({ + configPath: path.resolve(__dirname, '..', '..', '..', '.storybook'), + test: snapshotWithOptions({ createNodeMock }) +}) diff --git a/packages/carousel/src/react/__stories__/index.story.js b/packages/carousel/src/react/__stories__/index.story.js new file mode 100644 index 0000000000..0114f9f040 --- /dev/null +++ b/packages/carousel/src/react/__stories__/index.story.js @@ -0,0 +1,37 @@ +import { storiesOf } from '@storybook/react' + +import * as glamor from 'glamor' +import React from 'react' + +import Carousel from '../index.js' + +const MockItem = props => ( +
+) + +storiesOf('Carousel', module).add('working example', _ => ( + + + {new Array(11).fill(null).map((_, index) => ( + item: {index} + ))} + + +
+ + + {new Array(15).fill(null).map((_, index) => ( + item: {index} + ))} + +
+)) diff --git a/packages/carousel/src/react/index.js b/packages/carousel/src/react/index.js new file mode 100644 index 0000000000..32eb7351fc --- /dev/null +++ b/packages/carousel/src/react/index.js @@ -0,0 +1,158 @@ +import * as glamor from 'glamor' +import React, { Fragment, useState } from 'react' +import PropTypes from 'prop-types' + +import filterReactProps from '@pluralsight/ps-design-system-filter-react-props' +import { useTheme } from '@pluralsight/ps-design-system-theme/react' + +import css from '../css/index.js' + +const chunk = (arr, size) => + arr.reduce((acc, item, index) => { + if (index % size === 0) acc.push([item]) + else acc[acc.length - 1].push(item) + + return acc + }, []) + +const combineFns = (...fns) => (...args) => + fns.filter(isFunction).forEach(fn => fn(...args)) + +const isFunction = fn => typeof fn === 'function' + +const styles = { + carousel: () => glamor.css(css['.psds-carousel']), + controls: () => glamor.css(css['.psds-carousel__controls']), + control: (themeName, { direction }) => + glamor.compose( + glamor.css(css['.psds-carousel__controls__control']), + glamor.css(css[`.psds-carousel__controls__control--${direction}`]) + ), + pages: () => glamor.css(css['.psds-carousel__pages']), + page: () => glamor.css(css['.psds-carousel__page']), + item: () => glamor.css(css['.psds-carousel__item']) +} + +const CarouselContext = React.createContext() + +const Carousel = ({ controls, ...props }) => { + const pagesRef = React.useRef() + const [activePage, setActivePage] = useState(0) + const [offset, setOffset] = useState(0) + + // TODO: + // - get item count + // - calc num of pages + // - shim missing els + + const perPage = 3 + const pages = chunk(React.Children.toArray(props.children), perPage) + + React.useEffect(() => { + const { current: parentEl } = pagesRef + const nextPageEl = parentEl.children[activePage] + + // NOTE: this is to fix storyshots. find out why it's undefined + if (!nextPageEl) return + + setOffset(parentEl.offsetLeft - nextPageEl.offsetLeft) + }, [activePage]) + + const next = () => { + const nextPage = activePage + 1 + if (nextPage > pages.length - 1) return + + setActivePage(nextPage) + } + + const prev = () => { + const nextPage = activePage - 1 + if (nextPage < 0) return + + setActivePage(nextPage) + } + + return ( + +
+ {controls} + +
+ {pages.map((pageItems, pageIndex) => ( + + {pageItems.map((item, itemIndex) => ( + {item} + ))} + + ))} +
+
+
+ ) +} + +const Page = props => { + const { offset } = React.useContext(CarouselContext) + + return ( +
+ ) +} + +const Item = props =>
+ +const Controls = props => ( +
+) +Controls.propTypes = { + children: PropTypes.node.isRequired // TODO: better validator +} + +const Control = props => { + const themeName = useTheme() + const { next, prev } = React.useContext(CarouselContext) + + const handleClick = combineFns( + props.direction === 'next' ? next : prev, + props.onClick + ) + + return ( +