1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00

chore(1-3380): handle narrow windows for the flag header. (#9321)

Makes it so that the actions/tabs wrap on narrow width screens.

Constraints:
- When wrapping, the actions should go **before** the tabs, when not
wrapping, they should be placed **after**
- Need to maintain a logical tab order for wrapping, so just using
`flex-flow: row wrap-reverse` doesn't work because the tab order will be
wrong
- When the elements switch, you shouldn't lose your tab place in the
document

This solution uses container queries to determine the container size and
uses that to set the wrapping. Falls back to media queries if container
queries aren't supported (it's supported on >93% of browsers according
to caniuse).

The wrapping points don't use predefined media queries because:
- containers don't care about the size of the screen. It's the intrinsic
size of the container that matters.
- wrapping at 900px seemed too far out if container queries are
unsupported. But it's a fallback, so we can switch to that if we want.

If your keyboard focus is on one of the actions on a wide screen, and
the screen goes narrow, your focus will still be after the tabs (staying
consistent), so tabbing to the next element will take you into the flag
details, while backtab takes you back to the tabs.


Before wrapping:

![image](https://github.com/user-attachments/assets/21557d9d-e083-4c0c-a7e5-400751fe5822)

After wrapping:

![image](https://github.com/user-attachments/assets/efc482b9-39b0-446c-8d8e-cfa551ce5edd)

## A note on accessibility:

The spec for flexbox (taken from [MDN's
doc](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items))
states:

> "Authors must not use order or the *-reverse values of
[flex-flow](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-flow)/flex-direction
as a substitute for correct source ordering, as that can ruin the
accessibility of the document."

So even if wrap-reverse works visually, it's not a good solution for
this.
This commit is contained in:
Thomas Heartman 2025-02-20 10:31:08 +01:00 committed by GitHub
parent bdecad10c9
commit 6e1f683902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -4,6 +4,7 @@ import {
styled,
Tab,
Tabs,
type Theme,
Tooltip,
Typography,
useMediaQuery,
@ -49,8 +50,16 @@ const NewStyledHeader = styled('div')(({ theme }) => ({
backgroundColor: 'none',
marginBottom: theme.spacing(2),
borderBottom: `1px solid ${theme.palette.divider}`,
containerType: 'inline-size',
}));
const onNarrowHeader = (theme: Theme, css: object) => ({
'@container (max-width: 650px)': css,
'@supports not (container-type: inline-size)': {
[theme.breakpoints.down('md')]: css,
},
});
const UpperHeaderRow = styled('div')(({ theme }) => ({
display: 'flex',
flexFlow: 'row wrap',
@ -60,12 +69,23 @@ const UpperHeaderRow = styled('div')(({ theme }) => ({
const LowerHeaderRow = styled(UpperHeaderRow)(({ theme }) => ({
justifyContent: 'space-between',
flexFlow: 'row nowrap',
...onNarrowHeader(theme, {
flexFlow: 'column nowrap',
alignItems: 'flex-start',
}),
}));
const HeaderActions = styled('div')(({ theme }) => ({
display: 'flex',
const HeaderActions = styled('div', {
shouldForwardProp: (propName) => propName !== 'showOnNarrowScreens',
})<{ showOnNarrowScreens?: boolean }>(({ theme, showOnNarrowScreens }) => ({
display: showOnNarrowScreens ? 'none' : 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
...onNarrowHeader(theme, {
display: showOnNarrowScreens ? 'flex' : 'none',
}),
}));
const IconButtonWithTooltip: FC<
@ -174,6 +194,79 @@ const useLegacyVariants = (environments: IFeatureToggle['environments']) => {
return enableLegacyVariants || existingLegacyVariantsExist;
};
type HeaderActionsProps = {
feature: IFeatureToggle;
showOnNarrowScreens?: boolean;
onFavorite: () => void;
handleCopyToClipboard: () => void;
isFeatureNameCopied: boolean;
openStaleDialog: () => void;
openDeleteDialog: () => void;
};
const HeaderActionsComponent = ({
showOnNarrowScreens,
feature,
onFavorite,
handleCopyToClipboard,
isFeatureNameCopied,
openStaleDialog,
openDeleteDialog,
}: HeaderActionsProps) => (
<HeaderActions showOnNarrowScreens={showOnNarrowScreens}>
<IconButtonWithTooltip
label='Favorite this feature flag'
onClick={onFavorite}
data-loading
>
{feature.favorite ? <Star /> : <StarBorder />}
</IconButtonWithTooltip>
<IconButtonWithTooltip
label='Copy flag name'
onClick={handleCopyToClipboard}
data-loading
>
{isFeatureNameCopied ? <Check /> : <FileCopyOutlined />}
</IconButtonWithTooltip>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={feature.project}
data-loading
component={Link}
to={`/projects/${feature.project}/features/${feature.name}/copy`}
tooltipProps={{
title: 'Clone',
}}
>
<LibraryAddOutlined />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={feature.project}
tooltipProps={{
title: 'Archive feature flag',
}}
data-loading
onClick={openDeleteDialog}
>
<ArchiveOutlined />
</PermissionIconButton>
<PermissionIconButton
onClick={openStaleDialog}
permission={UPDATE_FEATURE}
projectId={feature.project}
tooltipProps={{
title: 'Toggle stale state',
}}
data-loading
>
<WatchLaterOutlined />
</PermissionIconButton>
</HeaderActions>
);
type Props = {
feature: IFeatureToggle;
};
@ -260,6 +353,22 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
}
};
const HeaderActionsInner: FC<{ showOnNarrowScreens?: boolean }> = ({
showOnNarrowScreens,
}) => {
return (
<HeaderActionsComponent
showOnNarrowScreens={showOnNarrowScreens}
feature={feature}
onFavorite={onFavorite}
handleCopyToClipboard={handleCopyToClipboard}
isFeatureNameCopied={isFeatureNameCopied}
openStaleDialog={() => setOpenStaleDialog(true)}
openDeleteDialog={() => setShowDelDialog(true)}
/>
);
};
return (
<>
{flagOverviewRedesign ? (
@ -271,6 +380,7 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
) : null}
</UpperHeaderRow>
<LowerHeaderRow>
<HeaderActionsInner showOnNarrowScreens />
<Tabs
value={activeTab.path}
indicatorColor='primary'
@ -286,62 +396,7 @@ export const FeatureViewHeader: FC<Props> = ({ feature }) => {
/>
))}
</Tabs>
<HeaderActions>
<IconButtonWithTooltip
label='Favorite this feature flag'
onClick={onFavorite}
data-loading
>
{feature.favorite ? <Star /> : <StarBorder />}
</IconButtonWithTooltip>
<IconButtonWithTooltip
label='Copy flag name'
onClick={handleCopyToClipboard}
data-loading
>
{isFeatureNameCopied ? (
<Check />
) : (
<FileCopyOutlined />
)}
</IconButtonWithTooltip>
<PermissionIconButton
permission={CREATE_FEATURE}
projectId={projectId}
data-loading
component={Link}
to={`/projects/${projectId}/features/${featureId}/copy`}
tooltipProps={{
title: 'Clone',
}}
>
<LibraryAddOutlined />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Archive feature flag',
}}
data-loading
onClick={() => setShowDelDialog(true)}
>
<ArchiveOutlined />
</PermissionIconButton>
<PermissionIconButton
onClick={() => setOpenStaleDialog(true)}
permission={UPDATE_FEATURE}
projectId={projectId}
tooltipProps={{
title: 'Toggle stale state',
}}
data-loading
>
<WatchLaterOutlined />
</PermissionIconButton>
</HeaderActions>
<HeaderActionsInner />
</LowerHeaderRow>
</NewStyledHeader>
) : (