1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-27 01:19:00 +02:00

chore: second design pass for editable constraints (#9843)

Fix a number of visual issues with the main editable constraint
component.

I've introduced a few more layers of container nesting to make the
layout break the right ways:
- Put everything on the same line when on wide.
- At 700px place selected values on the row below
- From 700px down, when necessary, also wrap operator options


Support super long context names without breaking layout
<img width="399" alt="image"
src="https://github.com/user-attachments/assets/07555e9c-d875-417f-ae6b-d4600731d5eb"
/>

Wrap values at 700px width container:
<img width="703" alt="image"
src="https://github.com/user-attachments/assets/deb6e059-57d4-4e47-88da-3ec5d6bce751"
/>

Wrap operator options when necessary
<img width="359" alt="image"
src="https://github.com/user-attachments/assets/ff96db40-f47d-4ddf-bed7-dfced4d69973"
/>

Absolutely position delete button to allow to not push it out of the
container on narrow screens:
<img width="330" alt="image"
src="https://github.com/user-attachments/assets/c7b8f88d-538a-46a1-ae3f-e5a761b50289"
/>

Remove extra focus styling from MUI (darken select background):
Before:
<img width="348" alt="image"
src="https://github.com/user-attachments/assets/99aff08d-c1af-46c0-8a75-40c1ea3c103f"
/>

<img width="357" alt="image"
src="https://github.com/user-attachments/assets/b7a0edac-2716-48a7-b50c-b3437e5f5be8"
/>

After:
<img width="379" alt="image"
src="https://github.com/user-attachments/assets/74da884c-7b1a-4b9a-8383-31592326a71b"
/>
<img width="350" alt="image"
src="https://github.com/user-attachments/assets/0ebea696-5f7d-4d4e-b91c-b087a8fc56a3"
/>
This commit is contained in:
Thomas Heartman 2025-04-25 12:37:04 +02:00 committed by GitHub
parent 44e9023fb3
commit 44082b24a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 105 additions and 49 deletions

View File

@ -47,6 +47,9 @@ const StyledSelect = styled(Select)(({ theme }) => ({
'.MuiInput-input': { '.MuiInput-input': {
paddingBlock: theme.spacing(0.25), paddingBlock: theme.spacing(0.25),
}, },
':focus-within .MuiSelect-select': {
background: 'none',
},
})); }));
const StyledMenuItem = styled(MenuItem, { const StyledMenuItem = styled(MenuItem, {

View File

@ -32,22 +32,44 @@ const Container = styled('article')(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusLarge, borderRadius: theme.shape.borderRadiusLarge,
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
containerType: 'inline-size',
})); }));
const onNarrowContainer = '@container (max-width: 700px)';
const TopRow = styled('div')(({ theme }) => ({ const TopRow = styled('div')(({ theme }) => ({
'--gap': theme.spacing(1),
padding: 'var(--padding)', padding: 'var(--padding)',
display: 'flex', display: 'flex',
flexFlow: 'row nowrap', flexFlow: 'row nowrap',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyItems: 'space-between', justifyItems: 'space-between',
gap: 'var(--gap)',
}));
const ConstraintOptions = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row nowrap',
gap: 'var(--gap)',
alignSelf: 'flex-start',
[onNarrowContainer]: {
flexFlow: 'row wrap',
},
}));
const OperatorOptions = styled(ConstraintOptions)(({ theme }) => ({
flexFlow: 'row nowrap',
})); }));
const ConstraintDetails = styled('div')(({ theme }) => ({ const ConstraintDetails = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
gap: theme.spacing(1), gap: 'var(--gap)',
flexFlow: 'row nowrap', flexFlow: 'row nowrap',
width: '100%', width: '100%',
height: 'min-content', height: 'min-content',
[onNarrowContainer]: {
flexDirection: 'column',
},
})); }));
const InputContainer = styled('div')(({ theme }) => ({ const InputContainer = styled('div')(({ theme }) => ({
@ -57,6 +79,10 @@ const InputContainer = styled('div')(({ theme }) => ({
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
fieldset: { border: 'none', borderRadius: 0 }, fieldset: { border: 'none', borderRadius: 0 },
maxWidth: '25ch',
':focus-within .MuiSelect-select': {
background: 'none',
},
':focus-within fieldset': { borderBottomStyle: 'solid' }, ':focus-within fieldset': { borderBottomStyle: 'solid' },
'label + &': { 'label + &': {
// mui adds a margin top to 'standard' selects with labels // mui adds a margin top to 'standard' selects with labels
@ -67,10 +93,19 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
}, },
})); }));
const StyledIconButton = styled(IconButton)(({ theme }) => ({
position: 'absolute',
right: theme.spacing(1),
}));
const StyledButton = styled('button')(({ theme }) => ({ const StyledButton = styled('button')(({ theme }) => ({
// todo (`addEditStrategy`): this is pretty rough, but it needs to be the
// same height as the input fields, which are 27.25 px at the moment.
// Consider editing this when we get new icons for the buttons. There may be
// a better solution.
height: `calc(${theme.typography.body1.fontSize} + ${theme.spacing(1.5)})`,
width: '5ch', width: '5ch',
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.25, 0),
fontSize: theme.fontSizes.smallerBody, fontSize: theme.fontSizes.smallerBody,
background: theme.palette.secondary.light, background: theme.palette.secondary.light,
border: `1px solid ${theme.palette.secondary.border}`, border: `1px solid ${theme.palette.secondary.border}`,
@ -82,6 +117,14 @@ const StyledButton = styled('button')(({ theme }) => ({
}, },
})); }));
const ButtonPlaceholder = styled('div')(({ theme }) => ({
// this is a trick that lets us use absolute positioning for the button so
// that it can go over the operator context fields when necessary (narrow
// screens), but still retain necessary space for the button when it's all
// on one line.
width: theme.spacing(2),
}));
const StyledCaseInsensitiveIcon = styled(CaseInsensitiveIcon)(({ theme }) => ({ const StyledCaseInsensitiveIcon = styled(CaseInsensitiveIcon)(({ theme }) => ({
path: { path: {
fill: theme.palette.text.disabled, fill: theme.palette.text.disabled,
@ -218,52 +261,55 @@ export const EditableConstraint: FC<Props> = ({
<Container> <Container>
<TopRow> <TopRow>
<ConstraintDetails> <ConstraintDetails>
<StyledSelect <ConstraintOptions>
visuallyHideLabel <StyledSelect
id='context-field-select' visuallyHideLabel
name='contextName' id='context-field-select'
label='Context Field' name='contextName'
autoFocus label='Context Field'
options={constraintNameOptions} autoFocus
value={contextName || ''} options={constraintNameOptions}
onChange={setContextName} value={contextName || ''}
variant='standard' onChange={setContextName}
/> variant='standard'
/>
<StyledButton <OperatorOptions>
type='button' <StyledButton
onClick={toggleInvertedOperator} type='button'
> onClick={toggleInvertedOperator}
{localConstraint.inverted ? 'aint' : 'is'} >
</StyledButton> {localConstraint.inverted ? 'aint' : 'is'}
</StyledButton>
<ConstraintOperatorSelect <ConstraintOperatorSelect
options={operatorsForContext(contextName)} options={operatorsForContext(contextName)}
value={operator} value={operator}
onChange={onOperatorChange} onChange={onOperatorChange}
inverted={localConstraint.inverted} inverted={localConstraint.inverted}
/> />
{showCaseSensitiveButton ? (
<CaseButton
type='button'
onClick={toggleCaseSensitivity}
>
{localConstraint.caseInsensitive ? (
<StyledCaseInsensitiveIcon aria-label='The match is not case sensitive.' />
) : (
<StyledCaseSensitiveIcon aria-label='The match is case sensitive.' />
)}
<ScreenReaderOnly>
Make match
{localConstraint.caseInsensitive
? ' '
: ' not '}
case sensitive
</ScreenReaderOnly>
</CaseButton>
) : null}
{showCaseSensitiveButton ? (
<CaseButton
type='button'
onClick={toggleCaseSensitivity}
>
{localConstraint.caseInsensitive ? (
<StyledCaseInsensitiveIcon aria-label='The match is not case sensitive.' />
) : (
<StyledCaseSensitiveIcon aria-label='The match is case sensitive.' />
)}
<ScreenReaderOnly>
Make match
{localConstraint.caseInsensitive
? ' '
: ' not '}
case sensitive
</ScreenReaderOnly>
</CaseButton>
) : null}
</OperatorOptions>
</ConstraintOptions>
<ValueList <ValueList
values={localConstraint.values} values={localConstraint.values}
removeValue={removeValue} removeValue={removeValue}
@ -290,16 +336,16 @@ export const EditableConstraint: FC<Props> = ({
) : null} ) : null}
</ValueList> </ValueList>
</ConstraintDetails> </ConstraintDetails>
<ButtonPlaceholder />
<HtmlTooltip title='Delete constraint' arrow> <HtmlTooltip title='Delete constraint' arrow>
<IconButton <StyledIconButton
type='button' type='button'
size='small' size='small'
onClick={onDelete} onClick={onDelete}
ref={deleteButtonRef} ref={deleteButtonRef}
> >
<Delete /> <Delete fontSize='inherit' />
</IconButton> </StyledIconButton>
</HtmlTooltip> </HtmlTooltip>
</TopRow> </TopRow>
{showInputField ? ( {showInputField ? (

View File

@ -94,6 +94,13 @@ export const ValueList: FC<PropsWithChildren<Props>> = ({
ref={(el) => { ref={(el) => {
constraintElementRefs.current[index] = el; constraintElementRefs.current[index] = el;
}} }}
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal',
},
}}
deleteIcon={<Clear />} deleteIcon={<Clear />}
label={value} label={value}
onDelete={() => { onDelete={() => {