Skip to content

Commit ef735a4

Browse files
committed
Refactor viewport component and update SizeInput parsing logic
- Updated SizeInput to correctly parse units, allowing for percentages and other units. - Refactored Viewport component to improve layout and styling, including adjustments to padding and border handling. - Removed deprecated shortcuts functionality and streamlined viewport tool integration. - Enhanced viewport state management to support custom viewport formats.
1 parent 4e4f082 commit ef735a4

File tree

8 files changed

+337
-157
lines changed

8 files changed

+337
-157
lines changed

code/core/src/manager/components/preview/SizeInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const SizeInput = ({
5959
}
6060
e.preventDefault();
6161
const num = parseInt(value, 10);
62-
const unit = value.match(/[0-9. ]+(.*)$/)?.[1] || 'px';
62+
const unit = value.match(/[0-9]+(%|[a-z]{0,4})?$/)?.[1] || 'px';
6363
const update = e.key === 'ArrowUp' ? num + 1 : num - 1;
6464
if (!Number.isNaN(num) && update >= 0) {
6565
setValue(`${update}${unit}`);

code/core/src/manager/components/preview/Viewport.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const managerContext: any = {
1212
getGlobals: fn(() => ({})),
1313
getStoryGlobals: fn(() => ({})),
1414
getUserGlobals: fn(() => ({})),
15+
updateGlobals: fn(),
16+
setAddonShortcut: fn(),
1517
on: fn(),
1618
off: fn(),
1719
emit: fn(),

code/core/src/manager/components/preview/Viewport.tsx

Lines changed: 128 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
22

33
import { ActionList, PopoverProvider } from 'storybook/internal/components';
44

5-
import { CloseAltIcon, TransferIcon } from '@storybook/icons';
5+
import { TransferIcon, UndoIcon } from '@storybook/icons';
66

77
import { styled } from 'storybook/theming';
88

@@ -13,24 +13,23 @@ import { SizeInput } from './SizeInput';
1313

1414
const ViewportWrapper = styled.div<{
1515
active: boolean;
16-
viewportWidth: string;
17-
viewportHeight: string;
18-
}>(({ active, viewportWidth, viewportHeight }) => ({
16+
isDefault: boolean;
17+
}>(({ active, isDefault }) => ({
1918
gridArea: '1 / 1',
2019
alignSelf: 'start',
2120
justifySelf: 'start',
2221
display: active ? 'inline-flex' : 'none',
2322
flexDirection: 'column',
24-
width: viewportWidth.endsWith('%') ? '100%' : 'auto',
25-
height: viewportHeight.endsWith('%') ? '100%' : 'auto',
26-
paddingTop: viewportHeight === '100%' ? 0 : 6,
27-
paddingBottom: viewportHeight === '100%' ? 0 : 40,
28-
paddingInline: viewportWidth === '100%' ? 0 : 40,
23+
gap: 6,
24+
width: '100%',
25+
height: '100%',
26+
paddingTop: isDefault ? 0 : 6,
27+
paddingBottom: isDefault ? 0 : 40,
28+
paddingInline: isDefault ? 0 : 40,
2929
}));
3030

3131
const ViewportControls = styled.div({
3232
display: 'flex',
33-
margin: '0 0 6px 6px',
3433
gap: 6,
3534
});
3635

@@ -39,28 +38,48 @@ const ViewportDimensions = styled.div({
3938
gap: 2,
4039
});
4140

42-
const TimesIcon = styled(CloseAltIcon)({
43-
padding: 2,
44-
});
45-
4641
const Dimensions = styled.div(({ theme }) => ({
4742
display: 'flex',
4843
gap: 2,
4944
fontFamily: theme.typography.fonts.mono,
5045
fontSize: theme.typography.size.s1 - 1,
46+
fontWeight: theme.typography.weight.regular,
5147
color: theme.textMutedColor,
5248
}));
5349

54-
const FrameWrapper = styled.div<{ fullWidth: boolean; fullHeight: boolean }>(
55-
({ fullWidth, fullHeight, theme }) => ({
56-
display: 'inline-block',
57-
border: `1px solid ${theme.button.border}`,
58-
borderRadius: fullWidth || fullHeight ? 0 : 4,
59-
overflow: 'hidden',
60-
...(fullWidth && { borderLeftWidth: 0, borderRightWidth: 0, width: '100%' }),
61-
...(fullHeight && { borderTopWidth: 0, borderBottomWidth: 0, height: '100%' }),
62-
})
63-
);
50+
const FrameWrapper = styled.div<{
51+
isDefault: boolean;
52+
'data-dragging': 'right' | 'bottom' | undefined;
53+
}>(({ isDefault, 'data-dragging': dragging, theme }) => ({
54+
position: 'relative',
55+
border: `1px solid ${theme.button.border}`,
56+
borderWidth: isDefault ? 0 : 1,
57+
borderRadius: isDefault ? 0 : 4,
58+
transition: 'border-color 0.2s',
59+
'&:has([data-side="right"]:hover), &[data-dragging="right"]': {
60+
borderRightColor: theme.color.secondary,
61+
boxShadow: `4px 0 5px -2px ${theme.background.hoverable}`,
62+
},
63+
'&:has([data-side="bottom"]:hover), &[data-dragging="bottom"]': {
64+
borderBottomColor: theme.color.secondary,
65+
boxShadow: `0 4px 5px -2px ${theme.background.hoverable}`,
66+
},
67+
iframe: {
68+
borderRadius: 'inherit',
69+
pointerEvents: dragging ? 'none' : 'auto',
70+
},
71+
}));
72+
73+
const DragHandle = styled.div<{
74+
'data-side': 'bottom' | 'right';
75+
}>(({ 'data-side': side }) => ({
76+
position: 'absolute',
77+
right: side === 'right' ? -12 : 0,
78+
bottom: side === 'bottom' ? -12 : 0,
79+
width: side === 'right' ? 20 : '100%',
80+
height: side === 'bottom' ? 20 : '100%',
81+
cursor: side === 'right' ? 'col-resize' : 'row-resize',
82+
}));
6483

6584
export const Viewport = ({
6685
active,
@@ -73,27 +92,81 @@ export const Viewport = ({
7392
src: string;
7493
scale: number;
7594
}) => {
76-
const { name, type, width, height, isDefault, isLocked, options, select, rotate, resize } =
77-
useViewport();
95+
const {
96+
name,
97+
type,
98+
width,
99+
height,
100+
option,
101+
isCustom,
102+
isDefault,
103+
isLocked,
104+
options,
105+
lastSelectedOption,
106+
resize,
107+
rotate,
108+
select,
109+
} = useViewport();
110+
111+
const [dragging, setDragging] = useState<undefined | 'right' | 'bottom'>(undefined);
112+
const targetRef = useRef<HTMLDivElement>(null);
113+
const dragRefX = useRef<HTMLDivElement>(null);
114+
const dragRefY = useRef<HTMLDivElement>(null);
115+
const dragSide = useRef<'bottom' | 'right'>('right');
116+
const dragStart = useRef<number | undefined>();
117+
118+
useEffect(() => {
119+
const onDrag = (e: MouseEvent) => {
120+
if (dragSide.current === 'right') {
121+
targetRef.current!.style.width = `${dragStart.current! + e.clientX}px`;
122+
} else {
123+
targetRef.current!.style.height = `${dragStart.current! + e.clientY}px`;
124+
}
125+
};
126+
127+
const onEnd = () => {
128+
window.removeEventListener('mouseup', onEnd);
129+
window.removeEventListener('mousemove', onDrag);
130+
setDragging(undefined);
131+
resize(targetRef.current!.style.width, targetRef.current!.style.height);
132+
dragStart.current = undefined;
133+
};
134+
135+
const onStart = (e: MouseEvent) => {
136+
e.preventDefault();
137+
window.addEventListener('mouseup', onEnd);
138+
window.addEventListener('mousemove', onDrag);
139+
dragSide.current = (e.currentTarget as HTMLElement).dataset.side as 'bottom' | 'right';
140+
dragStart.current =
141+
dragSide.current === 'right'
142+
? (targetRef.current?.offsetWidth ?? 0) - e.clientX
143+
: (targetRef.current?.offsetHeight ?? 0) - e.clientY;
144+
setDragging(dragSide.current);
145+
};
146+
147+
const handles = [dragRefX.current, dragRefY.current];
148+
handles.forEach((el) => el?.addEventListener('mousedown', onStart));
149+
return () => handles.forEach((el) => el?.removeEventListener('mousedown', onStart));
150+
}, [resize]);
78151

79152
return (
80-
<ViewportWrapper key={id} active={active} viewportWidth={width} viewportHeight={height}>
81-
{!isDefault && height !== '100%' && (
82-
<ViewportControls style={width !== '100%' ? { marginLeft: 0 } : {}}>
153+
<ViewportWrapper key={id} active={active} isDefault={isDefault}>
154+
{!isDefault && (
155+
<ViewportControls>
83156
<PopoverProvider
84157
offset={4}
85158
padding={0}
86159
popover={() => (
87160
<ActionList style={{ minWidth: 240 }}>
88-
{Object.entries(options).map(([key, value]) => (
89-
<ActionList.Item key={key}>
161+
{Object.entries(options).map(([key, { name, styles, type = 'other' }]) => (
162+
<ActionList.Item key={key} active={key === option}>
90163
<ActionList.Action ariaLabel={false} onClick={() => select(key)}>
91-
<ActionList.Icon>{iconsMap[value.type!]}</ActionList.Icon>
92-
<ActionList.Text>{value.name}</ActionList.Text>
164+
<ActionList.Icon>{iconsMap[type]}</ActionList.Icon>
165+
<ActionList.Text>{name}</ActionList.Text>
93166
<Dimensions>
94-
<span>{value.styles.width.replace('px', '')}</span>
167+
<span>{styles.width.replace('px', '')}</span>
95168
<span>&times;</span>
96-
<span>{value.styles.height.replace('px', '')}</span>
169+
<span>{styles.height.replace('px', '')}</span>
97170
</Dimensions>
98171
</ActionList.Action>
99172
</ActionList.Item>
@@ -107,7 +180,7 @@ export const Viewport = ({
107180
disabled={isLocked}
108181
readOnly={isLocked}
109182
>
110-
<ActionList.Icon>{iconsMap[type!]}</ActionList.Icon>
183+
<ActionList.Icon>{iconsMap[type]}</ActionList.Icon>
111184
<ActionList.Text>{name}</ActionList.Text>
112185
</ActionList.Button>
113186
</PopoverProvider>
@@ -119,33 +192,40 @@ export const Viewport = ({
119192
value={width}
120193
setValue={(value) => resize(value, height)}
121194
/>
122-
123195
<ActionList.Button
124196
key="viewport-rotate"
125197
size="small"
126198
padding="small"
127-
variant="ghost"
128-
ariaLabel={isLocked ? false : 'Rotate viewport'}
129-
disabled={isLocked}
130-
readOnly={isLocked}
199+
ariaLabel="Rotate viewport"
131200
onClick={rotate}
132201
>
133-
{isLocked ? <TimesIcon /> : <TransferIcon />}
202+
<TransferIcon />
134203
</ActionList.Button>
135-
136204
<SizeInput
137205
label="Viewport height:"
138206
prefix="H"
139207
value={height}
140208
setValue={(value) => resize(width, value)}
141209
/>
210+
{isCustom && lastSelectedOption && (
211+
<ActionList.Button
212+
key="viewport-restore"
213+
size="small"
214+
padding="small"
215+
ariaLabel="Restore viewport"
216+
onClick={() => select(lastSelectedOption)}
217+
>
218+
<UndoIcon />
219+
</ActionList.Button>
220+
)}
142221
</ViewportDimensions>
143222
</ViewportControls>
144223
)}
145224
<FrameWrapper
146-
fullWidth={width === '100%'}
147-
fullHeight={height === '100%'}
225+
isDefault={isDefault}
226+
data-dragging={dragging}
148227
style={{ width, height }}
228+
ref={targetRef}
149229
>
150230
<IFrame
151231
active={active}
@@ -156,6 +236,8 @@ export const Viewport = ({
156236
allowFullScreen
157237
scale={scale}
158238
/>
239+
<DragHandle data-side="right" ref={dragRefX} />
240+
<DragHandle data-side="bottom" ref={dragRefY} />
159241
</FrameWrapper>
160242
</ViewportWrapper>
161243
);

code/core/src/viewport/components/Tool.tsx

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
1-
import React, { type FC, useEffect, useState } from 'react';
1+
import React from 'react';
22

33
import { ToggleButton } from 'storybook/internal/components';
44

55
import { GrowIcon } from '@storybook/icons';
66

7-
import { type API } from 'storybook/manager-api';
8-
9-
import { registerShortcuts } from '../shortcuts';
107
import { useViewport } from '../useViewport';
118

12-
export const ViewportTool: FC<{ api: API }> = ({ api }) => {
13-
const viewport = useViewport();
14-
const { key, width, height, isDefault, isLocked, options, reset, select } = viewport;
15-
const [lastUsedViewport, setLastUsedViewport] = useState<string | null>(key);
16-
17-
useEffect(() => {
18-
if (key && !isDefault && !isLocked) {
19-
setLastUsedViewport(key);
20-
}
21-
}, [key, isDefault, isLocked]);
22-
23-
useEffect(() => {
24-
registerShortcuts(api, viewport);
25-
}, [api, viewport]);
26-
27-
if (!width || !height || !Object.keys(options).length) {
28-
return null;
29-
}
9+
export const ViewportTool = () => {
10+
const { isDefault, isLocked, options, lastSelectedOption, reset, select } = useViewport();
3011

3112
return (
3213
<ToggleButton
@@ -37,7 +18,7 @@ export const ViewportTool: FC<{ api: API }> = ({ api }) => {
3718
disabled={isLocked}
3819
ariaLabel={isLocked ? 'Viewport size set by story parameters' : 'Viewport'}
3920
tooltip={isLocked ? 'Viewport size set by story parameters' : 'Change viewport'}
40-
onClick={() => (isDefault ? select(lastUsedViewport || Object.keys(options)[0]) : reset())}
21+
onClick={() => (isDefault ? select(lastSelectedOption || Object.keys(options)[0]) : reset())}
4122
>
4223
<GrowIcon />
4324
</ToggleButton>

code/core/src/viewport/manager.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { addons, types } from 'storybook/manager-api';
55
import { ViewportTool } from './components/Tool';
66
import { ADDON_ID, TOOL_ID } from './constants';
77

8-
export default addons.register(ADDON_ID, (api) => {
8+
export default addons.register(ADDON_ID, () => {
99
if (globalThis?.FEATURES?.viewport) {
1010
addons.add(TOOL_ID, {
1111
title: 'viewport / media-queries',
1212
type: types.TOOL,
1313
match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId,
14-
render: () => <ViewportTool api={api} />,
14+
render: () => <ViewportTool />,
1515
});
1616
}
1717
});

code/core/src/viewport/shortcuts.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)