Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update @wordpress/components package's contributing guidelines #33960

Merged
merged 35 commits into from
Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7e93e07
README: add notes
ciampo Aug 9, 2021
37e1759
README: add link to `CONTRIBUTING.md`
ciampo Aug 9, 2021
cefad1f
CONTRIBUTING: Add initial notes
ciampo Aug 9, 2021
02114ce
More notes into CONTRIBUTING
ciampo Aug 10, 2021
3d665fb
Update CONTRIBUTING
ciampo Aug 11, 2021
0fb4753
Add `enable` to boolean prefixes
ciampo Aug 12, 2021
00e2b35
Remove `View` suffix for styled components
ciampo Aug 12, 2021
17ccabe
Update folder structure: move styles and types into each subfolder
ciampo Aug 12, 2021
1459214
Less strict working about typescript, rename PolymorphicComponent to …
ciampo Aug 24, 2021
c6f6a24
Update packages/components/CONTRIBUTING.md
ciampo Aug 24, 2021
f533857
Refer to the repo testing overview docs in the unit test section
ciampo Sep 1, 2021
4c1c200
Update polyfill info
ciampo Sep 1, 2021
89a89d1
Remove "Components Structure" paragraph
ciampo Sep 1, 2021
5238a88
Reword text in "Folder structure" section, exclude shared utilities
ciampo Sep 1, 2021
7acc925
Folder structure schemes: name parent folders, remove useless sub-com…
ciampo Sep 1, 2021
6621f1e
Expand Storybook section
ciampo Sep 1, 2021
6f6d8de
Add details to soft deprecation strategy
ciampo Sep 1, 2021
9d0ece7
Misc smaller improvements
ciampo Sep 1, 2021
d394f5d
Fix typo
ciampo Sep 1, 2021
3359502
Expand hooks vs components section, move under composition
ciampo Sep 1, 2021
cfc843d
Move sub components naming convention under "APIs consistency" section
ciampo Sep 1, 2021
abf50ad
Add Polymorphic components sub-section, reorganise "Components compos…
ciampo Sep 1, 2021
0021dfc
Add more details about listing props through knobs in storybook
ciampo Sep 2, 2021
15f7023
Remove hyphen from subcomponents
ciampo Sep 2, 2021
4520f7e
Add content to the `Context System` section
ciampo Sep 2, 2021
95de8fb
comment out unfinied sections
ciampo Sep 3, 2021
5393b9a
Apply suggestions from code review
ciampo Sep 8, 2021
6647446
Comment out reference to the "composition section"
ciampo Sep 8, 2021
670ca6c
Rename "Emotion" section to "Styling"
ciampo Sep 8, 2021
e65518d
Updale unit test section to link directly to components snapshot testing
ciampo Sep 8, 2021
b6edc2e
Update the storybook section, adding more details on stories and knobs
ciampo Sep 8, 2021
e8bb18d
Link to the Coding Guidelines in the "Documentation" section
ciampo Sep 8, 2021
08cd356
remove note about relevance of SCSS-related docs in README
ciampo Sep 8, 2021
c34f731
Update context section with example snippets
ciampo Sep 8, 2021
8c494c8
Format internal links to absolute path format
ciampo Sep 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 273 additions & 6 deletions packages/components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,279 @@ Thank you for taking the time to contribute.

The following is a set of guidelines for contributing to the `@wordpress/components` package to be considered in addition to the general ones described in our [Contributing Policy](/CONTRIBUTING.md).

## Examples
## Core principles

Each component needs to include an example in its README.md file to demonstrate the usage of the component.
Contributions to the `@wordpress/components` package should follow a set of core principles and technical requirements.

These examples can be consumed automatically from other projects in order to visualize them in their documentation. To ensure these examples are extractable, compilable and renderable, they should be structured in the following way:
This set of guidelines should apply especially to newly introduced components. It is, in fact, possible that some of the older components don't respect some of these guidelines for legacy/compatibility reasons.

- It has to be included in a `jsx` code block.
- It has to work out-of-the-box. No additional code should be needed to have working the example.
- It has to define a React component called `My<ComponentName>` which renders the example (i.e.: `MyButton`). Examples for the Higher Order Components should define a `MyComponent<ComponentName>` component (i.e.: `MyComponentWithNotices`).
### Compatibility

The `@wordpress/components` package includes components that are relied upon by many developers across different projects. It is, therefore, very important to avoid introducing breaking changes.

In these situations, one possible approach is to "soft-deprecate" a given legacy API. This is achieved by:
ciampo marked this conversation as resolved.
Show resolved Hide resolved

1. Removing traces of the API from the docs, while still supporting it in code.
2. Updating all places in Gutenberg that use that API.
3. Adding deprecation warnings (only after the previous point is completed, otherwise the Browser Console will be polluted by all those warnings and some e2e tests may fail).

When adding new components or new props to existing components, it's recommended to prefix them with `__unstable` or `__experimental` until they're stable enough to be exposed as part of the public API.

Learn more on [How to preserve backward compatibility for a React Component](/docs/how-to-guides/backward-compatibility/README.md#how-to-preserve-backward-compatibility-for-a-react-component) and [Experimental and Unstable APIs](/docs/contributors/code/coding-guidelines.md#experimental-and-unstable-apis).

ciampo marked this conversation as resolved.
Show resolved Hide resolved
### Components composition

<!-- ### Polymorphic Components (i.e. the `as` prop)

The primary way to compose components is through the `as` prop. This prop can be used to change the underlying element used to render a component, e.g.:

```tsx
function LinkButton( { href, children } ) {
return <Button variant="primary" as="a" href={href}>{ children }</Button>;
}
```

### Composition patterns

TBD — E.g. Using `children` vs custom render props vs arbitrary "data" props

### (Semi-)Controlled components

TBD

### Layout "responsibilities"

TBD — Components' layout responsibilities and boundaries (i.e., a component should only affect the layout of its children, not its own) -->

#### Components & Hooks

One way to enable reusability and composition is to extract a component's underlying logic into a hook (living in a separate `hook.ts` file). The actual component (usually defined in a `component.tsx` file) can then invoke the hook and use its output to render the required DOM elements. For example:

```tsx
// in `hook.ts`
function useExampleComponent( props: PolymorphicComponentProps< ExampleProps, 'div' > ) {
// Merge received props with the context system.
const { isVisible, className, ...otherProps } = useContextSystem( props, 'Example' );

// Any other reusable rendering logic (e.g. computing className, state, event listeners...)
const cx = useCx();
const classes = useMemo(
() =>
cx(
styles.example,
isVisible && styles.visible,
className
),
[ className, isVisible ]
);

return {
...otherProps,
className: classes
};
}

// in `component.tsx`
function Example(
props: PolymorphicComponentProps< ExampleProps, 'div' >,
forwardedRef: Ref< any >
) {
const exampleProps = useExampleComponent( props );

return <View { ...spacerProps } ref={ forwardedRef } />;
}
```

A couple of good examples of how hooks are used for composition are:

- the `Card` component, which builds on top of the `Surface` component by [calling the `useSurface` hook inside its own hook](/packages/components/src/card/card/hook.js);
- the `HStack` component, which builds on top of the `Flex` component and [calls the `useFlex` hook inside its own hook](/packages/components/src/h-stack/hook.js).

<!-- ### APIs Consinstency

[To be expanded] E.g.:
ciampo marked this conversation as resolved.
Show resolved Hide resolved

- Boolean component props should be prefixed with `is*` (e.g. `isChecked`), `has*` (e.g. `hasValue`) or `enable*` (e.g. `enableScroll`)
- Event callback props should be prefixed with `on*` (e.g. `onChanged`)
- Subcomponents naming conventions (e.g `CardBody` instead of `Card.Body`)
- ...

### Performance

TDB -->

### Technical requirements for new components

The following are a set of technical requirements for all newly introduced components. These requirements are also retroactively being applied to existing components.

For an example of a component that follows these requirements, take a look at [`ItemGroup`](/packages/components/src/item-group).

#### TypeScript

We strongly encourage using TypeScript for all new components. Components should be typed using the `WordPressComponent` type.

<!-- TODO: add to the previous paragraph once the composision section gets added to this document.
(more details about polymorphism can be found above in the "Components composition" section). -->

#### Styling

All new component should be styled using [Emotion](https://emotion.sh/docs/introduction).

Note: Instead of using Emotion's standard `cx` function, the custom [`useCx` hook](/packages/components/src/utils/hooks/use-cx.ts) should be used instead.
ciampo marked this conversation as resolved.
Show resolved Hide resolved

#### Context system

The `@wordpress/components` context system is based on [React's `Context` API](https://reactjs.org/docs/context.html), and is a way for components to adapt to the "context" they're being rendered in.

Components can use this system via a couple of functions:

- they can provide values using a shared `ContextSystemProvider` component
- they can connect to the Context via `contextConnect`
- they can read the "computed" values from the context via `useContextSystem`

An example of how this is used can be found in the [`Card` component family](/packages/components/src/card). For example, this is how the `Card` component injects the `size` and `isBorderless` props down to its `CardBody` subcomponent — which makes it use the correct spacing and border settings "auto-magically".

```jsx
//=========================================================================
// Simplified snippet from `packages/components/src/card/card/hook.js`
//=========================================================================
import { useContextSystem } from '../../ui/context';

export function useCard( props ) {
// Read any derived registered prop from the Context System in the `Card` namespace
const derivedProps = useContextSystem( props, 'Card' );

// [...]

return computedHookProps;
}

//=========================================================================
// Simplified snippet from `packages/components/src/card/card/component.js`
//=========================================================================
import { contextConnect, ContextSystemProvider } from '../../ui/context';

function Card( props, forwardedRef ) {
const {
size,
isBorderless,
...otherComputedHookProps
} = useCard( props );

// [...]

// Prepare the additional props that should be passed to subcomponents via the Context System.
const contextProviderValue = useMemo( () => {
return {
// Each key in this object should match a component's registered namespace.
CardBody: {
size,
isBorderless,
},
};
}, [ isBorderless, size ] );

return (
{ /* Write additional values to the Context System */ }
<ContextSystemProvider value={ contextProviderValue }>
{ /* [...] */ }
</ContextSystemProvider>
);
}

// Connect to the Context System under the `Card` namespace
const ConnectedCard = contextConnect( Card, 'Card' );
export default ConnectedCard;

//=========================================================================
// Simplified snippet from `packages/components/src/card/card-body/hook.js`
//=========================================================================
import { useContextSystem } from '../../ui/context';

export function useCardBody( props ) {
// Read any derived registered prop from the Context System in the `CardBody` namespace.
// If a `CardBody` component is rendered as a child of a `Card` component, the value of
// the `size` prop will be the one set by the parent `Card` component via the Context
// System (unless the prop gets explicitely set on the `CardBody` component).
const { size = 'medium', ...otherDerivedProps } = useContextSystem( props, 'CardBody' );

// [...]

return computedHookProps;
}
```

#### Unit tests

Please refer to the [JavaScript Testing Overview docs](/docs/contributors/code/testing-overview.md#snapshot-testing).

#### Storybook

All new components should add stories to the project's [Storybook](https://storybook.js.org/). Each [story](https://storybook.js.org/docs/react/get-started/whats-a-story) captures the rendered state of a UI component in isolation. This greatly simplifies working on a given component, while also serving as an interactive form of documentation.

A component's story should be showcasing its different states — for example, the different variants of a `Button`:

```jsx
import Button from '../';

export default { title: 'Components/Button', component: Button };

export const _default = () => <Button>Default Button</Button>;

export const primary = () => <Button variant="primary">Primary Button</Button>;

export const secondary = () => <Button variant="secondary">Secondary Button</Button>;
```

A great tool to use when writing stories is the [Storybook Controls addon](https://storybook.js.org/addons/@storybook/addon-controls). Ideally props should be exposed by using this addon, which provides a graphical UI to interact dynamically with the component without needing to write code.

The default value of each control should coincide with the default value of the props (i.e. it should be `undefined` if a prop is not required). A story should, therefore, also explicitly show how values from the Context System are applied to (sub)components. A good example of how this may look like is the [`Card` story](https://wordpress.github.io/gutenberg/?path=/story/components-card--default) (code [here](/packages/components/src/card/stories/index.js)).

Storybook can be started on a local maching by running `npm run storybook:dev`. Alternatively, the components' catalogue (up to date with the latest code on `trunk`) can be found at [wordpress.github.io/gutenberg/](https://wordpress.github.io/gutenberg/).

#### Documentation

All components, in addition to being typed, should be using JSDoc when necessary — as explained in the [Coding Guidelines](/docs/contributors/code/coding-guidelines.md#javascript-documentation-using-jsdoc).

Each component that is exported from the `@wordpress/components` package should include a `README.md` file, explaining how to use the component, showing examples, and documenting all the props.

#### Folder structure

As a result of the above guidelines, all new components (except for shared utilities) should _generally_ follow this folder structure:

```
component-name/
├── component.tsx
├── context.ts
├── hook.ts
├── index.ts
├── README.md
├── styles.ts
└── types.ts
```

In case of a family of components (e.g. `Card` and `CardBody`, `CardFooter`, `CardHeader` ...), each component's implementation should live in a separate subfolder:
ciampo marked this conversation as resolved.
Show resolved Hide resolved

```
component-family-name/
├── sub-component-name/
│ ├── index.ts
│ ├── component.tsx
│ ├── hook.ts
│ ├── README.md
│ ├── styles.ts
│ └── types.ts
├── sub-component-name/
│ ├── index.ts
│ ├── component.tsx
│ ├── hook.ts
│ ├── README.md
│ ├── styles.ts
│ └── types.ts
├── stories
│ └── index.js
├── test
│ └── index.js
├── context.ts
└── index.ts
```
6 changes: 5 additions & 1 deletion packages/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install the module
npm install @wordpress/components --save
```

_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as IE browsers then using [core-js](https://github.com/zloirock/core-js) will add polyfills for these methods._
_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](/packages/babel-preset-default#polyfill) in your code._
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job with this note’s update. I need to copy it to all other packages 😃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #34878 to propagate the same change to other packages.


## Usage

Expand All @@ -32,3 +32,7 @@ Many components include CSS to add style, you will need to add in order to appea
In non-WordPress projects, link to the `build-style/style.css` file directly, it is located at `node_modules/@wordpress/components/build-style/style.css`.

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>

## Contributing

See [CONTRIBUTING.md](/packages/components/CONTRIBUTING.md) for the contributing guidelines for the `@wordpress/components` package.