1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: banner UI/UX adjustments (#5151)

https://linear.app/unleash/issue/2-1549/ui-align-with-uiux

Includes UI/UX adjustments to the banners feature after aligning with
@nicolaesocaciu

There are a lot of changes, but here are a few:
 - Redesigned preview section
 - Redesigned banner status (enabled) section
 - Reordered form fields to better fit the flow
 - Reordered fields in the side-panel payload to reflect order in the UI
 - Made inputs full width
 - Adjusted multiline fields
 - Added a link to Markdown's basic syntax examples
 - Added a "preview dialog" button
 - Updated `HelpIcon` usage to use the `htmlTooltip`
- Improved `Banner` inline design, added a maxHeight prop for usage
inside a table
 - Improved `FormSwitch` design


![image](https://github.com/Unleash/unleash/assets/14320932/d8fe1f59-85ed-48eb-aa46-62628b12f0b1)

Co-authored-by: Nicolae <nicolae@getunleash.ai>
This commit is contained in:
Nuno Góis 2023-10-25 17:14:18 +01:00 committed by GitHub
parent 8e3863a27e
commit cc34db1659
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 256 additions and 149 deletions

View File

@ -1,4 +1,4 @@
import { styled } from '@mui/material';
import { Button, Checkbox, FormControlLabel, styled } from '@mui/material';
import { Banner } from 'component/banners/Banner/Banner';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
@ -7,6 +7,8 @@ import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import Input from 'component/common/Input/Input';
import { BannerVariant } from 'interfaces/banner';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { Visibility } from '@mui/icons-material';
import { BannerDialog } from 'component/banners/Banner/BannerDialog/BannerDialog';
const StyledForm = styled('form')(({ theme }) => ({
display: 'flex',
@ -14,6 +16,37 @@ const StyledForm = styled('form')(({ theme }) => ({
gap: theme.spacing(4),
}));
const StyledBannerPreview = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: theme.spacing(1.5),
gap: theme.spacing(1.5),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledBannerPreviewDescription = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));
const StyledRaisedSection = styled('div')(({ theme }) => ({
background: theme.palette.background.elevation1,
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadiusLarge,
}));
const StyledSection = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1.5),
}));
const StyledSectionLabel = styled('p')(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledFieldGroup = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
@ -25,10 +58,9 @@ const StyledInputDescription = styled('p')(({ theme }) => ({
color: theme.palette.text.primary,
}));
const StyledInput = styled(Input)(({ theme }) => ({
const StyledInput = styled(Input)({
width: '100%',
maxWidth: theme.spacing(50),
}));
});
const StyledTooltip = styled('div')(({ theme }) => ({
display: 'flex',
@ -42,6 +74,10 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
maxWidth: theme.spacing(50),
}));
const StyledPreviewButton = styled(Button)(({ theme }) => ({
marginRight: 'auto',
}));
const VARIANT_OPTIONS = [
{ key: 'info', label: 'Information' },
{ key: 'warning', label: 'Warning' },
@ -93,6 +129,8 @@ export const BannerForm = ({
setDialogTitle,
setDialog,
}: IBannerFormProps) => {
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [iconOption, setIconOption] = useState<IconOption>(
icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom',
);
@ -102,8 +140,10 @@ export const BannerForm = ({
return (
<StyledForm>
<StyledFieldGroup>
<StyledInputDescription>Preview:</StyledInputDescription>
<StyledBannerPreview>
<StyledBannerPreviewDescription>
Banner preview:
</StyledBannerPreviewDescription>
<Banner
banner={{
message:
@ -119,67 +159,58 @@ export const BannerForm = ({
}}
inline
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What is your banner message?
<HelpIcon
tooltip={
<StyledTooltip>
<p>Markdown is supported.</p>
</StyledTooltip>
</StyledBannerPreview>
<StyledRaisedSection>
<FormSwitch checked={enabled} setChecked={setEnabled}>
Banner status
</FormSwitch>
</StyledRaisedSection>
<StyledSection>
<StyledSectionLabel>Configuration</StyledSectionLabel>
<StyledFieldGroup>
<StyledInputDescription>
What type of banner is it?
</StyledInputDescription>
<StyledSelect
size='small'
value={variant}
onChange={(variant) =>
setVariant(variant as BannerVariant)
}
options={VARIANT_OPTIONS}
/>
</StyledInputDescription>
<StyledInput
autoFocus
label='Banner message'
value={message}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setMessage(e.target.value)
}
autoComplete='off'
required
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What type of banner is it?
</StyledInputDescription>
<StyledSelect
size='small'
value={variant}
onChange={(variant) => setVariant(variant as BannerVariant)}
options={VARIANT_OPTIONS}
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What icon should be displayed on the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={iconOption}
onChange={(iconOption) => {
setIconOption(iconOption as IconOption);
if (iconOption === 'None') {
setIcon('none');
} else {
setIcon('');
}
}}
options={['Default', 'Custom', 'None'].map((option) => ({
key: option,
label: option,
}))}
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What icon should be displayed on the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={iconOption}
onChange={(iconOption) => {
setIconOption(iconOption as IconOption);
if (iconOption === 'None') {
setIcon('none');
} else {
setIcon('');
}
}}
options={['Default', 'Custom', 'None'].map(
(option) => ({
key: option,
label: option,
}),
)}
/>
</StyledFieldGroup>
<ConditionallyRender
condition={iconOption === 'Custom'}
show={
<>
<StyledFieldGroup>
<StyledInputDescription>
What custom icon should be displayed?
Which custom icon?
<HelpIcon
htmlTooltip
tooltip={
<StyledTooltip>
<p>
@ -212,34 +243,75 @@ export const BannerForm = ({
}
autoComplete='off'
/>
</>
</StyledFieldGroup>
}
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What action should be available in the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={linkOption}
onChange={(linkOption) => {
setLinkOption(linkOption as LinkOption);
if (linkOption === 'Dialog') {
setLink('dialog');
} else {
setLink('');
<StyledFieldGroup>
<StyledInputDescription>
What is your banner message?
<HelpIcon
htmlTooltip
tooltip={
<StyledTooltip>
<p>
<a
href='https://www.markdownguide.org/basic-syntax/'
target='_blank'
rel='noreferrer'
>
Markdown
</a>{' '}
is supported.
</p>
</StyledTooltip>
}
/>
</StyledInputDescription>
<StyledInput
autoFocus
label='Banner message'
multiline
minRows={2}
maxRows={6}
value={message}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setMessage(e.target.value)
}
}}
options={['None', 'Link', 'Dialog'].map((option) => ({
key: option,
label: option,
}))}
/>
autoComplete='off'
required
/>
</StyledFieldGroup>
</StyledSection>
<StyledSection>
<StyledSectionLabel>Banner action</StyledSectionLabel>
<StyledFieldGroup>
<StyledInputDescription>
What action should be available in the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={linkOption}
onChange={(linkOption) => {
setLinkOption(linkOption as LinkOption);
if (linkOption === 'Dialog') {
setLink('dialog');
} else {
setLink('');
}
setLinkText('');
setDialogTitle('');
setDialog('');
}}
options={['None', 'Link', 'Dialog'].map((option) => ({
key: option,
label: option,
}))}
/>
</StyledFieldGroup>
<ConditionallyRender
condition={linkOption === 'Link'}
show={
<>
<StyledFieldGroup>
<StyledInputDescription>
What URL should be opened?
</StyledInputDescription>
@ -254,13 +326,13 @@ export const BannerForm = ({
}}
autoComplete='off'
/>
</>
</StyledFieldGroup>
}
/>
<ConditionallyRender
condition={linkOption !== 'None'}
show={
<>
<StyledFieldGroup>
<StyledInputDescription>
What is the action text?
</StyledInputDescription>
@ -272,72 +344,89 @@ export const BannerForm = ({
}
autoComplete='off'
/>
</>
</StyledFieldGroup>
}
/>
<ConditionallyRender
condition={linkOption === 'Dialog'}
show={
<>
<StyledInputDescription>
What is the dialog title?
</StyledInputDescription>
<StyledInput
label='Dialog title'
value={dialogTitle}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setDialogTitle(e.target.value)
}
autoComplete='off'
/>
<StyledInputDescription>
What is the dialog content?
<HelpIcon
tooltip={
<StyledTooltip>
<p>Markdown is supported.</p>
</StyledTooltip>
}
<StyledFieldGroup>
<StyledInputDescription>
What is the dialog title?
</StyledInputDescription>
<StyledInput
label='Dialog title'
value={dialogTitle}
onChange={(
e: ChangeEvent<HTMLInputElement>,
) => setDialogTitle(e.target.value)}
autoComplete='off'
/>
</StyledInputDescription>
<StyledInput
label='Dialog content'
multiline
minRows={4}
value={dialog}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setDialog(e.target.value)
}
autoComplete='off'
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
What is the dialog content?
<HelpIcon
htmlTooltip
tooltip={
<StyledTooltip>
<p>
<a
href='https://www.markdownguide.org/basic-syntax/'
target='_blank'
rel='noreferrer'
>
Markdown
</a>{' '}
is supported.
</p>
</StyledTooltip>
}
/>
</StyledInputDescription>
<StyledInput
label='Dialog content'
multiline
minRows={4}
value={dialog}
onChange={(
e: ChangeEvent<HTMLInputElement>,
) => setDialog(e.target.value)}
autoComplete='off'
/>
</StyledFieldGroup>
<StyledPreviewButton
variant='outlined'
color='primary'
startIcon={<Visibility />}
onClick={() => setPreviewDialogOpen(true)}
>
Preview dialog
</StyledPreviewButton>
<BannerDialog
open={previewDialogOpen}
setOpen={setPreviewDialogOpen}
title={dialogTitle || linkText}
>
{dialog!}
</BannerDialog>
</>
}
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
Is the banner sticky on the screen when scrolling?
</StyledInputDescription>
<FormSwitch
checked={sticky}
setChecked={setSticky}
sx={{
justifyContent: 'start',
}}
</StyledSection>
<StyledSection>
<StyledSectionLabel>Sticky banner</StyledSectionLabel>
<FormControlLabel
control={
<Checkbox
checked={sticky}
onChange={(e) => setSticky(e.target.checked)}
/>
}
label='Make the banner sticky on the screen when scrolling'
/>
</StyledFieldGroup>
<StyledFieldGroup>
<StyledInputDescription>
Is the banner currently visible to all users?
</StyledInputDescription>
<FormSwitch
checked={enabled}
setChecked={setEnabled}
sx={{
justifyContent: 'start',
}}
/>
</StyledFieldGroup>
</StyledSection>
</StyledForm>
);
};

View File

@ -69,15 +69,15 @@ export const BannerModal = ({ banner, open, setOpen }: IBannerModalProps) => {
const isValid = message.length;
const payload: AddOrUpdateBanner = {
message,
enabled,
variant,
icon,
message,
link,
linkText,
dialogTitle,
dialog,
sticky,
enabled,
};
const formatApiCode = () => {

View File

@ -75,7 +75,11 @@ export const BannersTable = () => {
Header: 'Banner',
accessor: 'message',
Cell: ({ row: { original: banner } }: any) => (
<Banner banner={{ ...banner, sticky: false }} inline />
<Banner
banner={{ ...banner, sticky: false }}
inline
maxHeight={42}
/>
),
disableSortBy: true,
minWidth: 200,

View File

@ -14,21 +14,28 @@ import { BannerVariant, IBanner } from 'interfaces/banner';
import { Sticky } from 'component/common/Sticky/Sticky';
const StyledBar = styled('aside', {
shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'inline',
})<{ variant: BannerVariant; inline?: boolean }>(
({ theme, variant, inline }) => ({
shouldForwardProp: (prop) =>
prop !== 'variant' && prop !== 'inline' && prop !== 'maxHeight',
})<{ variant: BannerVariant; inline?: boolean; maxHeight?: number }>(
({ theme, variant, inline, maxHeight }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(1),
gap: theme.spacing(1),
width: '100%',
...(inline
? {
border: '1px solid',
borderRadius: theme.shape.borderRadiusMedium,
}
: {
borderBottom: '1px solid',
}),
...(maxHeight && {
maxHeight: maxHeight,
overflow: 'auto',
}),
borderColor: theme.palette[variant].border,
background: theme.palette[variant].light,
color: theme.palette[variant].dark,
@ -47,9 +54,10 @@ const StyledIcon = styled('div', {
interface IBannerProps {
banner: IBanner;
inline?: boolean;
maxHeight?: number;
}
export const Banner = ({ banner, inline }: IBannerProps) => {
export const Banner = ({ banner, inline, maxHeight }: IBannerProps) => {
const [open, setOpen] = useState(false);
const {
@ -65,7 +73,7 @@ export const Banner = ({ banner, inline }: IBannerProps) => {
} = banner;
const bannerBar = (
<StyledBar variant={variant} inline={inline}>
<StyledBar variant={variant} inline={inline} maxHeight={maxHeight}>
<StyledIcon variant={variant}>
<BannerIcon icon={icon} variant={variant} />
</StyledIcon>

View File

@ -1,15 +1,21 @@
import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material';
import { Dispatch, ReactNode, SetStateAction } from 'react';
const StyledContainer = styled(Box)({
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
lineHeight: theme.spacing(2.75),
}));
const StyledControlLabel = styled(FormControlLabel)({
marginRight: 0,
});
const StyledSwitchSpan = styled('span')(({ theme }) => ({
marginLeft: theme.spacing(0.5),
fontSize: theme.fontSizes.smallBody,
}));
interface IFormSwitchProps extends BoxProps {
@ -27,7 +33,7 @@ export const FormSwitch = ({
return (
<StyledContainer {...props}>
{children}
<FormControlLabel
<StyledControlLabel
control={
<Switch
checked={checked}