diff --git a/.changeset/beige-chairs-suffer.md b/.changeset/beige-chairs-suffer.md
new file mode 100644
index 0000000000..c642b70cbb
--- /dev/null
+++ b/.changeset/beige-chairs-suffer.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': minor
+---
+
+`SimpleStepper` back button now works with `activeStep` property set higher than 0
diff --git a/.changeset/metal-crabs-agree.md b/.changeset/metal-crabs-agree.md
new file mode 100644
index 0000000000..c5a2d1102d
--- /dev/null
+++ b/.changeset/metal-crabs-agree.md
@@ -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.
diff --git a/canon-docs/src/app/(docs)/page.mdx b/canon-docs/src/app/(docs)/page.mdx
index b2abdefb12..862127ec7b 100644
--- a/canon-docs/src/app/(docs)/page.mdx
+++ b/canon-docs/src/app/(docs)/page.mdx
@@ -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 ✨
diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md
index 5a9044842f..9444dc9b6e 100644
--- a/docs/features/techdocs/how-to-guides.md
+++ b/docs/features/techdocs/how-to-guides.md
@@ -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?
diff --git a/packages/canon/.gitignore b/packages/canon/.gitignore
index e7c52ff306..b200b44847 100644
--- a/packages/canon/.gitignore
+++ b/packages/canon/.gitignore
@@ -2,4 +2,4 @@
storybook-static
# CSS output
-css
+/css
diff --git a/packages/canon/.storybook/preview.tsx b/packages/canon/.storybook/preview.tsx
index e278bb011f..b1bfa2dc88 100644
--- a/packages/canon/.storybook/preview.tsx
+++ b/packages/canon/.storybook/preview.tsx
@@ -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: {
diff --git a/packages/canon/scripts/build-css.mjs b/packages/canon/scripts/build-css.mjs
index b2782e090e..80f566ac7e 100644
--- a/packages/canon/scripts/build-css.mjs
+++ b/packages/canon/scripts/build-css.mjs
@@ -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
diff --git a/packages/canon/src/css/styles.css b/packages/canon/src/css/styles.css
new file mode 100644
index 0000000000..f5d623389f
--- /dev/null
+++ b/packages/canon/src/css/styles.css
@@ -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';
diff --git a/packages/core-components/src/components/Select/Select.test.tsx b/packages/core-components/src/components/Select/Select.test.tsx
index b83ac68d5b..505152db5f 100644
--- a/packages/core-components/src/components/Select/Select.test.tsx
+++ b/packages/core-components/src/components/Select/Select.test.tsx
@@ -93,4 +93,62 @@ describe('', () => {
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(
+ ,
+ );
+
+ // 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();
+ });
});
diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx
index 6b5174820b..7a862678b8 100644
--- a/packages/core-components/src/components/Select/Select.tsx
+++ b/packages/core-components/src/components/Select/Select.tsx
@@ -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 ? (
event.stopPropagation()}
+ />
+ }
onDelete={handleDelete(selectedValue)}
className={classes.chip}
/>
diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx
index 73baff8db2..e4093f07d2 100644
--- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx
+++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.test.tsx
@@ -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(
+
+
+
step0
+
+
+
step1
+
+
+
step2
+
+ ,
+ );
+
+ 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();
+ });
});
diff --git a/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx b/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx
index 4a1a4ab919..b0b6c1afd0 100644
--- a/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx
+++ b/packages/core-components/src/components/SimpleStepper/SimpleStepper.tsx
@@ -32,6 +32,7 @@ type InternalState = {
};
const noop = () => {};
+
export const VerticalStepperContext = React.createContext({
stepperLength: 0,
stepIndex: 0,
@@ -50,7 +51,17 @@ export interface StepperProps {
export function SimpleStepper(props: PropsWithChildren) {
const { children, elevated, onStepChange, activeStep = 0 } = props;
const [stepIndex, setStepIndex] = useState(activeStep);
- const [stepHistory, setStepHistory] = useState([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(
+ inOrderRecreatedStepHistory,
+ );
useEffect(() => {
setStepIndex(activeStep);