From fbe980779e38f5fa8a9bd148e280f85ec8b0ec16 Mon Sep 17 00:00:00 2001
From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com>
Date: Fri, 2 Sep 2022 08:31:37 -0300
Subject: [PATCH] feat: Adds a helper text option to the Select component
(#21269)
Co-authored-by: Geido <60598000+geido@users.noreply.github.com>
---
.../components/Select/AsyncSelect.test.tsx | 74 ++++++++-----------
.../src/components/Select/AsyncSelect.tsx | 30 +++++++-
.../src/components/Select/Select.test.tsx | 14 ++++
.../src/components/Select/Select.tsx | 28 ++++++-
4 files changed, 100 insertions(+), 46 deletions(-)
diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
index 8a50002c866f8..e5569f1be0fc4 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx
@@ -206,13 +206,7 @@ test('sort the options using a custom sort comparator', async () => {
option1: typeof OPTIONS[0],
option2: typeof OPTIONS[0],
) => option1.gender.localeCompare(option2.gender);
- render(
- ,
- );
+ render();
await open();
const options = await findAllSelectOptions();
const optionsPage = OPTIONS.slice(0, defaultProps.pageSize);
@@ -294,7 +288,7 @@ test('searches for label or value', async () => {
});
test('search order exact and startWith match first', async () => {
- render();
+ render();
await open();
await type('Her');
expect(await findSelectOption('Guilherme')).toBeInTheDocument();
@@ -333,7 +327,7 @@ test('same case should be ranked to the top', async () => {
});
test('ignores special keys when searching', async () => {
- render();
+ render();
await type('{shift}');
expect(screen.queryByText(LOADING)).not.toBeInTheDocument();
});
@@ -434,7 +428,7 @@ test('clear all the values', async () => {
});
test('does not add a new option if allowNewOptions is false', async () => {
- render();
+ render();
await open();
await type(NEW_OPTION);
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
@@ -469,7 +463,7 @@ test('adds the null option when selected in multiple mode', async () => {
});
test('renders the select with default props', () => {
- render();
+ render();
expect(getSelect()).toBeInTheDocument();
});
@@ -485,7 +479,7 @@ test('opens the select without any data', async () => {
});
test('displays the loading indicator when opening', async () => {
- render();
+ render();
await waitFor(() => {
userEvent.click(getSelect());
expect(screen.getByText(LOADING)).toBeInTheDocument();
@@ -494,7 +488,7 @@ test('displays the loading indicator when opening', async () => {
});
test('makes a selection in single mode', async () => {
- render();
+ render();
const optionText = 'Emma';
await open();
userEvent.click(await findSelectOption(optionText));
@@ -502,9 +496,7 @@ test('makes a selection in single mode', async () => {
});
test('multiple selections in multiple mode', async () => {
- render(
- ,
- );
+ render();
await open();
const [firstOption, secondOption] = OPTIONS;
userEvent.click(await findSelectOption(firstOption.label));
@@ -516,9 +508,7 @@ test('multiple selections in multiple mode', async () => {
test('changes the selected item in single mode', async () => {
const onChange = jest.fn();
- render(
- ,
- );
+ render();
await open();
const [firstOption, secondOption] = OPTIONS;
userEvent.click(await findSelectOption(firstOption.label));
@@ -542,9 +532,7 @@ test('changes the selected item in single mode', async () => {
});
test('deselects an item in multiple mode', async () => {
- render(
- ,
- );
+ render();
await open();
const option3 = OPTIONS[2];
const option8 = OPTIONS[7];
@@ -578,18 +566,14 @@ test('deselects an item in multiple mode', async () => {
});
test('adds a new option if none is available and allowNewOptions is true', async () => {
- render(
- ,
- );
+ render();
await open();
await type(NEW_OPTION);
expect(await findSelectOption(NEW_OPTION)).toBeInTheDocument();
});
test('does not add a new option if the option already exists', async () => {
- render(
- ,
- );
+ render();
const option = OPTIONS[0].label;
await open();
await type(option);
@@ -602,32 +586,21 @@ test('does not add a new option if the option already exists', async () => {
});
test('shows "No data" when allowNewOptions is false and a new option is entered', async () => {
- render(
- ,
- );
+ render();
await open();
await type(NEW_OPTION);
expect(await screen.findByText(NO_DATA)).toBeInTheDocument();
});
test('does not show "No data" when allowNewOptions is true and a new option is entered', async () => {
- render(
- ,
- );
+ render();
await open();
await type(NEW_OPTION);
expect(screen.queryByText(NO_DATA)).not.toBeInTheDocument();
});
test('sets a initial value in single mode', async () => {
- render(
- ,
- );
+ render();
expect(await findSelectValue()).toHaveTextContent(OPTIONS[0].label);
});
@@ -636,7 +609,6 @@ test('sets a initial value in multiple mode', async () => {
,
);
@@ -646,7 +618,7 @@ test('sets a initial value in multiple mode', async () => {
});
test('searches for matches in both loaded and unloaded pages', async () => {
- render();
+ render();
await open();
await type('and');
@@ -750,6 +722,20 @@ test('fires a new request if all values have not been fetched', async () => {
expect(mock).toHaveBeenCalledTimes(2);
});
+test('does not render a helper text by default', async () => {
+ render();
+ await open();
+ expect(screen.queryByRole('note')).not.toBeInTheDocument();
+});
+
+test('renders a helper text when one is provided', async () => {
+ const helperText = 'Helper text';
+ render();
+ await open();
+ expect(screen.getByRole('note')).toBeInTheDocument();
+ expect(screen.queryByText(helperText)).toBeInTheDocument();
+});
+
/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 643981ac19346..beeb7b262f62e 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -102,6 +102,11 @@ export interface AsyncSelectProps extends PickedSelectProps {
* Can be any ReactNode.
*/
header?: ReactNode;
+ /**
+ * It adds a helper text on top of the Select options
+ * with additional context to help with the interaction.
+ */
+ helperText?: string;
/**
* It fires a request against the server after
* the first interaction and not on render.
@@ -182,6 +187,9 @@ const StyledSelect = styled(AntdSelect)`
.ant-select-arrow .anticon:not(.ant-select-suffix) {
pointer-events: none;
}
+ .ant-select-dropdown {
+ padding: 0;
+ }
`}
`;
@@ -224,6 +232,16 @@ const StyledLoadingText = styled.div`
`}
`;
+const StyledHelperText = styled.div`
+ ${({ theme }) => `
+ padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+ color: ${theme.colors.grayscale.base};
+ font-size: ${theme.typography.sizes.s}px;
+ cursor: default;
+ border-bottom: 1px solid ${theme.colors.grayscale.light2};
+ `}
+`;
+
const MAX_TAG_COUNT = 4;
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
const DEFAULT_PAGE_SIZE = 100;
@@ -297,6 +315,7 @@ const AsyncSelect = forwardRef(
fetchOnlyOnSearch,
filterOption = true,
header = null,
+ helperText,
invertSelection = false,
lazyLoading = true,
loading,
@@ -612,7 +631,16 @@ const AsyncSelect = forwardRef(
if (isLoading && fullSelectOptions.length === 0) {
return {t('Loading...')};
}
- return error ? : originNode;
+ return error ? (
+
+ ) : (
+ <>
+ {helperText && (
+ {helperText}
+ )}
+ {originNode}
+ >
+ );
};
// use a function instead of component since every rerender of the
diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx
index 18111f30ca19e..0ee1f409e423d 100644
--- a/superset-frontend/src/components/Select/Select.test.tsx
+++ b/superset-frontend/src/components/Select/Select.test.tsx
@@ -535,6 +535,20 @@ test('triggers getPopupContainer if passed', async () => {
expect(getPopupContainer).toHaveBeenCalled();
});
+test('does not render a helper text by default', async () => {
+ render();
+ await open();
+ expect(screen.queryByRole('note')).not.toBeInTheDocument();
+});
+
+test('renders a helper text when one is provided', async () => {
+ const helperText = 'Helper text';
+ render();
+ await open();
+ expect(screen.getByRole('note')).toBeInTheDocument();
+ expect(screen.queryByText(helperText)).toBeInTheDocument();
+});
+
/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 80f558d64dd24..e668682b5dde7 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -84,6 +84,11 @@ export interface SelectProps extends PickedSelectProps {
* Can be any ReactNode.
*/
header?: ReactNode;
+ /**
+ * It adds a helper text on top of the Select options
+ * with additional context to help with the interaction.
+ */
+ helperText?: string;
/**
* It defines whether the Select should allow for the
* selection of multiple options or single.
@@ -139,6 +144,9 @@ const StyledSelect = styled(AntdSelect)`
.ant-select-arrow .anticon:not(.ant-select-suffix) {
pointer-events: none;
}
+ .ant-select-dropdown {
+ padding: 0;
+ }
`}
`;
@@ -162,6 +170,16 @@ const StyledLoadingText = styled.div`
`}
`;
+const StyledHelperText = styled.div`
+ ${({ theme }) => `
+ padding: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
+ color: ${theme.colors.grayscale.base};
+ font-size: ${theme.typography.sizes.s}px;
+ cursor: default;
+ border-bottom: 1px solid ${theme.colors.grayscale.light2};
+ `}
+`;
+
const MAX_TAG_COUNT = 4;
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
const EMPTY_OPTIONS: OptionsType = [];
@@ -224,6 +242,7 @@ const Select = forwardRef(
ariaLabel,
filterOption = true,
header = null,
+ helperText,
invertSelection = false,
labelInValue = false,
loading,
@@ -401,7 +420,14 @@ const Select = forwardRef(
if (isLoading && fullSelectOptions.length === 0) {
return {t('Loading...')};
}
- return originNode;
+ return (
+ <>
+ {helperText && (
+ {helperText}
+ )}
+ {originNode}
+ >
+ );
};
// use a function instead of component since every rerender of the