feat: add overrideable FavoriteToggle to core-components

Signed-off-by: Carl-Erik Bergström <cbergstrom@spotify.com>
This commit is contained in:
Carl-Erik Bergström
2024-09-05 15:28:56 +02:00
parent 50bedf65d1
commit c891b694c4
12 changed files with 289 additions and 56 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/core-components': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-techdocs': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-home': patch
---
Add `FavoriteToggle` in `core-components` to standardise favorite marking
+27
View File
@@ -19,6 +19,7 @@ import { default as CSS_2 } from 'csstype';
import { CSSProperties } from 'react';
import { ElementType } from 'react';
import { ErrorInfo } from 'react';
import IconButton from '@material-ui/core/IconButton';
import { IconComponent } from '@backstage/core-plugin-api';
import { Icons } from '@material-table/core';
import { IdentityApi } from '@backstage/core-plugin-api';
@@ -401,6 +402,32 @@ export type ErrorPanelProps = {
title?: string;
};
// @public
export function FavoriteToggle({
id,
title,
isFavorite: value,
onToggle: onChange,
...iconButtonProps
}: FavoriteToggleProps): React_2.JSX.Element;
// @public
export function FavoriteToggleIcon({
isFavorite,
}: {
isFavorite: boolean;
}): React_2.JSX.Element;
// Warning: (ae-missing-release-tag) "FavoriteToggleProps" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type FavoriteToggleProps = ComponentProps<typeof IconButton> & {
id: string;
title: string;
isFavorite: boolean;
onToggle: (value: boolean) => void;
};
// @public (undocumented)
export type FeatureCalloutCircleClassKey =
| '@keyframes pulsateSlightly'
@@ -0,0 +1,68 @@
/*
* 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 React from 'react';
import { FavoriteToggle } from './FavoriteToggle';
import {
UnifiedThemeProvider,
createBaseThemeOptions,
createUnifiedTheme,
palettes,
} from '@backstage/theme';
export default {
title: 'Core/FavoriteToggle',
component: FavoriteToggle,
};
export const Default = () => {
const [isFavorite, setFavorite] = React.useState(false);
return (
<FavoriteToggle
id="favorite-toggle"
title="Add entity to favorites"
isFavorite={isFavorite}
onToggle={setFavorite}
/>
);
};
const theme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.dark,
}),
components: {
BackstageFavoriteToggleIcon: {
styleOverrides: {
icon: () => ({ color: 'aqua' }),
iconBorder: () => ({ color: 'white' }),
},
},
},
});
export const WithThemeOverride = () => {
const [isFavorite, setFavorite] = React.useState(false);
return (
<UnifiedThemeProvider theme={theme}>
<FavoriteToggle
id="favorite-toggle"
title="Add entity to favorites"
isFavorite={isFavorite}
onToggle={setFavorite}
/>
</UnifiedThemeProvider>
);
};
@@ -0,0 +1,55 @@
/*
* 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 { render } from '@testing-library/react';
import { FavoriteToggle, FavoriteToggleProps } from './FavoriteToggle';
import React from 'react';
import userEvent from '@testing-library/user-event';
describe('<FavoriteToggle />', () => {
const onToggle = jest.fn();
const props: FavoriteToggleProps = {
title: 'Favorite this thing',
id: 'some-thing-favorite',
onToggle,
isFavorite: true,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders with valid props', async () => {
const { getByRole } = render(<FavoriteToggle {...props} />);
expect(getByRole('button', { name: props.title })).toBeInTheDocument();
});
it('should return inverted value on toggle', async () => {
const { getByRole } = render(<FavoriteToggle {...props} />);
await userEvent.click(getByRole('button', { name: props.title }));
expect(onToggle).toHaveBeenCalledWith(!props.isFavorite);
});
it('should show accessible tooltip', async () => {
const { findByRole, getByRole } = render(<FavoriteToggle {...props} />);
await userEvent.hover(getByRole('button', { name: props.title }));
expect(await findByRole('tooltip')).toHaveTextContent(props.title);
});
});
@@ -0,0 +1,92 @@
/*
* 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 React, { ComponentProps } from 'react';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import { Theme, makeStyles } from '@material-ui/core/styles';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
const useStyles = makeStyles<Theme>(
theme => ({
icon: {
color: '#f3ba37',
cursor: 'pointer',
},
iconBorder: {
color: theme.palette.text.primary,
cursor: 'pointer',
},
}),
{ name: 'BackstageFavoriteToggleIcon' },
);
// @public (undocumented)
export type FavoriteToggleIconClassKey = 'icon' | 'iconBorder';
// @public (undocumented)
export type FavoriteToggleProps = ComponentProps<typeof IconButton> & {
id: string;
title: string;
isFavorite: boolean;
onToggle: (value: boolean) => void;
};
/**
* Icon used in FavoriteToggle component.
*
* Can be used independently, useful when used as {@link @material-table/core#MaterialTableProps.actions} in {@link @material-table/core#MaterialTable}
*
* @public
*/
export function FavoriteToggleIcon({ isFavorite }: { isFavorite: boolean }) {
const classes = useStyles();
return isFavorite ? (
<Star className={classes.icon} />
) : (
<StarBorder className={classes.iconBorder} />
);
}
/**
* Toggle encapsulating logic for marking something as favorite,
* primarily used in various instances of entity lists and cards but can be used elsewhere.
*
* This component can only be used in as a controlled toggle and does not keep internal state.
*
* @public
*/
export function FavoriteToggle({
id,
title,
isFavorite: value,
onToggle: onChange,
...iconButtonProps
}: FavoriteToggleProps) {
return (
<Tooltip id={id} title={title}>
<IconButton
aria-label={title}
id={id}
onClick={() => onChange(!value)}
{...iconButtonProps}
>
<FavoriteToggleIcon isFavorite={value} />
</IconButton>
</Tooltip>
);
}
@@ -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.
*/
export { FavoriteToggle, FavoriteToggleIcon } from './FavoriteToggle';
export type { FavoriteToggleProps } from './FavoriteToggle';
@@ -25,6 +25,7 @@ export * from './DependencyGraph';
export * from './DismissableBanner';
export * from './EmptyState';
export * from './ErrorPanel';
export * from './FavoriteToggle';
export * from './ResponseErrorPanel';
export * from './FeatureDiscovery';
export * from './HeaderIconLinkRow';
@@ -91,6 +91,7 @@ import {
BoldHeaderClassKey,
CardTabClassKey,
} from './layout';
import { FavoriteToggleIconClassKey } from './components/FavoriteToggle/FavoriteToggle';
type BackstageComponentsNameToClassKey = {
BackstageAvatar: AvatarClassKey;
@@ -163,6 +164,7 @@ type BackstageComponentsNameToClassKey = {
BackstageTabbedCard: TabbedCardClassKey;
BackstageTabbedCardBoldHeader: BoldHeaderClassKey;
BackstageCardTab: CardTabClassKey;
BackstageFavoriteToggleIcon: FavoriteToggleIconClassKey;
};
/** @public */
@@ -16,26 +16,17 @@
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import IconButton from '@material-ui/core/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import { withStyles } from '@material-ui/core/styles';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import React, { ComponentProps } from 'react';
import { useStarredEntity } from '../../hooks/useStarredEntity';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { FavoriteToggle } from '@backstage/core-components';
/** @public */
export type FavoriteEntityProps = ComponentProps<typeof IconButton> & {
entity: Entity;
};
const YellowStar = withStyles({
root: {
color: '#f3ba37',
},
})(Star);
/**
* IconButton for showing if a current entity is starred and adding/removing it from the favorite entities
* @param props - MaterialUI IconButton props extended by required `entity` prop
@@ -56,16 +47,12 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => {
)}`;
return (
<IconButton
aria-label={title}
<FavoriteToggle
title={title}
id={id}
color="inherit"
isFavorite={isStarredEntity}
onToggle={toggleStarredEntity}
{...props}
onClick={() => toggleStarredEntity()}
>
<Tooltip id={id} title={title}>
{isStarredEntity ? <YellowStar /> : <StarBorder />}
</Tooltip>
</IconButton>
/>
);
};
@@ -35,12 +35,9 @@ import {
useStarredEntities,
} from '@backstage/plugin-catalog-react';
import Typography from '@material-ui/core/Typography';
import { withStyles } from '@material-ui/core/styles';
import { visuallyHidden } from '@mui/utils';
import Edit from '@material-ui/icons/Edit';
import OpenInNew from '@material-ui/icons/OpenInNew';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
import { capitalize } from 'lodash';
import pluralize from 'pluralize';
import React, { ReactNode, useMemo } from 'react';
@@ -50,6 +47,7 @@ import { PaginatedCatalogTable } from './PaginatedCatalogTable';
import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { catalogTranslationRef } from '../../alpha/translation';
import { FavoriteToggleIcon } from '@backstage/core-components';
/**
* Props for {@link CatalogTable}.
@@ -64,12 +62,6 @@ export interface CatalogTableProps {
subtitle?: string;
}
const YellowStar = withStyles({
root: {
color: '#f3ba37',
},
})(Star);
const refCompare = (a: Entity, b: Entity) => {
const toRef = (entity: Entity) =>
entity.metadata.title ||
@@ -160,12 +152,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
return {
cellStyle: { paddingLeft: '1em' },
icon: () => (
<>
<Typography style={visuallyHidden}>{title}</Typography>
{isStarred ? <YellowStar /> : <StarBorder />}
</>
),
icon: () => <FavoriteToggleIcon isFavorite={isStarred} />,
tooltip: title,
onClick: () => toggleStarredEntity(entity),
};
@@ -17,14 +17,12 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { entityRouteParams } from '@backstage/plugin-catalog-react';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import Tooltip from '@material-ui/core/Tooltip';
import IconButton from '@material-ui/core/IconButton';
import ListItemText from '@material-ui/core/ListItemText';
import React from 'react';
import { Link } from 'react-router-dom';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { useRouteRef } from '@backstage/core-plugin-api';
import StarIcon from '@material-ui/icons/Star';
import { FavoriteToggle } from '@backstage/core-components';
type EntityListItemProps = {
entity: Entity;
@@ -40,15 +38,12 @@ export const StarredEntityListItem = ({
return (
<ListItem key={stringifyEntityRef(entity)}>
<ListItemIcon>
<Tooltip title="Remove from starred">
<IconButton
edge="end"
aria-label="unstar"
onClick={() => onToggleStarredEntity(entity)}
>
<StarIcon style={{ color: '#f3ba37' }} />
</IconButton>
</Tooltip>
<FavoriteToggle
id={`remove-favorite-${entity.metadata.uid}`}
title="Remove entity from favorites"
isFavorite
onToggle={() => onToggleStarredEntity(entity)}
/>
</ListItemIcon>
<Link to={catalogEntityRoute(entityRouteParams(entity))}>
<ListItemText primary={entity.metadata.title ?? entity.metadata.name} />
@@ -17,15 +17,7 @@
import React from 'react';
import ShareIcon from '@material-ui/icons/Share';
import { DocsTableRow } from './types';
import { withStyles } from '@material-ui/core/styles';
import Star from '@material-ui/icons/Star';
import StarBorder from '@material-ui/icons/StarBorder';
const YellowStar = withStyles({
root: {
color: '#f3ba37',
},
})(Star);
import { FavoriteToggleIcon } from '@backstage/core-components';
/**
* Not directly exported, but through DocsTable.actions and EntityListDocsTable.actions
@@ -52,7 +44,7 @@ export const actionFactories = {
const isStarred = isStarredEntity(entity);
return {
cellStyle: { paddingLeft: '1em' },
icon: () => (isStarred ? <YellowStar /> : <StarBorder />),
icon: () => <FavoriteToggleIcon isFavorite={isStarred} />,
tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites',
onClick: () => toggleStarredEntity(entity),
};