From fd1eb904d64b8f2a53ba5a2bc335d8cde0e460b7 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 11 Mar 2024 12:48:03 -0600 Subject: [PATCH] finish --- .../02.problem.validation/README.mdx | 6 +- .../02.solution.validation/README.mdx | 8 +-- .../02.solution.validation/toggle.tsx | 2 +- exercises/03.compound-components/FINISHED.mdx | 5 ++ .../04.slots/01.problem.context/README.mdx | 26 +++++++ .../04.slots/01.problem.context/toggle.tsx | 2 +- .../04.slots/01.solution.context/README.mdx | 3 + .../04.slots/01.solution.context/toggle.tsx | 2 +- .../04.slots/02.problem.generic/README.mdx | 14 ++++ .../04.slots/02.problem.generic/toggle.tsx | 2 +- .../04.slots/02.solution.generic/README.mdx | 3 + .../04.slots/02.solution.generic/toggle.tsx | 2 +- exercises/04.slots/03.problem.prop/README.mdx | 42 ++++++++++++ exercises/04.slots/03.problem.prop/toggle.tsx | 2 +- .../04.slots/03.solution.prop/README.mdx | 6 ++ exercises/04.slots/FINISHED.mdx | 3 + exercises/04.slots/README.mdx | 67 +++++++++++++++++++ .../01.problem.collections/README.mdx | 6 +- .../01.solution.collections/README.mdx | 2 +- .../02.problem.getters/README.mdx | 6 +- .../02.solution.getters/README.mdx | 2 +- .../02.solution.getters/toggle.tsx | 2 +- exercises/05.prop-getters/FINISHED.mdx | 4 ++ .../06.state-reducer/01.problem/README.mdx | 20 +++--- .../06.state-reducer/01.problem/toggle.tsx | 4 +- .../06.state-reducer/01.solution/README.mdx | 2 +- .../06.state-reducer/01.solution/toggle.tsx | 4 +- .../02.problem.default/README.mdx | 14 +--- .../02.problem.default/toggle.tsx | 4 +- .../02.solution.default/README.mdx | 9 ++- .../02.solution.default/toggle.tsx | 4 +- exercises/06.state-reducer/FINISHED.mdx | 3 + exercises/06.state-reducer/README.mdx | 20 ++++++ .../07.control-props/01.problem/README.mdx | 9 +-- .../07.control-props/01.problem/toggle.tsx | 4 +- .../07.control-props/01.solution/README.mdx | 12 ++-- .../07.control-props/01.solution/toggle.tsx | 4 +- exercises/07.control-props/FINISHED.mdx | 4 ++ exercises/07.control-props/README.mdx | 10 +-- exercises/FINISHED.mdx | 2 +- 40 files changed, 274 insertions(+), 72 deletions(-) diff --git a/exercises/03.compound-components/02.problem.validation/README.mdx b/exercises/03.compound-components/02.problem.validation/README.mdx index cd377274..93dc5948 100644 --- a/exercises/03.compound-components/02.problem.validation/README.mdx +++ b/exercises/03.compound-components/02.problem.validation/README.mdx @@ -1,8 +1,8 @@ # Compound Components Validation -Change to this (temporarily): +👨‍💼 Change to this (temporarily): -```javascript +```tsx import { ToggleButton } from './toggle' export const App = () => @@ -12,7 +12,7 @@ Why doesn't that work (it's not supposed to, but can you explain why)? Can you figure out a way to give the developer a better error message that explains what they're doing wrong and how to fix it? -🚨 The tests will tell you the message for the error you must throw when the +🚨 The tests will tell you in the message for the error you must throw when the context is undefined. 🦺 Additionally, this is where we can make TypeScript happier (TypeScript knew diff --git a/exercises/03.compound-components/02.solution.validation/README.mdx b/exercises/03.compound-components/02.solution.validation/README.mdx index f6df20ec..0620e06a 100644 --- a/exercises/03.compound-components/02.solution.validation/README.mdx +++ b/exercises/03.compound-components/02.solution.validation/README.mdx @@ -1,6 +1,6 @@ # Compound Components Validation -Runtime validation isn't the best (it would be better if we could enforce this -statically via TypeScript), but unfortunately it's the best we can do with the -composition model offered by React. That said, we it's unlikely people will mess -this up now that we have this runtime validation in place. +👨‍💼 Runtime validation isn't the best (it would be better if we could enforce +this statically via TypeScript), but unfortunately it's the best we can do with +the composition model offered by React. That said, we it's unlikely people will +mess this up now that we have this runtime validation in place. diff --git a/exercises/03.compound-components/02.solution.validation/toggle.tsx b/exercises/03.compound-components/02.solution.validation/toggle.tsx index b86d3453..dd3af46f 100644 --- a/exercises/03.compound-components/02.solution.validation/toggle.tsx +++ b/exercises/03.compound-components/02.solution.validation/toggle.tsx @@ -17,7 +17,7 @@ export function Toggle({ children }: { children: React.ReactNode }) { function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/03.compound-components/FINISHED.mdx b/exercises/03.compound-components/FINISHED.mdx index cce3db9c..46371d1d 100644 --- a/exercises/03.compound-components/FINISHED.mdx +++ b/exercises/03.compound-components/FINISHED.mdx @@ -1 +1,6 @@ # Compound Components + +👨‍💼 Great work! You now know how to create one of the best composition APIs for +UI component libraries. The vast majority of component libraries employ this +pattern and even if you don't build your own, you'll be much better able to use +these libraries because you understand how they work. Good job! diff --git a/exercises/04.slots/01.problem.context/README.mdx b/exercises/04.slots/01.problem.context/README.mdx index 892c37f1..401d11b0 100644 --- a/exercises/04.slots/01.problem.context/README.mdx +++ b/exercises/04.slots/01.problem.context/README.mdx @@ -1 +1,27 @@ # Slot Context + +👨‍💼 It's a tale as old as time. Our `label` and `input` are not properly +associated in this form and so clicking the `label` will not focus the `input` +as expected (in addition to other accessibility issues). + +But we don't want developers to be able to make this mistake. So we've made a +`TextField` component which will generate the `id` for the relationship (if one +is not provided). The tricky bit is we want people to be able to structure their +label and input however they want, so we can't render the `input` and `label` +for them. Instead, we want to be able to provide the `id` and `htmlFor` props to +the `label` and `input`. + +So what we want you to do is first create a `SlotContext` and `useSlotProps` +hook in , then use those in the `Label` and +`Input` components to retrieve the necessary props. + +The `useSlotProps` hook should accept a props object and a slot name and return +the props to be applied to the element for that slot. It should merge the props +it's been given with the props from the `SlotContext` for that slot. + +Once you've finished that, then render the `SlotContext` provider in the +`TextField` component in to provide slot +props for the `label` and `input`. + +When you're finished, the label and input should be properly associated and +clicking the label should focus the input. diff --git a/exercises/04.slots/01.problem.context/toggle.tsx b/exercises/04.slots/01.problem.context/toggle.tsx index b86d3453..dd3af46f 100644 --- a/exercises/04.slots/01.problem.context/toggle.tsx +++ b/exercises/04.slots/01.problem.context/toggle.tsx @@ -17,7 +17,7 @@ export function Toggle({ children }: { children: React.ReactNode }) { function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/04.slots/01.solution.context/README.mdx b/exercises/04.slots/01.solution.context/README.mdx index 892c37f1..b180d544 100644 --- a/exercises/04.slots/01.solution.context/README.mdx +++ b/exercises/04.slots/01.solution.context/README.mdx @@ -1 +1,4 @@ # Slot Context + +👨‍💼 That's a great start! Now we have that in place, I think we can use this in +our toggle component! diff --git a/exercises/04.slots/01.solution.context/toggle.tsx b/exercises/04.slots/01.solution.context/toggle.tsx index b86d3453..dd3af46f 100644 --- a/exercises/04.slots/01.solution.context/toggle.tsx +++ b/exercises/04.slots/01.solution.context/toggle.tsx @@ -17,7 +17,7 @@ export function Toggle({ children }: { children: React.ReactNode }) { function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/04.slots/02.problem.generic/README.mdx b/exercises/04.slots/02.problem.generic/README.mdx index 9eab6155..ed11ea95 100644 --- a/exercises/04.slots/02.problem.generic/README.mdx +++ b/exercises/04.slots/02.problem.generic/README.mdx @@ -1 +1,15 @@ # Generic Slot Components + +👨‍💼 You'll notice our party mode toggle button is using `useId` to properly +associate the toggle button with its label. We'd like to make that implicit and +reuse the `Label` component for the `Toggle` as well. + +Please update the `Toggle` component in to +render a `SlotContext` provider (in addition to the `ToggleContext` provider +it's already rendering) so it can provide props for a `label` slot (the slot +name for a `Label`). You'll also want to put the `id` in the `ToggleContext` so +the `ToggleButton` can grab it. + +Once you're finished with that, you can remove the manual `id`/`htmlFor` props +in the file and the `id` should be provided +automatically. diff --git a/exercises/04.slots/02.problem.generic/toggle.tsx b/exercises/04.slots/02.problem.generic/toggle.tsx index 6c7b76ce..7490ff53 100644 --- a/exercises/04.slots/02.problem.generic/toggle.tsx +++ b/exercises/04.slots/02.problem.generic/toggle.tsx @@ -25,7 +25,7 @@ export function Toggle({ children }: { children: React.ReactNode }) { function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/04.slots/02.solution.generic/README.mdx b/exercises/04.slots/02.solution.generic/README.mdx index 9eab6155..9f333a9b 100644 --- a/exercises/04.slots/02.solution.generic/README.mdx +++ b/exercises/04.slots/02.solution.generic/README.mdx @@ -1 +1,4 @@ # Generic Slot Components + +👨‍💼 Great! Now we can reuse the `Label` component in our set of `Toggle` compound +components. Let's go a step further with a generic `Text` component. diff --git a/exercises/04.slots/02.solution.generic/toggle.tsx b/exercises/04.slots/02.solution.generic/toggle.tsx index 18164bee..e6154602 100644 --- a/exercises/04.slots/02.solution.generic/toggle.tsx +++ b/exercises/04.slots/02.solution.generic/toggle.tsx @@ -31,7 +31,7 @@ export function Toggle({ function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/04.slots/03.problem.prop/README.mdx b/exercises/04.slots/03.problem.prop/README.mdx index 0b5e2dc8..c83c3e6f 100644 --- a/exercises/04.slots/03.problem.prop/README.mdx +++ b/exercises/04.slots/03.problem.prop/README.mdx @@ -1 +1,43 @@ # Slot Prop + +👨‍💼 We have `ToggleOn` and `ToggleOff` components, but really we could make those +components a simple `Text` component that accepts a `slot` prop. Then the +`Toggle` component could define the props that the individual `Text` components +should have based on which slot they're taking. + +In fact, we could do this with the `Switch` as well! + +🧝‍♂️ I've added `Text` and `Switch` components to the + file for you to use. These are both already +wired up to consume a `slot` named `text` and `switch`. You can +check the diff for details. + +What we want to do in this exercise is add a `slot` prop to each of our slot +components so the slot they're taking can be defined by the parent component. + +Then you'll need to update `Toggle` to get rid of the `ToggleContext` provider +and instead use the `SlotProvider` for all the components it wants to send props +to: + +- `label` - `htmlFor` +- `onText` - `hidden` (`undefined` if `isOn` is true, and `true` if `isOn` is + `false`) +- `offText` - `hidden` (`undefined` if `isOn` is false, and `true` if `isOn` is + `true`) +- `switch` - `id`, `on`, and `onClick` + +So by the end of all of this, here's what I want the API to be like: + +```tsx + + + + Let's party 🥳 + Sad town 😭 + +``` + +Once that's been updated, you can delete the `useToggle` hook and the +`ToggleOn`, `ToggleOff`, and `ToggleButton` components. + +Reusability FTW! diff --git a/exercises/04.slots/03.problem.prop/toggle.tsx b/exercises/04.slots/03.problem.prop/toggle.tsx index 10d88696..7601cb3f 100644 --- a/exercises/04.slots/03.problem.prop/toggle.tsx +++ b/exercises/04.slots/03.problem.prop/toggle.tsx @@ -40,7 +40,7 @@ export function Toggle({ // 🐨 delete everything below here! function useToggle() { const context = use(ToggleContext) - if (context === undefined) { + if (!context) { throw new Error( 'Cannot find ToggleContext. All Toggle components must be rendered within ', ) diff --git a/exercises/04.slots/03.solution.prop/README.mdx b/exercises/04.slots/03.solution.prop/README.mdx index 0b5e2dc8..5549c11e 100644 --- a/exercises/04.slots/03.solution.prop/README.mdx +++ b/exercises/04.slots/03.solution.prop/README.mdx @@ -1 +1,7 @@ # Slot Prop + +👨‍💼 Great! Now we can use the same component for multiple slots! + +One thing to note is that there's not a great way to have good type safety on +these props, though if you're creative, you could add nice runtime errors, +[for example](https://github.com/adobe/react-spectrum/blob/3db98d2d378f977a88d94e9f2501feca8ef8ce51/packages/react-aria-components/src/utils.tsx#L169-L181). diff --git a/exercises/04.slots/FINISHED.mdx b/exercises/04.slots/FINISHED.mdx index 13637724..c1288ff1 100644 --- a/exercises/04.slots/FINISHED.mdx +++ b/exercises/04.slots/FINISHED.mdx @@ -1 +1,4 @@ # Slots + +👨‍💼 This is a really nice pattern if you find yourself building a library of +components that have a lot of common components. Great job! diff --git a/exercises/04.slots/README.mdx b/exercises/04.slots/README.mdx index 73982bed..74336e96 100644 --- a/exercises/04.slots/README.mdx +++ b/exercises/04.slots/README.mdx @@ -4,3 +4,70 @@ **One liner:** Slots allow you to specify an element which takes on a particular role in the overall collection of components. + +If you're building a component library, you have to deal with two competing +interests: + +1. Correctness +2. Flexibility + +You want to make sure people don't mess up things like accessibility, but you +also want to give them the flexibility to build things the way their diverse +needs require. Slots can help with this. + +Here's a quick example of a component that uses slots (from the +[`react-aria`](https://react-spectrum.adobe.com/react-aria/index.html) docs): + +```tsx + + + Dogs + Cats + Dragons + Select your pets. + +``` + +The `slot="description"` prop is letting the `Text` component know that it needs +to look for special props that are meant to be used as a description. Those +special props will be provided by the `CheckboxGroup` component. + +Essentially, the `CheckboxGroup` component will say: "here's a bucket of props +for any component that takes on the role of a description." The `Text` component +will then say: "Oh look, I've got a `slot` prop that matches the `description` +slot, so I'll use these props to render myself." + +All of this is built using context. + +What this enables is a powerfully flexible capability to have components which +are highly reusable. The `Text` component can be used in many different +contexts, and it can adapt to the needs of the parent component. For example, +it's also used in react-aria's `ComboBox` components. Here's the anatomy of a +react-aria `ComboBox` component: + +```tsx lines=5,10,11 + +