├── .npmrc ├── src ├── components │ ├── Icon │ │ ├── index.js │ │ └── Icon.js │ ├── Select │ │ ├── index.js │ │ ├── Select.helpers.js │ │ ├── Select.js │ │ └── Select.stories.mdx │ ├── IconInput │ │ ├── index.js │ │ ├── IconInput.js │ │ └── IconInput.stories.mdx │ ├── ProgressBar │ │ ├── index.js │ │ ├── ProgressBar.js │ │ └── ProgressBar.stories.mdx │ └── VisuallyHidden │ │ ├── index.js │ │ ├── VisuallyHidden.stories.mdx │ │ └── VisuallyHidden.js └── constants.js ├── .babelrc ├── .storybook ├── preview.js ├── preview-head.html ├── main.js └── global.css ├── sandbox.config.json ├── .gitignore ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /src/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Icon'; 2 | -------------------------------------------------------------------------------- /src/components/Select/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Select'; 2 | -------------------------------------------------------------------------------- /src/components/IconInput/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './IconInput'; 2 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ProgressBar'; 2 | -------------------------------------------------------------------------------- /src/components/VisuallyHidden/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './VisuallyHidden'; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["babel-plugin-styled-components", { "displayName": true }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import './global.css'; 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: '^on[A-Z].*' }, 5 | }; 6 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "port": 6006, 5 | "startScript": "start", 6 | "node": "16" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../src/**/*.stories.mdx", 4 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 5 | ], 6 | "addons": [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/preset-create-react-app", 10 | '@storybook/addon-controls' 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Select/Select.helpers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function getDisplayedValue(value, children) { 4 | const childArray = React.Children.toArray(children); 5 | const selectedChild = childArray.find( 6 | (child) => child.props.value === value 7 | ); 8 | 9 | return selectedChild.props.children; 10 | } 11 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const COLORS = { 2 | primary: 'hsl(240deg 80% 60%)', 3 | gray50: 'hsl(0deg 0% 95%)', 4 | gray300: 'hsl(0deg 0% 75%)', 5 | gray500: 'hsl(0deg 0% 50%)', 6 | gray700: 'hsl(0deg 0% 40%)', 7 | black: 'hsl(0deg 0% 0%)', 8 | transparentGray15: 'hsl(0deg 0% 50% / 0.15)', 9 | transparentGray35: 'hsl(0deg 0% 50% / 0.35)', 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { COLORS } from '../../constants'; 6 | import VisuallyHidden from '../VisuallyHidden'; 7 | 8 | const ProgressBar = ({ value, size }) => { 9 | return {value}; 10 | }; 11 | 12 | export default ProgressBar; 13 | -------------------------------------------------------------------------------- /src/components/IconInput/IconInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { COLORS } from '../../constants'; 5 | 6 | import Icon from '../Icon'; 7 | import VisuallyHidden from '../VisuallyHidden'; 8 | 9 | const IconInput = ({ 10 | label, 11 | icon, 12 | width = 250, 13 | size, 14 | placeholder, 15 | }) => { 16 | return 'TODO'; 17 | }; 18 | 19 | export default IconInput; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | -------------------------------------------------------------------------------- /src/components/VisuallyHidden/VisuallyHidden.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/addon-docs/blocks'; 2 | 3 | import VisuallyHidden from './VisuallyHidden'; 4 | import Icon from '../Icon'; 5 | 6 | 7 | 8 | # VisuallyHidden 9 | 10 | 11 | <> 12 | 13 | Search 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/Select/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { COLORS } from '../../constants'; 5 | import Icon from '../Icon'; 6 | import { getDisplayedValue } from './Select.helpers'; 7 | 8 | const Select = ({ label, value, onChange, children }) => { 9 | const displayedValue = getDisplayedValue(value, children); 10 | 11 | return ( 12 | 15 | ); 16 | }; 17 | 18 | export default Select; 19 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/addon-docs/blocks'; 2 | import ProgressBar from './ProgressBar'; 3 | 4 | 23 | 24 | export const Template = (args) => ; 25 | 26 | # ProgressBar 27 | 28 | 29 | {Template.bind({})} 30 | 31 | -------------------------------------------------------------------------------- /src/components/Select/Select.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/addon-docs/blocks'; 2 | import Select from './Select'; 3 | 4 | 5 | 6 | # Select 7 | 8 | export const ManagedSelect = () => { 9 | const [value, setValue] = React.useState('newest'); 10 | return ( 11 | <> 12 | 21 | 31 | 32 | ); 33 | }; 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/components/IconInput/IconInput.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/addon-docs/blocks'; 2 | import IconInput from './IconInput'; 3 | 4 | 37 | 38 | export const Template = (args) => ; 39 | 40 | # IconInput 41 | 42 | 51 | {Template.bind({})} 52 | 53 | -------------------------------------------------------------------------------- /src/components/VisuallyHidden/VisuallyHidden.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const VisuallyHidden = ({ children, ...delegated }) => { 5 | const [forceShow, setForceShow] = React.useState(false); 6 | 7 | React.useEffect(() => { 8 | if (process.env.NODE_ENV !== 'production') { 9 | const handleKeyDown = (ev) => { 10 | if (ev.key === 'Alt') { 11 | setForceShow(true); 12 | } 13 | }; 14 | 15 | const handleKeyUp = () => { 16 | setForceShow(false); 17 | }; 18 | 19 | window.addEventListener('keydown', handleKeyDown); 20 | window.addEventListener('keyup', handleKeyUp); 21 | 22 | return () => { 23 | window.removeEventListener('keydown', handleKeyDown); 24 | window.removeEventListener('keyup', handleKeyUp); 25 | }; 26 | } 27 | }, []); 28 | 29 | if (forceShow) { 30 | return children; 31 | } 32 | 33 | return {children}; 34 | }; 35 | 36 | const Wrapper = styled.div` 37 | position: absolute; 38 | overflow: hidden; 39 | clip: rect(0 0 0 0); 40 | height: 1px; 41 | width: 1px; 42 | margin: -1px; 43 | padding: 0; 44 | border: 0; 45 | `; 46 | 47 | export default VisuallyHidden; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-component-library", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "babel-loader": "8.2.2", 7 | "babel-plugin-styled-components": "1.12.0", 8 | "react": "17.0.2", 9 | "react-dom": "17.0.2", 10 | "react-feather": "2.0.9", 11 | "react-scripts": "4.0.1", 12 | "styled-components": "5.2.1" 13 | }, 14 | "scripts": { 15 | "dev": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006 -s public", 16 | "start": "npm run dev" 17 | }, 18 | "eslintConfig": { 19 | "extends": [ 20 | "react-app", 21 | "react-app/jest" 22 | ] 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | }, 36 | "devDependencies": { 37 | "@storybook/addon-actions": "6.5.15", 38 | "@storybook/addon-controls": "6.5.15", 39 | "@storybook/addon-essentials": "6.5.15", 40 | "@storybook/addon-links": "6.5.15", 41 | "@storybook/node-logger": "6.5.15", 42 | "@storybook/preset-create-react-app": "3.1.7", 43 | "@storybook/react": "6.5.15", 44 | "webpack": "4.44.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We're given this component "for free" since it's really more 3 | * specific to React and React Feather. Feel free to read if you're 4 | * interested, but otherwise you can rely on our docs to learn its 5 | * API / which props it takes. 6 | */ 7 | import React from 'react'; 8 | import styled from 'styled-components'; 9 | import { Search, AtSign, ChevronDown } from 'react-feather'; 10 | 11 | const icons = { 12 | search: Search, 13 | 'at-sign': AtSign, 14 | 'chevron-down': ChevronDown, 15 | }; 16 | 17 | const Icon = ({ id, size, strokeWidth = 1, ...delegated }) => { 18 | const Component = icons[id]; 19 | 20 | if (!Component) { 21 | throw new Error(`No icon found for ID: ${id}`); 22 | } 23 | 24 | return ( 25 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const Wrapper = styled.div` 38 | width: var(--size); 39 | height: var(--size); 40 | 41 | /* 42 | OMG I'm doing that thing I've warned against doing! 43 | Unfortunately, react-feather doesn't make it possible to pass 44 | discrete styles to the nested SVG within its components. 45 | 46 | Because I'm "reaching in" to third-party code, though, it feels 47 | OK. In my mind, this Icon is my bridge to that third-party code, 48 | and if I have to do some hacky stuff, that's fine. It's 49 | a special circumstance, and I won't ever have to look at the 50 | react-feather JSX and try to work out where this SVG style 51 | is coming from. 52 | */ 53 | & > svg { 54 | display: block; 55 | stroke-width: var(--stroke-width); 56 | } 57 | `; 58 | 59 | export default Icon; 60 | -------------------------------------------------------------------------------- /.storybook/global.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | font-family: 'Roboto', sans-serif; 6 | } 7 | 8 | /* http://meyerweb.com/eric/tools/css/reset/ 9 | v2.0 | 20110126 10 | License: none (public domain) 11 | */ 12 | 13 | html, 14 | body, 15 | div, 16 | span, 17 | applet, 18 | object, 19 | iframe, 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6, 26 | p, 27 | blockquote, 28 | pre, 29 | a, 30 | abbr, 31 | acronym, 32 | address, 33 | big, 34 | cite, 35 | code, 36 | del, 37 | dfn, 38 | em, 39 | img, 40 | ins, 41 | kbd, 42 | q, 43 | s, 44 | samp, 45 | small, 46 | strike, 47 | strong, 48 | sub, 49 | sup, 50 | tt, 51 | var, 52 | b, 53 | u, 54 | i, 55 | center, 56 | dl, 57 | dt, 58 | dd, 59 | ol, 60 | ul, 61 | li, 62 | fieldset, 63 | form, 64 | label, 65 | legend, 66 | table, 67 | caption, 68 | tbody, 69 | tfoot, 70 | thead, 71 | tr, 72 | th, 73 | td, 74 | article, 75 | aside, 76 | canvas, 77 | details, 78 | embed, 79 | figure, 80 | figcaption, 81 | footer, 82 | header, 83 | hgroup, 84 | menu, 85 | nav, 86 | output, 87 | ruby, 88 | section, 89 | summary, 90 | time, 91 | mark, 92 | audio, 93 | video { 94 | margin: 0; 95 | padding: 0; 96 | border: 0; 97 | font-size: 100%; 98 | vertical-align: baseline; 99 | } 100 | /* HTML5 display-role reset for older browsers */ 101 | article, 102 | aside, 103 | details, 104 | figcaption, 105 | figure, 106 | footer, 107 | header, 108 | hgroup, 109 | menu, 110 | nav, 111 | section { 112 | display: block; 113 | } 114 | body { 115 | line-height: 1; 116 | } 117 | ol, 118 | ul { 119 | list-style: none; 120 | } 121 | blockquote, 122 | q { 123 | quotes: none; 124 | } 125 | blockquote:before, 126 | blockquote:after, 127 | q:before, 128 | q:after { 129 | content: ''; 130 | content: none; 131 | } 132 | table { 133 | border-collapse: collapse; 134 | border-spacing: 0; 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Component Library Workshop 2 | 3 | In this workshop, we'll build 3 components from scratch: 4 | 5 | 1. ProgressBar 6 | 2. Select 7 | 3. IconInput 8 | 9 | Most of the pertinent information will be stored in the Figma document (https://www.figma.com/file/u0wCdLXheiN9f2FmAuPsE9/Mini-Component-Library), but this README will contain some additional information to help you on your mission! 10 | 11 | Two fully-formed components have already been included, to be used as-needed in your work: 12 | 13 | - `Icon`, an icon component that uses `react-feather` to render various icons 14 | - `VisuallyHidden`, a component that allows us to make text available to screen-reader users, but not to sighted users. 15 | 16 | Additionally, all of the colors you'll need are indexed in `constants.js`. 17 | 18 | All components in this project use [the `Roboto` font](https://fonts.google.com/specimen/Roboto). This font is already included in the Storybook environment, and is already applied to all elements. It comes in two weights: 19 | 20 | - 400 (default) 21 | - 700 (bold) 22 | 23 | ## Running Storybook 24 | 25 | This project uses Storybook, a component development tool. 26 | 27 | First, install dependencies with `npm install`. 28 | 29 | Once dependencies are installed, you can start storybook by running: 30 | 31 | ``` 32 | npm run start 33 | ``` 34 | 35 | Once running, you can visit storybook at http://localhost:6006. 36 | 37 | ## Troubleshooting 38 | 39 | ### Using Node LTS 40 | 41 | If you run into any issues running a dev server, my first suggestion would be to make sure you’re using the current LTS (Long Term Support) version of Node. There tends to be lots of bugs and quirks with the very latest release of Node.js. 42 | 43 | For example, Node 25.2 introduced a [regression](https://github.com/nodejs/node/issues/60704) that leads to an error, and this can be fixed by downgrading to Node 24.11.1 (the current LTS version, at the time of writing). 44 | 45 | ### Digital Envelope Routines 46 | 47 | You may get an error when running the `start` script that looks like this: 48 | 49 | ``` 50 | Error: error:0308010C:digital envelope routines::unsupported 51 | at new Hash (node:internal/crypto/hash:67:19) 52 | at Object.createHash (node:crypto:130:10) 53 | ``` 54 | 55 | You can fix this issue either by downgrading to Node 16, or by updating the `package.json` file as follows: 56 | 57 | ```diff 58 | "scripts": { 59 | - "start": "start-storybook -p 6006 -s public", 60 | + "start": "NODE_OPTIONS=--openssl-legacy-provider start-storybook -p 6006 -s public", 61 | }, 62 | ``` 63 | 64 | For more info, check out the [Troubleshooting Guide](https://courses.joshwcomeau.com/troubleshooting) on the course platform. 65 | 66 | ## The Components 67 | 68 | ### ProgressBar 69 | 70 | The figma document mentions that this component should be "accessible". You can learn how to build a semantically-valid, accessible progress-bar component by reading this doc: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_progressbar_role 71 | 72 | This component uses a **box shadow**. We haven't seen this property yet! For now, you can achieve this effect by copying the following CSS declaration into your component: 73 | 74 | ```css 75 | box-shadow: inset 0px 2px 4px ${COLORS.transparentGray35}; 76 | ``` 77 | 78 | We'll learn much more about the `box-shadow` property in future modules =) 79 | 80 | ### Select 81 | 82 | The Select component will need a down-arrow icon! You can use the `chevron-down` ID with the `Icon` component. 83 | 84 | We want to use a native `` tag has a built-in chevron (downward-pointing 91 | > character), but it's inconsistent between browsers. We want to 92 | > use the provided “chevron-down” icon. But how do we replace the 93 | > default built-in one?? 94 | > 95 | > This is something we haven't learned how to do, and so you'll need 96 | > to do some googling + experimentation. I want you to get a bit 97 | > of practice trying to tackle “unconventional” challenges like this! 98 | > 99 | > If you get stuck and/or would like a hint, you'll find one on 100 | > the solution page: 101 | > https://courses.joshwcomeau.com/css-for-js/03-components/19-workshop-select#select 102 | 103 | ### IconInput 104 | 105 | This component also uses the `Icon` component — the specific ID will be provided as a prop. 106 | 107 | This component requires bold text. You can achieve this look by using `font-weight: 700`. 108 | --------------------------------------------------------------------------------