Docs: Edit style-guides (part 12 of doc improvement project) (#92263)

* Docs: Edit style-guides (part 12)

* Update contribute/style-guides/storybook.md

* Prettier fixes
This commit is contained in:
Joseph Perez 2024-08-22 12:15:12 -07:00 committed by GitHub
parent 8f807a2df7
commit a7b57be04f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 105 additions and 83 deletions

View File

@ -1,16 +1,18 @@
# end-to-end tests for plugins
# End-to-end tests for plugins
When end-to-end testing Grafana plugins, it's recommended to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. `@grafana/plugin-e2e` extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers; enabling comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana. For information on how to get started with Plugin end-to-end testing and Playwright, checkout the [Get started](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started) guide.
When end-to-end testing Grafana plugins, a best practice is to use the [`@grafana/plugin-e2e`](https://www.npmjs.com/package/@grafana/plugin-e2e?activeTab=readme) testing tool. The `@grafana/plugin-e2e` tool extends [`@playwright/test`](https://playwright.dev/) capabilities with relevant fixtures, models, and expect matchers. Use it to enable comprehensive end-to-end testing of Grafana plugins across multiple versions of Grafana.
## Adding end-to-end tests for a core plugin
> **Note:** To learn more, refer to our documentation on [plugin development](https://grafana.com/developers/plugin-tools/) and [end-to-end plugin testing](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/get-started).
Playwright end-to-end tests for plugins should be added to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory.
## Add end-to-end tests for a core plugin
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is where your plugin tests will be kept.
You can add Playwright end-to-end tests for plugins to the [`e2e/plugin-e2e`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e) directory.
2. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` sub directory matches the name of the directory that contains your plugin tests.
Adding `'authenticate'` to the list of dependencies and specifying `'playwright/.auth/admin.json'` as storage state will ensure all tests in your project will start already authenticated as an admin user. If you wish to use a different role for and perhaps test RBAC for some of your tests, please refer to the plugin-e2e [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication).
1. Add a new directory that has the name as your plugin [`here`](https://github.com/grafana/grafana/tree/main/e2e/plugin-e2e). This is the directory where your plugin tests will be kept.
1. Playwright uses [projects](https://playwright.dev/docs/test-projects) to logically group tests together. All tests in a project share the same configuration.
In the [Playwright config file](https://github.com/grafana/grafana/blob/main/playwright.config.ts), add a new project item. Make sure the `name` and the `testDir` subdirectory match the name of the directory that contains your plugin tests.
Add `'authenticate'` to the list of dependencies and specify `'playwright/.auth/admin.json'` as the storage state to ensure that all tests in your project will start already authenticated as an admin user. If you want to use a different role for and perhaps test RBAC for some of your tests, refer to our [documentation](https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication).
```ts
{
@ -24,14 +26,16 @@ Playwright end-to-end tests for plugins should be added to the [`e2e/plugin-e2e`
},
```
3. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1.
1. Update the [CODEOWNERS](https://github.com/grafana/grafana/blob/main/.github/CODEOWNERS/#L315) file so that your team is owner of the tests in the directory you added in step 1.
## Commands
- `yarn e2e:playwright` will run all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a certain project.
- `yarn e2e:playwright` runs all Playwright tests. Optionally, you can provide the `--project mysql` argument to run tests in a specific project.
The script above assumes you have Grafana running on `localhost:3000`. You may change this by providing environment variables.
The `yarn e2e:playwright` script assumes you have Grafana running on `localhost:3000`. You may change this with an environment variable:
`HOST=127.0.0.1 PORT=3001 yarn e2e:playwright`
- `yarn e2e:playwright:server` will start a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and run the Playwright tests. The development server is provisioned with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources and apps.
The `yarn e2e:playwright:server` starts a Grafana [development server](https://github.com/grafana/grafana/blob/main/scripts/grafana-server/start-server) on port 3001 and runs the Playwright tests.
- You can provision the development server with the [devenv](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md#add-data-sources) dashboards, data sources, and apps.

View File

@ -1,4 +1,4 @@
# End-to-End tests
# End-to-end tests
Grafana Labs uses a minimal [homegrown solution](../../e2e/utils/index.ts) built on top of [Cypress](https://cypress.io) for its end-to-end (E2E) tests.
@ -6,17 +6,17 @@ Important notes:
- We generally store all element identifiers ([CSS selectors](https://mdn.io/docs/Web/CSS/CSS_Selectors)) within the framework for reuse and maintainability.
- We generally do not use stubs or mocks as to fully simulate a real user.
- Cypress' promises [do not behave as you'd expect](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Mixing-Async-and-Sync-code).
- [Testing core Grafana](e2e-core.md) is different than [testing plugins](e2e-plugins.md) - core Grafana uses Cypress whereas plugins use [Playwright test](https://playwright.dev/).
- Cypress' promises [don't behave as you might expect](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Mixing-Async-and-Sync-code).
- [Testing core Grafana](e2e-core.md) is different than [testing plugins](e2e-plugins.md)core Grafana uses Cypress whereas plugins use [Playwright test](https://playwright.dev/).
## Framework structure
Inspired by https://martinfowler.com/bliki/PageObject.html
Our framework structure is inspired by [Martin Fowler's Page Object](https://martinfowler.com/bliki/PageObject.html).
- `Selector`: A unique identifier that is used from the E2E framework to retrieve an element from the Browser
- `Page`: An abstraction for an object that contains one or more `Selectors` with `visit` function to navigate to the page.
- `Component`: An abstraction for an object that contains one or more `Selectors` but without `visit` function
- `Flow`: An abstraction that contains a sequence of actions on one or more `Pages` that can be reused and shared between tests
- **`Selector`**: A unique identifier that is used from the E2E framework to retrieve an element from the browser
- **`Page`**: An abstraction for an object that contains one or more `Selector` identifiers with the `visit` function to go to the page.
- **`Component`**: An abstraction for an object that contains one or more `Selector` identifiers but without the `visit` function
- **`Flow`**: An abstraction that contains a sequence of actions on one or more `Page` abstractions that can be reused and shared between tests
## Basic example
@ -26,13 +26,15 @@ Let's start with a simple [JSX](https://reactjs.org/docs/introducing-jsx.html) e
<input className="gf-form-input login-form-input" type="text" />
```
We _could_ target the field with a CSS selector like `.gf-form-input.login-form-input` but that would be brittle as style changes occur frequently. Furthermore there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details.
It is possible to target the field with a CSS selector like `.gf-form-input.login-form-input`. However, doing so is a brittle solution because style changes occur frequently.
Furthermore, there is nothing that signals to future developers that this input is part of an E2E test. At Grafana, we use `data-testid` attributes as our preferred way of defining selectors. See [Aria-Labels vs data-testid](#aria-labels-vs-data-testid) for more details.
```jsx
<input data-testid="Username input field" className="gf-form-input login-form-input" type="text" />
```
The next step is to create a `Page` representation in our E2E framework to glue the test with the real implementation using the `pageFactory` function. For that function we can supply a `url` and `selectors` like in the example below:
The next step is to create a `Page` representation in our E2E framework. Doing so glues the test with the real implementation using the `pageFactory` function. For that function we can supply a `url` and selector like in the following example:
```typescript
export const Login = {
@ -43,9 +45,9 @@ export const Login = {
};
```
Note that the selector is prefixed with `data-testid` - this is a signal to the framework to look for the selector in the `data-testid` attribute.
In this example, the selector is prefixed with `data-testid`. The prefix is a signal to the framework to look for the selector in the `data-testid` attribute.
The next step is to add the `Login` page to the `Pages` export within [_\<repo-root>/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `e2e.pages` in our IDE.
The next step is to add the `Login` page to the `Pages` export within [_\<repo-root>/packages/grafana-e2e-selectors/src/selectors/pages.ts_](../../packages/grafana-e2e-selectors/src/selectors/pages.ts) so that it appears when we type `e2e.pages` in your IDE.
```typescript
export const Pages = {
@ -56,7 +58,9 @@ export const Pages = {
};
```
Now that we have a `Page` called `Login` in our `Pages` const we can use that to add a selector in our html like shown below and now this really signals to future developers that it is part of an E2E test.
Now that we have a page called `Login` in our `Pages` const, use it to add a selector in our HTML as shown in the following example. This page really signals to future developers that it is part of an E2E test.
Example:
```jsx
import { selectors } from '@grafana/e2e-selectors';
@ -66,9 +70,8 @@ import { selectors } from '@grafana/e2e-selectors';
The last step in our example is to use our `Login` page as part of a test.
- The `url` property is used whenever we call the `visit` function and is equivalent to the Cypress' [`cy.visit()`](https://docs.cypress.io/api/commands/visit.html#Syntax).
- Any defined selector can be accessed from the `Login` page by invoking it. This is equivalent to the result of the Cypress function [`cy.get(…)`](https://docs.cypress.io/api/commands/get.html#Syntax).
- Use the `url` property whenever you call the `visit` function. It is equivalent to the [`cy.visit()`](https://docs.cypress.io/api/commands/visit.html#Syntax) in Cypress.
- Access any defined selector from the `Login` page by invoking it. This is equivalent to the result of the Cypress function [`cy.get(…)`](https://docs.cypress.io/api/commands/get.html#Syntax).
```typescript
describe('Login test', () => {
@ -83,7 +86,7 @@ describe('Login test', () => {
## Advanced example
Let's take a look at an example that uses the same `selector` for multiple items in a list for instance. In this example app we have a list of data sources that we want to click on during an E2E test.
Let's take a look at an example that uses the same selector for multiple items in a list for instance. In this example app, there's a list of data sources that we want to click on during an E2E test.
```jsx
<ul>
@ -97,7 +100,7 @@ Let's take a look at an example that uses the same `selector` for multiple items
</ul>
```
Just as before in the basic example we'll start by creating a page abstraction using the `pageFactory` function:
Like in the basic example, start by creating a page abstraction using the `pageFactory` function:
```typescript
export const DataSources = {
@ -106,11 +109,11 @@ export const DataSources = {
};
```
You might have noticed that instead of a simple `string` as the `selector`, we're using a `function` that takes a string parameter as an argument and returns a formatted string using the argument.
You might have noticed that instead of a simple string as the selector, there's a function that takes a string parameter as an argument and returns a formatted string using the argument.
Just as before we need to add the `DataSources` page to the exported const `Pages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`.
Just as before, you need to add the `DataSources` page to the exported const `Pages` in `packages/grafana-e2e-selectors/src/selectors/pages.ts`.
The next step is to use the `dataSources` selector function as in our example below:
The next step is to use the `dataSources` selector function as in the following example:
```jsx
<ul>
@ -126,7 +129,7 @@ The next step is to use the `dataSources` selector function as in our example be
</ul>
```
When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML would look like:
When this list is rendered with the data sources with names `A`, `B` and `C` ,the resulting HTML looks like this:
```html
<div class="card-item-name" data-testid="data-testid Data source list item A">A</div>
@ -134,7 +137,7 @@ When this list is rendered with the data sources with names `A`, `B` and `C` ,th
<div class="card-item-name" data-testid="data-testid Data source list item C">C</div>
```
Now we can write our test. The one thing that differs from the [basic example](#basic-example) above is that we pass in which data source we want to click on as an argument to the selector function:
Now we can write our test. The one thing that differs from the previous [basic example](#basic-example) is that we pass in which data source we want to click as an argument to the selector function:
```typescript
describe('List test', () => {
@ -147,17 +150,17 @@ describe('List test', () => {
});
```
## Aria-Labels vs data-testid
## aria-label versus data-testid
Our selectors are set up to work with both aria-labels and data-testid attributes. Aria-labels help assistive technologies such as screenreaders identify interactive elements of a page for our users.
Our selectors are set up to work with both `aria-label` attributes and `data-testid` attributes. The `aria-label` attributes help assistive technologies such as screen readers identify interactive elements of a page for our users.
A good example of a time to use an aria-label might be if you have a button with an X to close:
A good example of a time to use an aria-label might be if you have a button with an **X** to close:
```
<button aria-label="close">X<button>
```
It might be clear visually that the X closes the modal, but audibly it would not be clear for example.
It might be clear visually that the **X** closes the modal, but audibly it would not be clear, for example.
```
<button aria-label="close">Close<button>
@ -165,28 +168,26 @@ It might be clear visually that the X closes the modal, but audibly it would not
The example might read aloud to a user as "Close, Close" or something similar.
However adding aria-labels to elements that are already clearly labeled or not interactive can be confusing and redundant for users with assistive technologies.
However, adding an aria-label to an element that is already clearly labeled or not interactive can be confusing and redundant for users with assistive technologies.
In such cases rather than adding unnecessary aria-labels to components so as to make them selectable for testing, it is preferable to use a data attribute that would not be read aloud with an assistive technology for example:
In such cases, don't add an unnecessary aria-label to a component so as to make them selectable for testing. Instead, use a data attribute that will not be read aloud with an assistive technology. For example:
```
<button data-testid="modal-close-button">Close<button>
```
We have added support for this in our selectors, to use:
Prefix your selector string with "data-testid":
We have added support for data attributes in our selectors Prefix your selector string with `data-testid`:
```typescript
export const Components = {
Login: {
openButton: 'open-button', // this would look for an aria-label
closeButton: 'data-testid modal-close-button', // this would look for a data-testid
openButton: 'open-button', // this looks for an aria-label
closeButton: 'data-testid modal-close-button', // this looks for a data-testid
},
};
```
and in your component, import the selectors and add the data test id:
and in your component, import the selectors and add the `data-testid`:
```
<button data-testid={Selectors.Components.Login.closeButton}>

View File

@ -1,6 +1,8 @@
# Storybook
[Storybook](https://storybook.js.org/) is a tool which we use to manage our design system and the components which are a part of it. Storybook consists of _stories:_ each story represents a component and a case in which it is used. To show a wide variety of use cases is good both documentation wise and for troubleshooting -- it might be possible to reproduce a bug for an edge case in a story.
[Storybook](https://storybook.js.org/) is a tool which Grafana uses to manage our design system and its components. Storybook consists of _stories_. Each story represents a component and the case in which it is used.
To show a wide variety of use cases is good both documentation wise and for troubleshooting—it might be possible to reproduce a bug for an edge case in a story.
Storybook is:
@ -10,16 +12,22 @@ Storybook is:
## How to create stories
Stories for a component should be placed next to the component file. The Storybook file requires the same name as the component file. For example, a story for `SomeComponent.tsx` will have the file name `SomeComponent.story.tsx`. If a story should be internal, not visible in production, name the file `SomeComponent.story.internal.tsx`.
Stories for a component should be placed next to the component file. The Storybook file requires the same name as the component file. For example, a story for `SomeComponent.tsx` has the file name `SomeComponent.story.tsx`.
If a story should be internal—not visible in production—name the file `SomeComponent.story.internal.tsx`.
### Writing stories
When writing stories, we use the [CSF format](https://storybook.js.org/docs/formats/component-story-format/). For more in-depth information on writing stories, see [Storybooks documentation on writing stories](https://storybook.js.org/docs/basics/writing-stories/).
When writing stories, we use the [CSF format](https://storybook.js.org/docs/formats/component-story-format/).
> **Note:** For more in-depth information on writing stories, see [Storybooks documentation](https://storybook.js.org/docs/basics/writing-stories/).
With the CSF format, the default export defines some general information about the stories in the file:
- `title`: Where the component is going to live in the hierarchy
- `decorators`: A list which can contain wrappers or provide context, such as theming
- **`title`**: Where the component is going to live in the hierarchy
- **`decorators`**: A list which can contain wrappers or provide context, such as theming
Example:
```jsx
// In MyComponent.story.tsx
@ -34,18 +42,18 @@ export default {
```
When it comes to writing the actual stories, you continue in the same file with named exports. The exports are turned into the story name.
When it comes to writing the actual stories, you should continue in the same file with named exports. The exports are turned into the story name like so:
```jsx
// Will produce a story name “some story”
export const someStory = () => <MyComponent />;
```
If you want to write cover cases with different values for props, then using knobs is usually enough. You dont need to create a new story. This will be covered further down.
If you want to write cover cases with different values for props, then using knobs is usually enough. You dont need to create a new story. This topic will be covered further down.
### Categorization
We currently have these categories:
We have these categories of components:
- **Docs Overview** - Guidelines and information regarding the design system
- **Forms** - Components commonly used in forms such as different kind of inputs
@ -55,7 +63,7 @@ We currently have these categories:
## Writing MDX documentation
An MDX file is basically a markdown file with the possibility to add jsx. These files are used by Storybook to create a “docs” tab.
An MDX file is a markdown file with the possibility to add JSX. These files are used by Storybook to create a “docs” tab.
### Link the MDX file to a components stories
@ -83,7 +91,7 @@ export default {
### MDX file structure
There are some things that the MDX file should contain:
The MDX file should contain the following items:
- When and why the component should be used
- Best practices - dos and donts for the component
@ -101,7 +109,9 @@ import { MyComponent } from './MyComponent';
### MDX file without a relationship to a component
An MDX file can exist by itself without any connection to a story. This can be good for writing things such as a general guidelines page. Two things are required for this to work:
An MDX file can exist by itself without any connection to a story. This can be good for writing things such as a general guidelines page.
Two conditions must be met for this to work:
- The file needs to be named `*.story.mdx`
- A `Meta` tag must exist that says where in the hierarchy the component lives. It can look like this:
@ -115,7 +125,7 @@ An MDX file can exist by itself without any connection to a story. This can be g
```
You can add parameters to the Meta tag. This example shows how to hide the tools:
You can add parameters to the `Meta` tag. This example shows how to hide the tools:
```jsx
<Meta title="Docs Overview/Color Palettes" parameters={{ options: { isToolshown: false }}}/>
@ -128,11 +138,11 @@ You can add parameters to the Meta tag. This example shows how to hide the tools
## Documenting component properties
A quick way to get an overview of what a component does is by looking at its properties. That's why it is important that we document these in a good way.
A quick way to get an overview of what a component does is by looking at its properties. That's why it is important that you document these in a good way.
### Comments
When writing the props interface for a component, it is possible to add a comment to that specific property, which will end up in the Props table in the MDX file. The comments are generated by [react-docgen](https://github.com/reactjs/react-docgen) and are formatted by writing `/** */`.
When writing the props interface for a component, it's possible to add a comment to that specific property. When you do so, the comment will appear in the Props table in the MDX file. The comments are generated by [react-docgen](https://github.com/reactjs/react-docgen) and are formatted by writing `/** */`.
```jsx
interface MyProps {
@ -143,25 +153,28 @@ interface MyProps {
### Controls
The [controls addon](https://storybook.js.org/docs/react/essentials/controls) provides a way to interact with a component's properties dynamically and requires much less code than knobs. We're deprecating knobs in favor of using controls.
The [controls addon](https://storybook.js.org/docs/react/essentials/controls) provides a way to interact with a component's properties dynamically. It also requires much less code than knobs.
Knobs are deprecated in favor of using controls.
#### Migrating a story from Knobs to Controls
As a test, we migrated the [button story](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/Button/Button.story.tsx). Here's the guide on how to migrate a story to controls.
As a test, we migrated the [button story](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/Button/Button.story.tsx).
Here's the guide on how to migrate a story to controls.
1. Remove the `@storybook/addon-knobs` dependency.
2. Import the Story type from `@storybook/react`
2. Import the `Story` type from `@storybook/react`
`import { Story } from @storybook/react`
3. Import the props interface from the component you're working on (these must be exported in the component).
3. Import the props interface from the component you're working on (these must be exported in the component):
`import { Props } from './Component'`
4. Add the Story type to all stories in the file, then replace the props sent to the component
and remove any knobs.
4. Add the `Story` type to all stories in the file, then replace the props sent to the component and remove any knobs:
Before
Before:
```tsx
export const Simple = () => {
@ -172,7 +185,7 @@ As a test, we migrated the [button story](https://github.com/grafana/grafana/blo
};
```
After
After:
```tsx
export const Simple: Story<Props> = ({ prop1, prop2 }) => {
@ -180,7 +193,7 @@ As a test, we migrated the [button story](https://github.com/grafana/grafana/blo
};
```
5. Add default props (or args in Storybook language).
5. Add default props (or `args` in Storybook language):
```tsx
Simple.args = {
@ -189,8 +202,7 @@ As a test, we migrated the [button story](https://github.com/grafana/grafana/blo
};
```
6. If the component has advanced props type (ie. other than string, number, boolean), you need to
specify these in an `argTypes`. This is done in the default export of the story.
6. If the component has advanced props type (that is, other than string, number, or Boolean), you need to specify these in an `argTypes`. Do this in the default export of the story:
```tsx
export default {
@ -204,6 +216,6 @@ As a test, we migrated the [button story](https://github.com/grafana/grafana/blo
## Best practices
- When creating a new component or writing documentation for an existing one, always cover the basic use case it was intended for with a code example.
- When creating a new component or writing documentation for an existing one, add a code example. The example should always cover the basic use case it was intended for.
- Use stories and knobs to create edge cases. If you are trying to solve a bug, try to reproduce it with a story.
- Do not create stories in the MDX, always create them in the `*.story.tsx` file.
- Do not create stories in the MDX. Instead, create them in the `*.story.tsx` file.

View File

@ -1,6 +1,6 @@
# Styling Grafana
[Emotion](https://emotion.sh/docs/introduction) is our default-to-be approach to styling React components. It provides a way for styles to be a consequence of properties and state of a component.
[Emotion](https://emotion.sh/docs/introduction) is Grafana's default-to-be approach to styling React components. It provides a way for styles to be a consequence of properties and state of a component.
## Usage
@ -8,9 +8,9 @@ For styling components, use [Emotion's `css` function](https://emotion.sh/docs/e
### Basic styling
To access the theme in your styles, use the `useStyles` hook. It provides basic memoization and access to the theme object.
To access the Emotion theme in your styles, use the `useStyles` hook. This hook provides basic memoization and access to the theme object.
> Please remember to put `getStyles` function at the end of the file!
> **Note:** Please remember to put `getStyles` function at the end of the file!
```tsx
import { GrafanaTheme2 } from '@grafana/data';
@ -30,12 +30,17 @@ const getStyles = (theme: GrafanaTheme2) =>
});
```
### Styling complex components
### Style complex components
In more complex cases, especially when you need to style multiple DOM elements in one component, or when using styles that depend on properties and/or state you
can have your getStyles function return an object with many class names and use [Emotion's `cx` function](https://emotion.sh/docs/@emotion/css#cx) to compose them.
In more complex cases, you can have the `getStyles` function return an object with many class names and use [Emotion's `cx` function](https://emotion.sh/docs/@emotion/css#cx) to compose them.
Let's say you need to style a component that has a different background depending on the `isActive` property :
This feature can be especially useful in certain use cases:
- when you need to style multiple DOM elements in one component
- when using styles that depend on properties
- when using styles that depend on state
Let's say you need to style a component that has a different background depending on the `isActive` property. For example:
```tsx
import { css, cx } from '@emotion/css';
@ -75,4 +80,4 @@ const getStyles = (theme: GrafanaTheme2) => {
};
```
For more information about themes at Grafana please see the [themes guide](./themes.md).
For more information about themes at Grafana, refer to the [themes guide](./themes.md).