|
1 | 1 | /* |
2 | 2 |
|
3 | | -Make these two components work like a normal <select><option/></select> |
4 | | -component. |
5 | | -
|
6 | | -First, don't worry about accessibility, we want to illustrate controlled v. |
7 | | -uncontrolled components first. |
8 | | -
|
9 | | -First, make the uncontrolled usage work. This means it will keep the value in |
10 | | -state. |
11 | | -
|
12 | | -1. Get the label to display correctly based on state. |
13 | | -2. When you click the component it opens/closes |
14 | | -3. When you click an option the component closes and updates the value in |
15 | | - state, and the label displays correctly |
16 | | -
|
17 | | -Now, make the uncontrolled version work. Instead of reading from state, you'll |
18 | | -read from props, and instead of setting state, you'll need to do something |
19 | | -else! |
20 | | -
|
21 | | -Once you've got that done, get started on making it accessible. |
| 3 | +1. Fill in `Contact` to use `ContactsResource` to fetch the contact, use the |
| 4 | +path: `/contacts/${id}` like this: |
| 5 | +
|
| 6 | +2. Notice how we transition before the image loads and then the page jumps |
| 7 | +around? We're waiting for the contact data but not the image. Create an |
| 8 | +`<Img/>` component and a new `ImgResource` to delay transitioning until |
| 9 | +the image is loaded |
| 10 | +
|
| 11 | +Remember, `createResource` needs to return a promise |
| 12 | +
|
| 13 | +Tips: |
| 14 | +
|
| 15 | +``` |
| 16 | +// you can load images programatically |
| 17 | +let image = new Image(); |
| 18 | +image.src = someUrl |
| 19 | +image.onload = () => {} |
| 20 | +
|
| 21 | +// You can create promises out of anything, here we'll use |
| 22 | +// setTimeout to make a "sleep" promise: |
| 23 | +function sleep(ms=1000) { |
| 24 | + return new Promise((resolve, reject) => { |
| 25 | + setTimeout(resolve, ms) |
| 26 | + }) |
| 27 | +} |
22 | 28 |
|
23 | | -Here are some guides, but we'll be doing it together as a class, too. |
| 29 | +// combine those two things to create your `ImageResource` |
| 30 | +``` |
24 | 31 |
|
25 | | -https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox |
26 | | -https://www.w3.org/TR/wai-aria-practices-1.1/examples/listbox/listbox-collapsible.html |
| 32 | +3. Finally, render a `Placeholder` (you'll need to import it from react) around |
| 33 | +`Img` with a `2000` delayMs, and then artificially delay your ImageResource by |
| 34 | +3000ms with setTimeout. What happens when you click the links now? |
27 | 35 |
|
28 | 36 | */ |
29 | 37 |
|
30 | | -import "./index.css"; |
31 | 38 | import React from "react"; |
32 | | -import PropTypes from "prop-types"; |
33 | | - |
34 | | -class Select extends React.Component { |
35 | | - static propTypes = { |
36 | | - onChange: PropTypes.func, |
37 | | - value: PropTypes.any, |
38 | | - defaultValue: PropTypes.any |
39 | | - }; |
40 | | - |
41 | | - render() { |
42 | | - const isOpen = false; |
43 | | - return ( |
44 | | - <div className="select"> |
45 | | - <button className="label"> |
46 | | - label <span className="arrow">▾</span> |
47 | | - </button> |
48 | | - {isOpen && <ul className="options">{this.props.children}</ul>} |
49 | | - </div> |
50 | | - ); |
51 | | - } |
52 | | -} |
53 | | - |
54 | | -class Option extends React.Component { |
55 | | - render() { |
56 | | - return <li className="option">{this.props.children}</li>; |
57 | | - } |
| 39 | +import { cache } from "./lib/cache"; |
| 40 | +import { createResource } from "simple-cache-provider"; |
| 41 | +import { Router, Link } from "@reach/router"; |
| 42 | + |
| 43 | +let ContactsResource = createResource(async path => { |
| 44 | + let response = await fetch(`https://contacts.now.sh${path}`); |
| 45 | + let json = await response.json(); |
| 46 | + return json; |
| 47 | +}); |
| 48 | + |
| 49 | +function Home() { |
| 50 | + let { contacts } = ContactsResource.read(cache, "/contacts"); |
| 51 | + return ( |
| 52 | + <div> |
| 53 | + <h1>Contacts</h1> |
| 54 | + <ul> |
| 55 | + {contacts.map(contact => ( |
| 56 | + <li key={contact.id}> |
| 57 | + <Link to={contact.id}> |
| 58 | + {contact.first} {contact.last} |
| 59 | + </Link> |
| 60 | + </li> |
| 61 | + ))} |
| 62 | + </ul> |
| 63 | + </div> |
| 64 | + ); |
58 | 65 | } |
59 | 66 |
|
60 | | -class App extends React.Component { |
61 | | - state = { |
62 | | - selectValue: "dosa" |
63 | | - }; |
64 | | - |
65 | | - setToMintChutney = () => { |
66 | | - this.setState({ |
67 | | - selectValue: "mint-chutney" |
68 | | - }); |
69 | | - }; |
70 | | - |
71 | | - render() { |
72 | | - return ( |
73 | | - <div className="app"> |
74 | | - <div className="block"> |
75 | | - <h2>Uncontrolled</h2> |
76 | | - <Select defaultValue="tikka-masala"> |
77 | | - <Option value="tikka-masala">Tikka Masala</Option> |
78 | | - <Option value="tandoori-chicken">Tandoori Chicken</Option> |
79 | | - <Option value="dosa">Dosa</Option> |
80 | | - <Option value="mint-chutney">Mint Chutney</Option> |
81 | | - </Select> |
82 | | - </div> |
83 | | - |
84 | | - <div className="block"> |
85 | | - <h2>Controlled</h2> |
86 | | - <p> |
87 | | - <button onClick={this.setToMintChutney}>Set to Mint Chutney</button> |
88 | | - </p> |
89 | | - <Select |
90 | | - value={this.state.selectValue} |
91 | | - onChange={selectValue => { |
92 | | - this.setState({ selectValue }); |
93 | | - }} |
94 | | - > |
95 | | - <Option value="tikka-masala">Tikka Masala</Option> |
96 | | - <Option value="tandoori-chicken">Tandoori Chicken</Option> |
97 | | - <Option value="dosa">Dosa</Option> |
98 | | - <Option value="mint-chutney">Mint Chutney</Option> |
99 | | - </Select> |
100 | | - </div> |
101 | | - </div> |
102 | | - ); |
| 67 | +let FAKE_DATA = { |
| 68 | + contact: { |
| 69 | + first: "Ryan", |
| 70 | + last: "Florence", |
| 71 | + avatar: "https://placekitten.com/510/510" |
103 | 72 | } |
| 73 | +}; |
| 74 | + |
| 75 | +function Contact({ id }) { |
| 76 | + // don't use FAKE_DATA, use `ContactsResource` |
| 77 | + let { contact } = FAKE_DATA; |
| 78 | + return ( |
| 79 | + <div> |
| 80 | + <h1> |
| 81 | + {contact.first} {contact.last} |
| 82 | + </h1> |
| 83 | + <p> |
| 84 | + <img |
| 85 | + alt={`${contact.first} smiling, maybe`} |
| 86 | + height="250" |
| 87 | + src={contact.avatar} |
| 88 | + /> |
| 89 | + </p> |
| 90 | + <p> |
| 91 | + <Link to="/">Home</Link> |
| 92 | + </p> |
| 93 | + </div> |
| 94 | + ); |
104 | 95 | } |
105 | 96 |
|
106 | | -export default App; |
| 97 | +export default () => ( |
| 98 | + <Router> |
| 99 | + <Home path="/" /> |
| 100 | + <Contact path=":id" /> |
| 101 | + </Router> |
| 102 | +); |
0 commit comments