NestedFolderPicker: Add clearable prop (#81114)

* add clearable prop to NestedFolderPicker

* update types
This commit is contained in:
Ashley Harrison 2024-01-24 13:18:01 +00:00 committed by GitHub
parent 7872a128a2
commit 3f1e97cb07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 74 additions and 8 deletions

View File

@ -113,6 +113,13 @@ describe('NestedFolderPicker', () => {
expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title);
});
it('can clear a selection if clearable is specified', async () => {
render(<NestedFolderPicker clearable value={folderA.item.uid} onChange={mockOnChange} />);
await userEvent.click(await screen.findByRole('button', { name: 'Clear selection' }));
expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined);
});
it('can select a folder from the picker with the keyboard', async () => {
render(<NestedFolderPicker onChange={mockOnChange} />);
const button = await screen.findByRole('button', { name: 'Select folder' });

View File

@ -42,7 +42,10 @@ export interface NestedFolderPickerProps {
excludeUIDs?: string[];
/* Callback for when the user selects a folder */
onChange?: (folderUID: string, folderName: string) => void;
onChange?: (folderUID: string | undefined, folderName: string | undefined) => void;
/* Whether the picker should be clearable */
clearable?: boolean;
}
const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const];
@ -64,6 +67,7 @@ export function NestedFolderPicker({
value,
invalid,
showRootFolder = true,
clearable = false,
excludeUIDs,
onChange,
}: NestedFolderPickerProps) {
@ -158,6 +162,17 @@ export function NestedFolderPicker({
[onChange]
);
const handleClearSelection = useCallback(
(event: React.MouseEvent<SVGElement> | React.KeyboardEvent<SVGElement>) => {
event.preventDefault();
event.stopPropagation();
if (onChange) {
onChange(undefined, undefined);
}
},
[onChange]
);
const handleCloseOverlay = useCallback(() => setOverlayOpen(false), [setOverlayOpen]);
const baseHandleLoadMore = useLoadNextChildrenPage(EXCLUDED_KINDS);
@ -259,6 +274,7 @@ export function NestedFolderPicker({
return (
<Trigger
label={label}
handleClearSelection={clearable && value !== undefined ? handleClearSelection : undefined}
invalid={invalid}
isLoading={selectedFolder.isLoading}
autoFocus={autoFocusButton}

View File

@ -4,19 +4,29 @@ import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, getInputStyles, useTheme2, Text } from '@grafana/ui';
import { focusCss } from '@grafana/ui/src/themes/mixins';
import { Trans } from 'app/core/internationalization';
import { focusCss, getFocusStyles, getMouseFocusStyles } from '@grafana/ui/src/themes/mixins';
import { Trans, t } from 'app/core/internationalization';
interface TriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLoading: boolean;
handleClearSelection?: (event: React.MouseEvent<SVGElement> | React.KeyboardEvent<SVGElement>) => void;
invalid?: boolean;
label?: ReactNode;
}
function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: React.ForwardedRef<HTMLButtonElement>) {
function Trigger(
{ handleClearSelection, isLoading, invalid, label, ...rest }: TriggerProps,
ref: React.ForwardedRef<HTMLButtonElement>
) {
const theme = useTheme2();
const styles = getStyles(theme, invalid);
const handleKeyDown = (event: React.KeyboardEvent<SVGElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
handleClearSelection?.(event);
}
};
return (
<div className={styles.wrapper}>
<div className={styles.inputWrapper}>
@ -41,6 +51,18 @@ function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: Reac
<Trans i18nKey="browse-dashboards.folder-picker.button-label">Select folder</Trans>
</Text>
)}
{!isLoading && handleClearSelection && (
<Icon
role="button"
tabIndex={0}
aria-label={t('browse-dashboards.folder-picker.clear-selection', 'Clear selection')}
className={styles.clearIcon}
name="times"
onClick={handleClearSelection}
onKeyDown={handleKeyDown}
/>
)}
</button>
<div className={styles.suffix}>
@ -92,11 +114,26 @@ const getStyles = (theme: GrafanaTheme2, invalid = false) => {
'&:focus-visible': css`
${focusCss(theme)}
`,
alignItems: 'center',
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'space-between',
paddingRight: 28,
},
]),
hasPrefix: css({
paddingLeft: 28,
}),
clearIcon: css({
color: theme.colors.text.secondary,
cursor: 'pointer',
'&:hover': {
color: theme.colors.text.primary,
},
'&:focus:not(:focus-visible)': getMouseFocusStyles(theme),
'&:focus-visible': getFocusStyles(theme),
}),
};
};

View File

@ -90,7 +90,7 @@ export class GeneralSettingsEditView
this._dashboard.setState({ tags: value });
};
public onFolderChange = (newUID: string, newTitle: string) => {
public onFolderChange = (newUID: string | undefined, newTitle: string | undefined) => {
const newMeta = {
...this._dashboard.state.meta,
folderUid: newUID || this._dashboard.state.meta.folderUid,

View File

@ -45,7 +45,7 @@ export function GeneralSettingsUnconnected({
const [dashboardDescription, setDashboardDescription] = useState(dashboard.description);
const pageNav = config.featureToggles.dockedMegaMenu ? sectionNav.node.parentItem : undefined;
const onFolderChange = (newUID: string, newTitle: string) => {
const onFolderChange = (newUID: string | undefined, newTitle: string | undefined) => {
dashboard.meta.folderUid = newUID;
dashboard.meta.folderTitle = newTitle;
dashboard.meta.hasUnsavedFolderChange = true;

View File

@ -167,7 +167,7 @@ export const SaveDashboardAsForm = ({
render={({ field: { ref, ...field } }) => (
<FolderPicker
{...field}
onChange={(uid: string, title: string) => field.onChange({ uid, title })}
onChange={(uid: string | undefined, title: string | undefined) => field.onChange({ uid, title })}
value={field.value?.uid}
// Old folder picker fields
initialTitle={dashboard.meta.folderTitle}

View File

@ -57,7 +57,7 @@ export const plugin = new PanelPlugin<Options>(DashList)
id: 'folderUID',
defaultValue: undefined,
editor: function RenderFolderPicker({ value, onChange }) {
return <FolderPicker value={value} onChange={(folderUID) => onChange(folderUID)} />;
return <FolderPicker clearable value={value} onChange={(folderUID) => onChange(folderUID)} />;
},
})
.addCustomEditor({

View File

@ -76,6 +76,7 @@
"folder-picker": {
"accessible-label": "Ordner auswählen: {{ label }} aktuell ausgewählt",
"button-label": "Ordner auswählen",
"clear-selection": "",
"empty-message": "Keine Ordner gefunden",
"error-title": "Fehler beim Laden der Ordner",
"search-placeholder": "Ordner suchen",

View File

@ -76,6 +76,7 @@
"folder-picker": {
"accessible-label": "Select folder: {{ label }} currently selected",
"button-label": "Select folder",
"clear-selection": "Clear selection",
"empty-message": "No folders found",
"error-title": "Error loading folders",
"search-placeholder": "Search folders",

View File

@ -81,6 +81,7 @@
"folder-picker": {
"accessible-label": "Seleccionar carpeta: {{ label }} seleccionada actualmente",
"button-label": "Seleccionar carpeta",
"clear-selection": "",
"empty-message": "No se ha encontrado ninguna carpeta",
"error-title": "Error al cargar las carpetas",
"search-placeholder": "Buscar carpetas",

View File

@ -81,6 +81,7 @@
"folder-picker": {
"accessible-label": "Sélectionnez un dossier : {{ label }} est sélectionné actuellement",
"button-label": "Sélectionner un dossier",
"clear-selection": "",
"empty-message": "Aucun dossier trouvé",
"error-title": "Impossible de charger les dossiers",
"search-placeholder": "Rechercher dans les dossiers",

View File

@ -76,6 +76,7 @@
"folder-picker": {
"accessible-label": "Ŝęľęčŧ ƒőľđęř: {{ label }} čūřřęʼnŧľy şęľęčŧęđ",
"button-label": "Ŝęľęčŧ ƒőľđęř",
"clear-selection": "Cľęäř şęľęčŧįőʼn",
"empty-message": "Ńő ƒőľđęřş ƒőūʼnđ",
"error-title": "Ēřřőř ľőäđįʼnģ ƒőľđęřş",
"search-placeholder": "Ŝęäřčĥ ƒőľđęřş",

View File

@ -71,6 +71,7 @@
"folder-picker": {
"accessible-label": "选择文件夹:{{ label }} 当前已选择",
"button-label": "选择文件夹",
"clear-selection": "",
"empty-message": "未找到文件夹",
"error-title": "加载文件夹时出错",
"search-placeholder": "搜索文件夹",