fetch latest:
git fetch --all
Checkout the session 2 branch
git checkout session-2-start
Pull latest
yarn install
Once that's done, start the application:
yarn start
You should see the application with tabs at the top. Clicking tabs will switch the coffee list.
Go to src/pages/Home/index.tsx
. There's Tabs
with an items
prop which is an array of objects with the shape of { title: string, content: React.ReactNode }
. We call the style of this type of complex component a "configuration component" because you configure the component, but you don't have any direct access to the internals of the component without extra prop config.
Read the Compound Components documentation to learn the advantages of compound components. Refactor the Home
component to use the Tabs compound component API. Remember to import Tabs from @workday/canvas-kit-labs-react/tabs
. Below is the final code.
// ...
import { Tabs } from '@workday/canvas-kit-labs-react/tabs';
//...
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
return (
<Tabs>
<Tabs.List>
<Tabs.Item>All</Tabs.Item>
<Tabs.Item>Popular</Tabs.Item>
<Tabs.Item>New & Interesting</Tabs.Item>
<Tabs.Item>Staff Favorites</Tabs.Item>
</Tabs.List>
<Tabs.Panel>
<CoffeeList coffee={coffee} />
</Tabs.Panel>
<Tabs.Panel>
<CoffeeList coffee={popularCoffee} />
</Tabs.Panel>
<Tabs.Panel>
<CoffeeList coffee={newCoffee} />
</Tabs.Panel>
<Tabs.Panel>
<CoffeeList coffee={staffCoffee} />
</Tabs.Panel>
</Tabs>
);
};
The application should look exactly the same as it did before.
All sub components of a Compound Component API can have the element adjusted: https://github.com/Workday/canvas-kit/blob/6491da3eace1996a1e4e8e9d9271214b3d6e2b88/COMPOUND_COMPONENTS.md#configuring-components
Go ahead and change the element of the Tabs.List
component to a section
.
Let's explore the TabsModel
to see how we can change or observe the behavior of the Tabs model.
If we want to observe when tabs change, we can add the onActivate
callback of the Tabs Model. The Compound Component API allows us to configure the model through the container component. Go ahead and add the following to the Tabs
element:
<Tabs onActivate={({ data, prevState }) => {
console.log('onActivate', data, prevState)
}}>
Now when you click on a tab, you'll see the tab event data and the previous state in the dev console. You'll notice the data
contains an object with a tab
property. The tab
will output something like "0"
. This is because we didn't specify a name for the tab items or tab panels. By default the name will be the index of the item or panel converted to a string. Let's go ahead and name our items and panels.
<Tabs.Item name="all">
The final code will look like:
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
return (
<Tabs
onActivate={({ data, prevState }) => {
console.log('onActivate', data, prevState);
}}
>
<Tabs.List as="section">
<Tabs.Item name="all">All</Tabs.Item>
<Tabs.Item name="popular">Popular</Tabs.Item>
<Tabs.Item name="new">New & Interesting</Tabs.Item>
<Tabs.Item name="alan">Staff Favorites</Tabs.Item>
</Tabs.List>
<Tabs.Panel name="all">
<CoffeeList coffee={coffee} />
</Tabs.Panel>
<Tabs.Panel name="popular">
<CoffeeList coffee={popularCoffee} />
</Tabs.Panel>
<Tabs.Panel name="new">
<CoffeeList coffee={newCoffee} />
</Tabs.Panel>
<Tabs.Panel name="alan">
<CoffeeList coffee={staffCoffee} />
</Tabs.Panel>
</Tabs>
);
};
Now you will see the tab name in the event data in the dev console output.
There are also guards that allow us to prevent certain events from happening under any condition we want.
<Tabs
shouldActivate={({ data }) => data.tab !== 'alan'}
>
This will prevent the alan
tab from being activated. This might be useful if you have an "Add Tab" tab that adds a new tab. It is meant to be a button and not an actual tab, so there would be no panel content.
Now we will hoist the model so that we have access to the state and events in our application:
Update the import:
import { Tabs, useTabsModel } from '@workday/canvas-kit-labs-react/tabs';
Update the Home component:
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
const model = useTabsModel({
onActivate: ({ data, prevState }) => {
console.log('onActivate', data, prevState);
},
});
return (
<Tabs model={model}>
<Tabs.List as="section">
<Tabs.Item name="all">All</Tabs.Item>
<Tabs.Item name="popular">Popular</Tabs.Item>
<Tabs.Item name="new">New & Interesting</Tabs.Item>
<Tabs.Item name="alan">Staff Favorites</Tabs.Item>
</Tabs.List>
<Tabs.Panel name="all">
<CoffeeList coffee={coffee} />
</Tabs.Panel>
<Tabs.Panel name="popular">
<CoffeeList coffee={popularCoffee} />
</Tabs.Panel>
<Tabs.Panel name="new">
<CoffeeList coffee={newCoffee} />
</Tabs.Panel>
<Tabs.Panel name="alan">
<CoffeeList coffee={staffCoffee} />
</Tabs.Panel>
</Tabs>
);
};
We're using the Tabs as more like filters than individual tab panels. It would be more efficient to update our list of coffee vs having many lists since Tabs retains everything in the DOM. We'll start by removing all but a single tab:
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
const model = useTabsModel({
onActivate: ({ data, prevState }) => {
console.log('onActivate', data, prevState);
},
});
return (
<Tabs model={model}>
<Tabs.List as="section">
<Tabs.Item name="all">All</Tabs.Item>
<Tabs.Item name="popular">Popular</Tabs.Item>
<Tabs.Item name="new">New & Interesting</Tabs.Item>
<Tabs.Item name="alan">Staff Favorites</Tabs.Item>
</Tabs.List>
<Tabs.Panel name="all">
<CoffeeList coffee={coffee} />
</Tabs.Panel>
</Tabs>
);
};
We notice that all
is the only tab panel that will ever show. If you activate other tabs, the panel will be blank. The tabs component works by connecting a tab item with a tab panel by the name. It uses a hidden
attribute on tab panels to handle which tab is "active". If we want a single panel, we'll have to override this mechanism. Luckily, CK components will take any props handed to it and use those attributes directly, overriding default functionality. We'll update our example to a single panel.
First, let's add a mapping of coffee data:
// We use `Record` to make TS happy later
const coffees: Record<string, Coffee[]> = {
all: coffee,
popular: popularCoffee,
new: newCoffee,
alan: staffCoffee,
};
We'll update the Tabs.Panel
to render a CoffeeList
with coffee from this map.
<Tabs.Panel hidden={undefined}>
<CoffeeList coffee={coffees[model.state.activeTab] || []} />
</Tabs.Panel>
We have the || []
as a fallback. There's a short time when when the active tab isn't set yet. This will send an empty array to the CoffeeList
.
We ended up breaking some accessibility though. The tab item has an aria-controls
that maps to the id
of a tab panel. We'll need to add back that functionality:
<Tabs.Item name="all" aria-controls="my-panel">
All
</Tabs.Item>
<Tabs.Panel hidden={undefined} id="my-panel">
...
The final code looks like:
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
const model = useTabsModel({
onActivate: ({ data, prevState }) => {
console.log('onActivate', data, prevState);
},
});
const coffees: Record<string, Coffee[]> = {
all: coffee,
popular: popularCoffee,
new: newCoffee,
alan: staffCoffee,
};
return (
<>
<Tabs model={model}>
<Tabs.List>
<Tabs.Item name="all" aria-controls="my-panel">
All
</Tabs.Item>
<Tabs.Item name="popular" aria-controls="my-panel">
Popular
</Tabs.Item>
<Tabs.Item name="new" aria-controls="my-panel">
New & Interesting
</Tabs.Item>
<Tabs.Item name="alan" aria-controls="my-panel">
Staff Favorites
</Tabs.Item>
</Tabs.List>
<Tabs.Panel hidden={undefined} id="my-panel">
<CoffeeList coffee={coffees[model.state.activeTab] || []} />
</Tabs.Panel>
</Tabs>
</>
);
};
Now we have a fully accessible single-tab implementation. We could extend or change base functionality without creating a new mode on the Tabs
component. This showcases some of the power of the Compound Component API. More use-cases can be supported without creating new components from scratch.
Since we have access to the model, we can also send events to it. We might want to control which tab is activated external via a route or an external button. Let's create a button that can switch our tab to the Staff Favorites tab.
Here's the button:
import { Button } from '@workday/canvas-kit-react/button';
// ...
<Button onClick={() => model.events.activate({ tab: 'alan' })}>
Activate Staff Favorites
</Button>
The onClick
of the button calls model.events.activate
with the alan
tab as the event data. This will activate the tab in the exact same way the tabs is updated internally. If we manually updated the coffee data, but didn't update the model, the model would be out of sync. This way the Tabs model remains the source of truth for the state of the component.
Here's the final Home
component:
export const Home: React.FC = () => {
const { coffee } = useAllCoffee();
const [newCoffee, popularCoffee, staffCoffee] = splitCoffee(coffee);
const model = useTabsModel({
onActivate: ({ data, prevState }) => {
console.log('onActivate', data, prevState);
},
});
const coffees: Record<string, Coffee[]> = {
all: coffee,
popular: popularCoffee,
new: newCoffee,
alan: staffCoffee,
};
return (
<>
<Button onClick={() => model.events.activate({ tab: 'alan' })}>
Activate Staff Favorites
</Button>
{model.state.activeTab}
<Tabs model={model}>
<Tabs.List>
<Tabs.Item name="all" aria-controls="my-panel">
All
</Tabs.Item>
<Tabs.Item name="popular" aria-controls="my-panel">
Popular
</Tabs.Item>
<Tabs.Item name="new" aria-controls="my-panel">
New & Interesting
</Tabs.Item>
<Tabs.Item name="alan" aria-controls="my-panel">
Staff Favorites
</Tabs.Item>
</Tabs.List>
<Tabs.Panel hidden={undefined} id="my-panel">
<CoffeeList coffee={coffees[model.state.activeTab] || []} />
</Tabs.Panel>
</Tabs>
</>
);
};