style: new toast design closer to SIP-34 (#10178)

This commit is contained in:
Lily Kuang 2020-07-10 14:58:59 -07:00 committed by GitHub
parent 80b06f6827
commit 3b4df51848
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 286 additions and 257 deletions

View File

@ -0,0 +1,22 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.34784 20.9464 6.8043 19.0711 4.92893C17.1957 3.05357 14.6522 2 12 2Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.7191 8.79L10.4291 13.09L8.77911 11.44C8.53472 11.1546 8.15099 11.0303 7.78569 11.1182C7.42039 11.2061 7.13517 11.4913 7.0473 11.8566C6.95942 12.2219 7.08373 12.6056 7.36911 12.85L9.71911 15.21C9.90783 15.3972 10.1633 15.5015 10.4291 15.5C10.6914 15.4989 10.9428 15.3947 11.1291 15.21L16.1291 10.21C16.3184 10.0222 16.4249 9.76664 16.4249 9.5C16.4249 9.23336 16.3184 8.97777 16.1291 8.79C15.7391 8.40228 15.1091 8.40228 14.7191 8.79Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,22 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
-->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path id="Shape" fill-rule="evenodd" clip-rule="evenodd" d="M21.71 7.56L16.44 2.29C16.2484 2.10727 15.9948 2.00368 15.73 2H8.27C8.00523 2.00368 7.75163 2.10727 7.56 2.29L2.29 7.56C2.10727 7.75163 2.00368 8.00523 2 8.27V15.73C2.00368 15.9948 2.10727 16.2484 2.29 16.44L7.56 21.71C7.75163 21.8927 8.00523 21.9963 8.27 22H15.73C15.9948 21.9963 16.2484 21.8927 16.44 21.71L21.71 16.44C21.8927 16.2484 21.9963 15.9948 22 15.73V8.27C21.9963 8.00523 21.8927 7.75163 21.71 7.56Z" fill="currentColor"/>
<path id="Combined Shape" fill-rule="evenodd" clip-rule="evenodd" d="M11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8V12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12V8ZM11 16C11 15.4477 11.4477 15 12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -22,6 +22,7 @@ import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import { ParentSize } from '@vx/responsive';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import { Sticky, StickyContainer } from 'react-sticky';
import { TabContainer, TabContent, TabPane } from 'react-bootstrap';
@ -77,6 +78,10 @@ describe('DashboardBuilder', () => {
<Provider store={store}>
<WithDragDropContext>{builder}</WithDragDropContext>
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
},
)
: shallow(builder);
}

View File

@ -34,9 +34,9 @@ describe('ToastPresenter', () => {
return wrapper;
}
it('should render a div with class toast-presenter', () => {
it('should render a div with id toast-presenter', () => {
const wrapper = setup();
expect(wrapper.find('.toast-presenter')).toHaveLength(1);
expect(wrapper.find('#toast-presenter')).toHaveLength(1);
});
it('should render a Toast for each toast object', () => {

View File

@ -19,21 +19,19 @@
import { Alert } from 'react-bootstrap';
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import Toast from 'src/messageToasts/components/Toast';
import mockMessageToasts from '../mockMessageToasts';
const props = {
toast: mockMessageToasts[0],
onCloseToast() {},
};
const setup = overrideProps => mount(<Toast {...props} {...overrideProps} />);
describe('Toast', () => {
const props = {
toast: mockMessageToasts[0],
onCloseToast() {},
};
function setup(overrideProps) {
const wrapper = mount(<Toast {...props} {...overrideProps} />);
return wrapper;
}
it('should render an Alert', () => {
const wrapper = setup();
expect(wrapper.find(Alert)).toHaveLength(1);
@ -52,9 +50,13 @@ describe('Toast', () => {
expect(id).toBe(props.toast.id);
done();
};
const wrapper = setup({ onCloseToast });
const handleClosePress = wrapper.instance().handleClosePress;
expect(wrapper.find(Alert).prop('onDismiss')).toBe(handleClosePress);
const handleClosePress = wrapper.find('[label="Close alert"]').props()
.onClick;
const alertProps = wrapper.find(Alert).props();
expect(alertProps.onDismiss).toBe(handleClosePress);
handleClosePress(); // there is a timeout for onCloseToast to be called
});
});

View File

@ -23,6 +23,7 @@ import URI from 'urijs';
import { Tab } from 'react-bootstrap';
import { shallow, mount } from 'enzyme';
import sinon from 'sinon';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
@ -98,6 +99,8 @@ describe('TabbedSqlEditors', () => {
uriStub.returns({ id: 1 });
wrapper = mount(<TabbedSqlEditors {...mockedProps} />, {
context: { store },
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
});
expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe(
true,
@ -110,6 +113,8 @@ describe('TabbedSqlEditors', () => {
uriStub.returns({ savedQueryId: 1 });
wrapper = mount(<TabbedSqlEditors {...mockedProps} />, {
context: { store },
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
});
expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe(
true,
@ -122,6 +127,8 @@ describe('TabbedSqlEditors', () => {
uriStub.returns({ sql: 1, dbid: 1 });
wrapper = mount(<TabbedSqlEditors {...mockedProps} />, {
context: { store },
wrappingComponent: ThemeProvider,
wrappingComponentProps: { theme: supersetTheme },
});
expect(TabbedSqlEditors.prototype.componentDidMount.calledOnce).toBe(
true,

View File

@ -19,12 +19,14 @@
import React, { SVGProps } from 'react';
import styled from '@superset-ui/style';
import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
import { ReactComponent as CheckboxHalfIcon } from 'images/icons/checkbox-half.svg';
import { ReactComponent as CheckboxOffIcon } from 'images/icons/checkbox-off.svg';
import { ReactComponent as CheckboxOnIcon } from 'images/icons/checkbox-on.svg';
import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
import { ReactComponent as DatasetPhysicalIcon } from 'images/icons/dataset_physical.svg';
import { ReactComponent as DatasetVirtualIcon } from 'images/icons/dataset_virtual.svg';
import { ReactComponent as ErrorIcon } from 'images/icons/error.svg';
import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg';
@ -35,12 +37,14 @@ import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
type Icon =
| 'cancel-x'
| 'check'
| 'checkbox-half'
| 'checkbox-off'
| 'checkbox-on'
| 'compass'
| 'dataset-physical'
| 'dataset-virtual'
| 'error'
| 'pencil'
| 'search'
| 'sort'
@ -58,7 +62,9 @@ const iconsRegistry: { [key in Icon]: React.ComponentType } = {
'dataset-virtual': DatasetVirtualIcon,
'sort-asc': SortAscIcon,
'sort-desc': SortDescIcon,
check: CheckIcon,
compass: CompassIcon,
error: ErrorIcon,
pencil: PencilIcon,
search: SearchIcon,
sort: SortIcon,

View File

@ -1,109 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { Alert } from 'react-bootstrap';
import cx from 'classnames';
import Interweave from 'interweave';
import PropTypes from 'prop-types';
import React from 'react';
import { toastShape } from '../propShapes';
import {
INFO_TOAST,
SUCCESS_TOAST,
WARNING_TOAST,
DANGER_TOAST,
} from '../constants';
const propTypes = {
toast: toastShape.isRequired,
onCloseToast: PropTypes.func.isRequired,
};
const defaultProps = {};
class Toast extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
};
this.showToast = this.showToast.bind(this);
this.handleClosePress = this.handleClosePress.bind(this);
}
componentDidMount() {
const { toast } = this.props;
setTimeout(this.showToast);
if (toast.duration > 0) {
this.hideTimer = setTimeout(this.handleClosePress, toast.duration);
}
}
componentWillUnmount() {
clearTimeout(this.hideTimer);
}
showToast() {
this.setState({ visible: true });
}
handleClosePress() {
clearTimeout(this.hideTimer);
this.setState({ visible: false }, () => {
// Wait for the transition
setTimeout(() => {
this.props.onCloseToast(this.props.toast.id);
}, 150);
});
}
render() {
const { visible } = this.state;
const {
toast: { toastType, text },
} = this.props;
return (
<Alert
onDismiss={this.handleClosePress}
bsClass={cx(
'alert',
'toast',
visible && 'toast--visible',
toastType === INFO_TOAST && 'toast--info',
toastType === SUCCESS_TOAST && 'toast--success',
toastType === WARNING_TOAST && 'toast--warning',
toastType === DANGER_TOAST && 'toast--danger',
)}
>
<Interweave content={text} />
</Alert>
);
}
}
Toast.propTypes = propTypes;
Toast.defaultProps = defaultProps;
export default Toast;

View File

@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 { Alert } from 'react-bootstrap';
import styled from '@superset-ui/style';
import cx from 'classnames';
import Interweave from 'interweave';
import React, { useEffect, useState } from 'react';
import Icon from 'src/components/Icon';
import { ToastType } from 'src/messageToasts/types';
import { SUCCESS_TOAST, WARNING_TOAST, DANGER_TOAST } from '../constants';
const ToastContianer = styled.div`
display: flex;
justify-content: center;
align-items: center;
span {
padding: 0 11px;
}
`;
interface ToastPresenterProps {
toast: { id: string; toastType: ToastType; text: string; duration: number };
onCloseToast: (id: string) => void;
}
export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
let hideTimer: ReturnType<typeof setTimeout>;
const [visible, setVisible] = useState(false);
const showToast = () => {
setVisible(true);
};
const handleClosePress = () => {
clearTimeout(hideTimer);
// Wait for the transition
setVisible(() => {
setTimeout(() => {
onCloseToast(toast.id);
}, 150);
return false;
});
};
useEffect(() => {
setTimeout(showToast);
if (toast.duration > 0) {
hideTimer = setTimeout(handleClosePress, toast.duration);
}
return () => {
clearTimeout(hideTimer);
};
}, []);
return (
<Alert
onDismiss={handleClosePress}
bsClass={cx(
'alert',
'toast',
visible && 'toast--visible',
toast.toastType === SUCCESS_TOAST && 'toast--success',
toast.toastType === WARNING_TOAST && 'toast--warning',
toast.toastType === DANGER_TOAST && 'toast--danger',
)}
>
<ToastContianer>
{toast.toastType === SUCCESS_TOAST && <Icon name="check" />}
{toast.toastType === WARNING_TOAST ||
(toast.toastType === DANGER_TOAST && <Icon name="error" />)}
<Interweave content={toast.text} />
</ToastContianer>
</Alert>
);
}

View File

@ -1,56 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 PropTypes from 'prop-types';
import React from 'react';
import Toast from './Toast';
import { toastShape } from '../propShapes';
import '../stylesheets/toast.less';
const propTypes = {
toasts: PropTypes.arrayOf(toastShape),
removeToast: PropTypes.func.isRequired,
};
const defaultProps = {
toasts: [],
};
// eslint-disable-next-line react/prefer-stateless-function
class ToastPresenter extends React.Component {
render() {
const { toasts, removeToast } = this.props;
return (
toasts.length > 0 && (
<div className="toast-presenter">
{toasts.map(toast => (
<Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
))}
</div>
)
);
}
}
ToastPresenter.propTypes = propTypes;
ToastPresenter.defaultProps = defaultProps;
export default ToastPresenter;

View File

@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 styled from '@superset-ui/style';
import { ToastType } from 'src/messageToasts/types';
import Toast from './Toast';
const StyledToastPresenter = styled.div`
max-width: 600px;
position: fixed;
bottom: 0px;
right: -110px;
transform: translate(-50%, 0);
z-index: ${({ theme }) => theme.zIndex.max};
.toast {
background: ${({ theme }) => theme.colors.grayscale.dark1};
border-radius: ${({ theme }) => theme.borderRadius};
box-shadow: 0 2px 4px 0
fade(
${({ theme }) => theme.colors.grayscale.dark2},
${({ theme }) => theme.opacity.mediumLight}
);
color: ${({ theme }) => theme.colors.grayscale.light5};
opacity: 0;
position: relative;
transform: translateY(-100%);
white-space: pre-line;
will-change: transform, opacity;
transition: transform ${({ theme }) => theme.transitionTiming}s,
opacity ${({ theme }) => theme.transitionTiming}s;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
}
}
.toast > button {
color: ${({ theme }) => theme.colors.grayscale.light5};
opacity: 1;
}
.toast--visible {
opacity: 1;
transform: translateY(0);
}
`;
type ToastShape = {
id: string;
toastType: ToastType;
text: string;
duration: number;
};
interface ToastPresenterProps {
toasts: Array<ToastShape>;
removeToast: () => void;
}
const ToastPresenter = ({ toasts, removeToast }: ToastPresenterProps) =>
toasts.length > 0 && (
<StyledToastPresenter id="toast-presenter">
{toasts.map(toast => (
<Toast key={toast.id} toast={toast} onCloseToast={removeToast} />
))}
</StyledToastPresenter>
);
export default ToastPresenter;

View File

@ -1,78 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 '../../../stylesheets/less/variables.less';
.toast-presenter {
position: fixed;
bottom: 16px;
left: 50%;
transform: translate(-50%, 0);
width: 600px;
z-index: @z-index-max;
}
.toast {
background: @lightest;
color: @almost-black;
opacity: 0;
position: relative;
white-space: pre-line;
box-shadow: 0 2px 4px 0 fade(@darkest, @opacity-medium-light);
will-change: transform, opacity;
transform: translateY(-100%);
transition: transform @timing-normal, opacity @timing-normal;
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
}
}
.toast > button {
color: @almost-black;
&:hover {
color: @gray-dark;
}
}
.toast--visible {
transform: translateY(0);
opacity: 1;
}
.toast--info:after {
background: @info;
}
.toast--success:after {
background: @success;
}
.toast--warning:after {
background: @warning;
}
.toast--danger:after {
background: @danger;
}

View File

@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 type ToastType =
| 'INFO_TOAST'
| 'SUCCESS_TOAST'
| 'WARNING_TOAST'
| 'DANGER_TOAST';