feat: refined ux update a taxonomy by downloading and uploading [FC-0036] (#732)
This PR improves the import tags functionality for existing taxonomies implemented at #675. Co-authored-by: Jillian <jill@opencraft.com> Co-authored-by: Braden MacDonald <mail@bradenm.com>
This commit is contained in:
76
src/generic/loading-button/LoadingButton.test.jsx
Normal file
76
src/generic/loading-button/LoadingButton.test.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import LoadingButton from '.';
|
||||
|
||||
const buttonTitle = 'Button Title';
|
||||
|
||||
const RootWrapper = (onClick) => (
|
||||
<LoadingButton label={buttonTitle} onClick={onClick} />
|
||||
);
|
||||
|
||||
describe('<LoadingButton />', () => {
|
||||
it('renders the title and doesnt handle the spinner initially', () => {
|
||||
const { container, getByText } = render(RootWrapper(() => { }));
|
||||
expect(getByText(buttonTitle)).toBeInTheDocument();
|
||||
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
|
||||
});
|
||||
|
||||
it('doesnt render the spinner without onClick function', () => {
|
||||
const { container, getByRole, getByText } = render(RootWrapper());
|
||||
const titleElement = getByText(buttonTitle);
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
|
||||
fireEvent.click(getByRole('button'));
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
|
||||
});
|
||||
|
||||
it('renders the spinner correctly', async () => {
|
||||
let resolver;
|
||||
const longFunction = () => new Promise((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
const { container, getByRole, getByText } = render(RootWrapper(longFunction));
|
||||
const buttonElement = getByRole('button');
|
||||
fireEvent.click(buttonElement);
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(1);
|
||||
expect(getByText(buttonTitle)).toBeInTheDocument();
|
||||
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
|
||||
// expect(buttonElement).toBeDisabled();
|
||||
expect(buttonElement).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
await act(async () => { resolver(); });
|
||||
|
||||
expect(buttonElement).not.toHaveAttribute('aria-disabled', 'true');
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
|
||||
});
|
||||
|
||||
it('renders the spinner correctly even with error', async () => {
|
||||
let rejecter;
|
||||
const longFunction = () => new Promise((_resolve, reject) => {
|
||||
rejecter = reject;
|
||||
});
|
||||
const { container, getByRole, getByText } = render(RootWrapper(longFunction));
|
||||
const buttonElement = getByRole('button');
|
||||
|
||||
fireEvent.click(buttonElement);
|
||||
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(1);
|
||||
expect(getByText(buttonTitle)).toBeInTheDocument();
|
||||
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
|
||||
// expect(buttonElement).toBeDisabled();
|
||||
expect(buttonElement).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
await act(async () => { rejecter(new Error('error')); });
|
||||
|
||||
// StatefulButton only sets aria-disabled (not disabled) when the state is pending
|
||||
// expect(buttonElement).toBeEnabled();
|
||||
expect(buttonElement).not.toHaveAttribute('aria-disabled', 'true');
|
||||
expect(container.getElementsByClassName('icon-spin').length).toBe(0);
|
||||
});
|
||||
});
|
||||
72
src/generic/loading-button/index.jsx
Normal file
72
src/generic/loading-button/index.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
// @ts-check
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A button that shows a loading spinner when clicked.
|
||||
* @param {object} props
|
||||
* @param {string} props.label
|
||||
* @param {function=} props.onClick
|
||||
* @param {boolean=} props.disabled
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LoadingButton = ({
|
||||
label,
|
||||
onClick,
|
||||
disabled,
|
||||
}) => {
|
||||
const [state, setState] = useState('');
|
||||
// This is used to prevent setting the isLoading state after the component has been unmounted.
|
||||
const componentMounted = useRef(true);
|
||||
|
||||
useEffect(() => () => {
|
||||
componentMounted.current = false;
|
||||
}, []);
|
||||
|
||||
const loadingOnClick = useCallback(async (e) => {
|
||||
if (!onClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState('pending');
|
||||
try {
|
||||
await onClick(e);
|
||||
} catch (err) {
|
||||
// Do nothing
|
||||
} finally {
|
||||
if (componentMounted.current) {
|
||||
setState('');
|
||||
}
|
||||
}
|
||||
}, [componentMounted, onClick]);
|
||||
|
||||
return (
|
||||
<StatefulButton
|
||||
disabledStates={disabled ? [state] : ['pending'] /* StatefulButton doesn't support disabled prop */}
|
||||
onClick={loadingOnClick}
|
||||
labels={{ default: label }}
|
||||
state={state}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LoadingButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
LoadingButton.defaultProps = {
|
||||
onClick: undefined,
|
||||
disabled: undefined,
|
||||
};
|
||||
|
||||
export default LoadingButton;
|
||||
Reference in New Issue
Block a user