Merge branch 'master' into canon-styles

This commit is contained in:
Charles de Dreuille
2025-03-13 14:20:57 +00:00
12 changed files with 141 additions and 10 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': minor
---
`SimpleStepper` back button now works with `activeStep` property set higher than 0
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': minor
---
Declared CancelIcon explicitly on Chip component inside Select.tsx to disable onMouseDown event by default that creates the flaw of re-opening select component when user tries to remove a selected filter.
+1 -2
View File
@@ -24,8 +24,7 @@ Install Canon using a package manager.
Import the global CSS file at the root of your application.
```tsx
import '@backstage/canon/css/core.css';
import '@backstage/canon/css/components.css';
import '@backstage/canon/css/styles.css';
```
### 3. Start building ✨
+3 -2
View File
@@ -92,10 +92,11 @@ the source code hosting provider. Notice that instead of the `dir:` prefix, the
Note, just as it's possible to specify a subdirectory with the `dir:` prefix,
you can also provide a path to a non-root directory inside the repository which
contains the `mkdocs.yml` file and `docs/` directory.
contains the `mkdocs.yml` file and `docs/` directory. It is important that it is
suffixed with a '/' in order for relative path resolution to work consistently.
e.g.
`url:https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/examples/documented-component`
`url:https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend/examples/documented-component/`
### Why is URL Reader faster than a git clone?
+1 -1
View File
@@ -2,4 +2,4 @@
storybook-static
# CSS output
css
/css
+1 -4
View File
@@ -1,10 +1,7 @@
import React from 'react';
import type { Preview, ReactRenderer } from '@storybook/react';
import { withThemeByDataAttribute } from '@storybook/addon-themes';
// Canon specific styles
import '../src/css/core.css';
import '../src/css/components.css';
import '../src/css/styles.css';
const preview: Preview = {
parameters: {
+1
View File
@@ -31,6 +31,7 @@ const componentsDir = 'src/components';
const cssFiles = [
{ path: `${cssDir}/core.css`, newName: 'core.css' },
{ path: `${cssDir}/components.css`, newName: 'components.css' },
{ path: `${cssDir}/styles.css`, newName: 'styles.css' },
];
// Components files
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import './core.css';
@import './components.css';
@@ -93,4 +93,62 @@ describe('<Select />', () => {
const input = getByTestId('custom-select');
expect(input.textContent).toBe('All results');
});
it('should not open dropdown when deleting Chip component from Select', () => {
const items = [
{ value: 'test_1', label: 'test 1' },
{ value: 'test_2', label: 'test 2' },
];
const handleChange = jest.fn();
const { getByTestId, queryByText } = render(
<Select
label="Default"
items={items}
multiple
selected={['test_1']} // Creates Chip Component initially
onChange={handleChange}
placeholder="All results"
/>,
);
// Verify Chip component exist
const chip = getByTestId('chip');
expect(chip).toBeInTheDocument();
expect(chip.textContent).toContain('test 1');
// Find cancel icon
const cancelIcon = getByTestId('cancel-icon');
expect(cancelIcon).toBeInTheDocument();
// Verify dropdown is initially closed
expect(queryByText('test 2')).not.toBeInTheDocument();
// Fire mouseDown on Chip's CancelIcon that tests if onMouseDown={event => event.stopPropagation()} is working properly
fireEvent.mouseDown(cancelIcon);
// Verify dropdown is closed after mouseDown on Chip's CancelIcon
expect(queryByText('test 2')).not.toBeInTheDocument();
// Delete the Chip
fireEvent.click(cancelIcon);
// Verify dropdown is still closed after the removal of Chip
expect(queryByText('test 2')).not.toBeInTheDocument();
// Verify Chip is removed
expect(chip).not.toBeInTheDocument();
expect(queryByText('test 1')).not.toBeInTheDocument();
// Verify we can still open the dropdown with a click on the select
const selectInput = getByTestId('select');
expect(selectInput.textContent).toBe('All results');
// Simulate click on select
fireEvent.mouseDown(within(selectInput).getByRole('button'));
// Now dropdown should be open
expect(queryByText('test 2')).toBeInTheDocument();
});
});
@@ -28,6 +28,7 @@ import {
withStyles,
} from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import CancelIcon from '@material-ui/icons/Cancel';
import React, { useEffect, useState } from 'react';
import ClosedDropdown from './static/ClosedDropdown';
@@ -222,9 +223,16 @@ export function SelectComponent(props: SelectProps) {
const item = items.find(el => el.value === selectedValue);
return item ? (
<Chip
data-testid="chip"
key={item?.value}
label={item?.label}
clickable
deleteIcon={
<CancelIcon
data-testid="cancel-icon"
onMouseDown={event => event.stopPropagation()}
/>
}
onDelete={handleDelete(selectedValue)}
className={classes.chip}
/>
@@ -173,4 +173,32 @@ describe('Stepper', () => {
fireEvent.click(getTextInSlide(rendered, 2)('Back') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
});
it('Handles onBack action properly when activeStep is higher than 0', async () => {
const rendered = await renderInTestApp(
<Stepper activeStep={2}>
<Step title="Step 0" data-testid="step0">
<div>step0</div>
</Step>
<Step title="Step 1" data-testid="step1">
<div>step1</div>
</Step>
<Step title="Step 2" data-testid="step2">
<div>step2</div>
</Step>
</Stepper>,
);
fireEvent.click(getTextInSlide(rendered, 2)('Back') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 1)('Back') as Node);
expect(rendered.getByText('step0')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 0)('Next') as Node);
expect(rendered.getByText('step1')).toBeInTheDocument();
fireEvent.click(getTextInSlide(rendered, 1)('Next') as Node);
expect(rendered.getByText('step2')).toBeInTheDocument();
});
});
@@ -32,6 +32,7 @@ type InternalState = {
};
const noop = () => {};
export const VerticalStepperContext = React.createContext<InternalState>({
stepperLength: 0,
stepIndex: 0,
@@ -50,7 +51,17 @@ export interface StepperProps {
export function SimpleStepper(props: PropsWithChildren<StepperProps>) {
const { children, elevated, onStepChange, activeStep = 0 } = props;
const [stepIndex, setStepIndex] = useState<number>(activeStep);
const [stepHistory, setStepHistory] = useState<number[]>([0]);
/*
Recreates the stepHistory array based on the activeStep
to make sure the handleBack function of the Footer works when activeStep is higher than 0
*/
const inOrderRecreatedStepHistory = Array.from(
{ length: activeStep + 1 },
(_, i) => i,
);
const [stepHistory, setStepHistory] = useState<number[]>(
inOrderRecreatedStepHistory,
);
useEffect(() => {
setStepIndex(activeStep);