Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
- Node.js:
>=16.18.0 - Yarn:
>=1.21.1
To install all dependencies and build all packages run the following commands from the root of the project.
yarn
yarn build
You are ready to go! You can either develop your apps from apps folder or run a playground of all components in development mode.
Good pull requests, such as patches, improvements, and new features, are a fantastic help. They should remain focused in scope and avoid containing unrelated commits.
Please ask first if somebody else is already working on this or the Contentful developers think your feature is in-scope. Generally always have a related issue with discussions for whatever you are including.
@contenful/field-editors is a monorepo, meaning it is divided into independent sub-packages.
These packages can be found in the packages/ directory.
This monorepo is maintained using Lerna. Get started with Lerna by following this link.
We use storybook as a components playground.
# to run playground in a development mode
yarn startGo to a shared package and run the following command to watch for changes and rebuild the package once it's changed:
cd packages/_shared
yarn watchRelevant for Contentful employees only.
It is convenient to link a local copy of a package to a locally running Contentful web application without publishing a package.
yarn
yarn build
cd packages/single-line
# register a symplink
yarn link
# watch for changes in the package
yarn watchIn the web app repository:
# create a symlink to a local folder
yarn link '@contentful/field-editor-single-line'To add another package create a new directory in the packages folder. Since we are using Lerna all package scripts are available from the root by running lerna run <script_name>.
The project supports i18n thanks to the Lingui library.
To translate your strings, you need to make them identifiable with a translation key, represented by the id.
To ensure uniqueness, consistency, and descriptive keys, the keys follow a naming convention.
A custom ESLint rule is available to ensure the naming convention is followed, and that only supported projects prefixes are used.
Depending on the structure and complexity of your strings, use the t function from @lingui/core/macro or the Trans react component from @lingui/react. Important: you have to add a default message to every new key added, in case the frontend application where a package is used, is not providing a translation catalog.
All commit messages should meet the conventional commit format. The easiest way is to use yarn cm command which launches commit message wizard.
You don't need to worry about formatting your code. It is automatically reformatted using prettier on every commit using Git hooks.
We use ESLint and Typescript ESLint for linting and checking code for errors.
All modern editors should automatically pick up configuration and show errors and warnings while you type.
# at the monorepo root
yarn lint# at the monorepo root
yarn tscWe use Vitest and Testing Library for writing unit tests.
cd packages/single-line
yarn test# at the monorepo root
yarn test:ciThe repo has three complementary layers of accessibility tooling, active at different points in the development workflow.
The accessibility addon is pre-configured. Run yarn start and open any story — the Accessibility panel shows live axe results as you develop.
We use vitest-axe for automated accessibility checks in unit tests. The toHaveNoViolations matcher is globally available.
Every component needs axe coverage across its meaningful states. A single test on the default render is not sufficient: a component that passes in its default state can still have violations when disabled, in an error state, or with different content rendered. Cover each state where the DOM structure or ARIA attributes change.
What to cover per component:
- Default / empty — baseline render with no value
- Filled — rendered with actual content (especially relevant for rich text or media editors where content changes the DOM)
- Disabled — if element can be rendered in a disabled state
- Error / validation state — after errors are emitted
- Any state that adds or removes interactive elements (e.g. dialogs, popovers, expanded panels)
import { axe } from 'vitest-axe';
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { createFakeFieldAPI, createFakeLocalesAPI } from '@contentful/field-editor-test-utils';
import { SingleLineEditor } from './SingleLineEditor';
describe('SingleLineEditor accessibility', () => {
it('has no violations in default state', async () => {
const [field] = createFakeFieldAPI((f) => ({ ...f, type: 'Symbol' }));
const { container } = render(
<SingleLineEditor
field={field}
isInitiallyDisabled={false}
locales={createFakeLocalesAPI()}
/>,
);
expect(await axe(container)).toHaveNoViolations();
});
it('has no violations when disabled', async () => {
const [field] = createFakeFieldAPI((f) => ({ ...f, type: 'Symbol' }));
const { container } = render(
<SingleLineEditor
field={field}
isInitiallyDisabled={true}
locales={createFakeLocalesAPI()}
/>,
);
expect(await axe(container)).toHaveNoViolations();
});
it('has no violations with a validation error', async () => {
const [field, mitt] = createFakeFieldAPI((f) => ({ ...f, type: 'Symbol' }));
const { container } = render(
<SingleLineEditor
field={field}
isInitiallyDisabled={false}
locales={createFakeLocalesAPI()}
/>,
);
// trigger an error state before asserting
act(() => mitt.emit('schemaErrors', [{ message: 'Required' }]));
expect(await axe(container)).toHaveNoViolations();
});
});Component tests have cypress-axe wired up via cypress/support/component.ts. Use cy.checkA11y() to assert no violations after mounting a component.
Axe is injected lazily on the first cy.checkA11y() call. Four structural rules are globally disabled as they do not apply to isolated component tests: html-has-lang, landmark-one-main, page-has-heading-one, region. Violations are printed to the terminal for readable CI output.
To scope the check or disable a rule for one test:
// Check only a specific element
cy.checkA11y('[role="dialog"]');
// Disable a rule for one test
cy.checkA11y(undefined, { rules: { 'color-contrast': { enabled: false } } });it('has no a11y violations', () => {
cy.mount(<MyComponent />);
cy.checkA11y();
});