Skip to content

chore(React19): Enable React19 #725

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/jsx-runtime",
"prettier"
],
"overrides": [
Expand Down Expand Up @@ -95,6 +96,7 @@
"react-hooks/exhaustive-deps": "warn",
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
"spaced-comment": "error",
"use-isnan": "error"
"use-isnan": "error",
"react/react-in-jsx-scope": "off"
}
}
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This repo contains a set of opinionated react component groups used to standardize functionality and look and feel across products. The components are based on PatternFly with some additional functionality.

### Branches

`main` - PatternFly 6 implementation

`v5` - PatternFly 5 implementation
Expand All @@ -14,22 +15,27 @@ This repo contains a set of opinionated react component groups used to standardi
---

### Migration from [RedHatInsights/frontend-components](https://github.com/RedHatInsights/frontend-components) to [patternfly/react-component-groups](https://github.com/patternfly/react-component-groups)

Please see the [migration guide](./migration.md)

---

## Contribution guide

### Before adding a new component:

- make sure your use case is new/complex enough to be added to this extension
- the component should bring a value value above and beyond existing PatternFly components

### To add a new component:

1. create a folder in `src/` matching its name (for example `src/MyComponent`)
2. to the new folder add a new `.tsx` file named after the component (for example `src/MyComponent/MyComponent.tsx`)
3. to the same folder include an `index.ts` which will export the component as a default and then all necessary interfaces
4. if this file structure is not met, your component won't be exposed correctly

#### Example component:

```
import * as React from 'react';
import { Content } from '@patternfly/react-core';
Expand All @@ -49,7 +55,8 @@ const useStyles = createUseStyles({
})

// do not use the named export of your component, just a default one
const MyComponent: React.FunctionComponent<MyComponentProps> = () => {
import { FunctionComponent } from 'react';
const MyComponent: FunctionComponent<MyComponentProps> = () => {
const classes = useStyles();

return (
Expand All @@ -60,42 +67,49 @@ const MyComponent: React.FunctionComponent<MyComponentProps> = () => {
};

export default MyComponent;
```
```

#### Index file example:

```
export { default } from './MyComponent';
export * from './MyComponent';
```
```

#### Component directory structure example:

```
src
|- MyComponent
|- index.ts
|- MyComponent.tsx
```
```

### Component's API rules:

- prop names comply with PatternFly components naming standards (`variant`, `onClick`, `position`, etc.)
- the API is maximally simplified and all props are provided with a description
- it is built on top of existing PatternFly types without prop omitting
- it is well documented using the PatternFly documentation (`/packages/module/patternfly-docs/content/extensions/component-groups/examples/MyComponent/MyComponent.md`) with examples of all possible use cases (`packages/module/patternfly-docs/content/extensions/component-groups/examples/MyComponent/MyComponent[...]Example.tsx`)
- do not unnecessarily use external libraries in your component - rather, delegate the necessary logic to the component's user using the component's API

#### Component API definition example:

```

import { FunctionComponent } from 'react';

// when possible, extend available PatternFly types
export interface MyComponentProps extends ButtonProps {
customLabel: Boolean
};

export const MyComponent: React.FunctionComponent<MyComponentProps> = ({ customLabel, ...props }) => ( ... );
export const MyComponent: FunctionComponent<MyComponentProps> = ({ customLabel, ...props }) => ( ... );
```


#### Markdown file example:
```

````
---
section: Component groups
subsection: My component's category
Expand All @@ -113,36 +127,42 @@ MyComponent has been created to demo contributing to this repository.

```js file="./MyComponentExample.tsx"```

```
````

#### Component usage file example: (`MyComponentExample.tsx`)

```
import React from 'react';
import { FunctionComponent } from 'react';

const MyComponentExample: React.FunctionComponent = () => (
const MyComponentExample: FunctionComponent = () => (
<MyComponent customLabel="My label">
);

export default MyComponentExample;
```

### Sub-components:
When adding a component for which it is advantageous to divide it into several sub-components make sure:

When adding a component for which it is advantageous to divide it into several sub-components make sure:

- component and all its sub-components are located in separate files and directories straight under the `src/` folder
- sub-components are exported and documented separately from their parent
- parent component should provide a way to pass props to all its sub-components

The aim is to enable the user of our "complex" component to use either complete or take advantage of its sub-components and manage their composition independently.

### Testing:

When adding/making changes to a component, always make sure your code is tested:
- use React Testing Library for unit testing

- use React Testing Library for unit testing
- add unit tests to a `[ComponentName].test.tsx` file to your component's directory
- make sure all the core functionality is covered using Cypress component or E2E tests
- add component tests to `cypress/component/[ComponentName].cy.tsx` file and E2E tests to `cypress/e2e/[ComponentName].spec.cy.ts`
- add `ouiaId` to the component props definition with a default value of the component name (for subcomponents, let's use `ComponentName-element-specification` naming convention e.g. `ouiaId="WarningModal-confirm-button"`)

### Styling:

- for styling always use JSS
- new classNames should be named in camelCase starting with the name of a given component and following with more details clarifying its purpose/component's subsection to which the class is applied (`actionMenu`, `actionMenuDropdown`, `actionMenuDropdownToggle`, etc.)
- do not use `pf-v6-u-XXX` classes, use CSS variables in a custom class instead (styles for the utility classes are not bundled with the standard patternfly.css - it would require the consumer to import also addons.css)
Expand All @@ -153,10 +173,12 @@ When adding/making changes to a component, always make sure your code is tested:
- run `npm run build`

## Development

- run `npm install`
- run `npm run start` to build and start the development server

## Testing and Linting

- run `npm run test` to run the unit tests
- run `npm run cypress:run:cp` to run Cypress component tests
- run `npm run cypress:run:e2e` to run Cypress E2E tests
Expand All @@ -165,4 +187,3 @@ When adding/making changes to a component, always make sure your code is tested:
## A11y testing

- run `npm run build:docs` followed by `npm run serve:docs`, then run `npm run test:a11y` in a new terminal window to run our accessibility tests for the components. Once the accessibility tests have finished running you can run `npm run serve:a11y` to locally view the generated report.

7 changes: 3 additions & 4 deletions cypress/component/Ansible.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React from 'react';
import { Ansible } from '@patternfly/react-component-groups/dist/dynamic/Ansible';

describe('Ansible', () => {
it('renders supported Ansible', () => {
cy.mount(<Ansible />)
cy.mount(<Ansible />);
cy.get('i').should('have.class', 'ansibleSupported-0-2-2');
});
it('renders unsupported Ansible', () => {
cy.mount(<Ansible isSupported={false}/>)
cy.mount(<Ansible isSupported={false} />);
cy.get('i').should('have.class', 'ansibleUnsupported-0-2-3');
});
});
});
56 changes: 29 additions & 27 deletions cypress/component/BulkSelect.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import React, { useState } from 'react';
import { useState } from 'react';
import BulkSelect, { BulkSelectProps, BulkSelectValue } from '../../packages/module/dist/dynamic/BulkSelect';

interface DataItem {
name: string
};
name: string;
}

const BulkSelectTestComponent = ({ canSelectAll, isDataPaginated }: Omit<BulkSelectProps, 'onSelect' | 'selectedCount' >) => {
const BulkSelectTestComponent = ({
canSelectAll,
isDataPaginated
}: Omit<BulkSelectProps, 'onSelect' | 'selectedCount'>) => {
const [ selected, setSelected ] = useState<DataItem[]>([]);

const allData = [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }, { name: '5' }, { name: '6' } ];
const pageData = [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }, { name: '5' } ];
const pageDataNames = pageData.map((item) => item.name);
const pageSelected = pageDataNames.every(item => selected.find(selectedItem => selectedItem.name === item));
const pageSelected = pageDataNames.every((item) => selected.find((selectedItem) => selectedItem.name === item));

const handleBulkSelect = (value: BulkSelectValue) => {
if (value === BulkSelectValue.page) {
const updatedSelection = [ ...selected ];
pageData.forEach(item => !updatedSelection.some(selectedItem => selectedItem.name === item.name) && updatedSelection.push(item));
pageData.forEach(
(item) =>
!updatedSelection.some((selectedItem) => selectedItem.name === item.name) && updatedSelection.push(item)
);
setSelected(updatedSelection);
}
value === BulkSelectValue.nonePage && setSelected(selected.filter(item => !pageDataNames.includes(item.name)))
value === BulkSelectValue.nonePage && setSelected(selected.filter((item) => !pageDataNames.includes(item.name)));
value === BulkSelectValue.none && setSelected([]);
value === BulkSelectValue.all && setSelected(allData);
};
Expand All @@ -32,71 +38,67 @@ const BulkSelectTestComponent = ({ canSelectAll, isDataPaginated }: Omit<BulkSel
totalCount={allData.length}
selectedCount={selected.length}
pageSelected={pageSelected}
pagePartiallySelected={pageDataNames.some(item => selected.find(selectedItem => selectedItem.name === item)) && !pageSelected}
pagePartiallySelected={
pageDataNames.some((item) => selected.find((selectedItem) => selectedItem.name === item)) && !pageSelected
}
onSelect={handleBulkSelect}
/>
);
};

describe('BulkSelect', () => {
it('renders the bulk select without all', () => {
cy.mount(
<BulkSelectTestComponent />
);
cy.mount(<BulkSelectTestComponent />);
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click();
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').should('not.exist');
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').should('exist');

cy.contains('0 selected').should('not.exist');
});

it('renders the bulk select with all and without page', () => {
cy.mount(
<BulkSelectTestComponent canSelectAll isDataPaginated={false} />
);
cy.mount(<BulkSelectTestComponent canSelectAll isDataPaginated={false} />);
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click();
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').should('not.exist');
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').should('exist');

cy.contains('0 selected').should('not.exist');
});

it('renders the bulk select with data', () => {
cy.mount(
<BulkSelectTestComponent canSelectAll />
);

cy.mount(<BulkSelectTestComponent canSelectAll />);

// Initial state
cy.get('input[type="checkbox"]').each(($checkbox) => {
cy.wrap($checkbox).should('not.be.checked');
});

// Checkbox select
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').first().click();
cy.get('input[type="checkbox"]').should('be.checked');
cy.contains('5 selected').should('exist');

// Select none
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').first().click();
cy.get('input[type="checkbox"]').should('not.be.checked');

// Select all
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').first().click();
cy.contains('6 selected').should('exist');

// Checkbox deselect
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').first().click({ force: true });
cy.contains('1 selected').should('exist');

// Select page
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').first().click();
cy.contains('6 selected').should('exist');
});
});
});
15 changes: 11 additions & 4 deletions cypress/component/CloseButton.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import React from 'react';
import CloseButton from '../../packages/module/dist/dynamic/CloseButton';

describe('CloseButton', () => {
/* eslint-disable no-console */
it('renders the Close button', () => {
cy.mount(<CloseButton dataTestID="close-button-example" onClick={()=>{console.log('Close button clicked')}} style={{ float: 'none' }}/>)
cy.mount(
<CloseButton
dataTestID="close-button-example"
onClick={() => {
console.log('Close button clicked');
}}
style={{ float: 'none' }}
/>
);
cy.get('[data-test-id="close-button-example"]').should('exist');
});
it('should call callback on click', () => {
const onClickSpy = cy.spy().as('onClickSpy');
cy.mount(<CloseButton dataTestID="close-button-example" onClick={onClickSpy}/>);
cy.mount(<CloseButton dataTestID="close-button-example" onClick={onClickSpy} />);
cy.get('[data-test-id="close-button-example"]').click();
cy.get('@onClickSpy').should('have.been.called');
});
})
});
19 changes: 12 additions & 7 deletions cypress/component/ErrorBoundary.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import React from 'react';
import ErrorBoundary from '../../packages/module/dist/dynamic/ErrorBoundary';

describe('ErrorBoundary', () => {
it('renders the ErrorBoundary ', () => {
cy.mount(<ErrorBoundary headerTitle="My app header" errorTitle="Something wrong happened"><div data-ouia-component-id="test">Test</div></ErrorBoundary>)
cy.mount(
<ErrorBoundary headerTitle="My app header" errorTitle="Something wrong happened">
<div data-ouia-component-id="test">Test</div>
</ErrorBoundary>
);
cy.get('[data-ouia-component-id="test"]').should('have.text', 'Test');
});

it('should expand the details section', () => {
const Surprise = () => {
throw new Error('but a welcome one');
};
cy.mount(<ErrorBoundary headerTitle="My app header" errorTitle="Something wrong happened">
<Surprise />
</ErrorBoundary>)
cy.mount(
<ErrorBoundary headerTitle="My app header" errorTitle="Something wrong happened">
<Surprise />
</ErrorBoundary>
);

cy.get('[data-ouia-component-id="ErrorBoundary-toggle"').click();
cy.get('[class="pf-v5-c-expandable-section__content"]').should('contain.text', 'Error: but a welcome one');
})
})
});
});
Loading