-
Notifications
You must be signed in to change notification settings - Fork 593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bug 1829549: fix resizing issues with cloud shell terminal drawer #5224
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.ocs-draggable-core-iframe-fix { | ||
& iframe { | ||
pointer-events: none !important; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as React from 'react'; | ||
import { DraggableCore, DraggableEvent, DraggableData } from 'react-draggable'; | ||
|
||
import './DraggableCoreIFrameFix.scss'; | ||
|
||
const DraggableCoreIFrameFix: React.FC<React.ComponentProps<typeof DraggableCore>> = ({ | ||
onStart, | ||
onStop, | ||
...other | ||
}) => { | ||
const onStartFn = | ||
// rule is inconsistent with typescript return type | ||
// eslint-disable-next-line consistent-return | ||
(e: DraggableEvent, data: DraggableData): false | void => { | ||
document.body.classList.add('ocs-draggable-core-iframe-fix'); | ||
if (onStart) { | ||
return onStart(e, data); | ||
} | ||
}; | ||
|
||
const onStopFn = | ||
// rule is inconsistent with typescript return type | ||
// eslint-disable-next-line consistent-return | ||
(e: DraggableEvent, data: DraggableData): false | void => { | ||
document.body.classList.remove('ocs-draggable-core-iframe-fix'); | ||
if (onStop) { | ||
return onStop(e, data); | ||
} | ||
}; | ||
|
||
return <DraggableCore {...other} onStart={onStartFn} onStop={onStopFn} />; | ||
}; | ||
|
||
export default DraggableCoreIFrameFix; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,29 @@ | ||
import * as React from 'react'; | ||
import { CSSTransition } from 'react-transition-group'; | ||
import { DraggableData, DraggableCore } from 'react-draggable'; | ||
import { DraggableEvent } from 'react-draggable'; | ||
import DraggableCoreIFrameFix from './DraggableCoreIFrameFix'; | ||
import './Drawer.scss'; | ||
|
||
type DrawerProps = { | ||
/** | ||
* Height of the drawer, | ||
* should be set when used as controlled component with onChange callback | ||
* Controlled height of the drawer. | ||
* Should be set when used as controlled component with onChange callback. | ||
*/ | ||
height?: number; | ||
/** | ||
* Default Value: 300 | ||
* default height of the drawer component, | ||
* should be set when used as Uncontrolled component | ||
* Uncontrolled default height of the drawer. | ||
*/ | ||
defaultHeight?: number; | ||
/** | ||
* Defines the minimized state of drawer | ||
* false: Height is minimum height (minimized state) | ||
* true: Height is greater than minimum height | ||
* Toggles controlled open state. | ||
*/ | ||
open?: boolean; | ||
/** | ||
* Default Value: true | ||
* Uncontrolled open state of the drawer on first render. | ||
*/ | ||
defaultOpen?: boolean; | ||
/** | ||
* Maximum height drawer can be resized to. | ||
*/ | ||
|
@@ -53,61 +56,79 @@ const useSize = <T extends HTMLElement>(): [number, (element: T) => void] => { | |
return [height, callback]; | ||
}; | ||
|
||
// get the pageX value from a mouse or touch event | ||
const getPageY = (e: DraggableEvent): number => | ||
(e as MouseEvent).pageY ?? (e as TouchEvent).touches?.[0]?.pageY; | ||
|
||
const Drawer: React.FC<DrawerProps> = ({ | ||
children, | ||
defaultHeight = 300, | ||
height, | ||
maxHeight = '100%', | ||
open = true, | ||
open, | ||
defaultOpen = true, | ||
resizable = false, | ||
header, | ||
onChange, | ||
}) => { | ||
const drawerRef = React.useRef<HTMLDivElement>(); | ||
const [heightState, setHeightState] = React.useState(defaultHeight); | ||
const lastObservedHeightRef = React.useRef<number>(0); | ||
const [openState, setOpenState] = React.useState(defaultOpen); | ||
const lastObservedHeightRef = React.useRef<number>(); | ||
const startRef = React.useRef<number>(); | ||
const [minHeight, headerRef] = useSize<HTMLDivElement>(); | ||
const resizeHeight = height ?? heightState; | ||
const minimumHeight = minHeight ?? 0; | ||
const handleDrag = (e: MouseEvent, data: DraggableData) => { | ||
const newHeight = resizeHeight - data.y; | ||
|
||
// merge controlled and uncontrolled states | ||
const currentOpen = open ?? openState; | ||
const currentHeight = height ?? heightState; | ||
|
||
const setHeight = (drawerHeight: number, forceOpen?: boolean) => { | ||
const newHeight = Math.max(drawerHeight, minimumHeight); | ||
const newOpen = forceOpen ?? newHeight > minimumHeight; | ||
setHeightState(newHeight); | ||
setOpenState(newOpen); | ||
if (onChange) { | ||
if (newHeight > minimumHeight) { | ||
onChange(true, newHeight); | ||
} else { | ||
onChange(false, minimumHeight); | ||
} | ||
} else { | ||
setHeightState(newHeight); | ||
onChange(newOpen, newHeight); | ||
} | ||
}; | ||
|
||
const handleResizeStart = (e: MouseEvent, data: DraggableData) => { | ||
lastObservedHeightRef.current = resizeHeight; | ||
const newHeight = resizeHeight - data.y; | ||
// if the drawer is in minimized state and drag started then | ||
// reset the lastObservedHeight to minimumHeight | ||
if (onChange && !open && newHeight > minimumHeight) { | ||
onChange(false, minimumHeight); | ||
const handleDrag = (e: DraggableEvent) => { | ||
setHeight(startRef.current - getPageY(e)); | ||
}; | ||
|
||
const handleResizeStart = (e: DraggableEvent) => { | ||
lastObservedHeightRef.current = currentHeight; | ||
// always start with actual drawer height | ||
const drawerHeight = drawerRef.current?.offsetHeight || currentHeight; | ||
startRef.current = drawerHeight + getPageY(e); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DraggableData doesn't work for us once we hit max height. if you look at the height that's computed in master when you start to drag up beyond the masthead, you'll see that the height quickly increases by hundreds and then thousands. Which is incorrect. By using pageY we are always computing a height value relative to the starting height rather than relative to the last event. |
||
if (drawerHeight !== currentHeight) { | ||
setHeight(drawerHeight); | ||
} | ||
}; | ||
|
||
const handleResizeStop = (e: MouseEvent, data: DraggableData) => { | ||
const newHeight = resizeHeight - data.y; | ||
if (onChange && newHeight <= minimumHeight) { | ||
onChange(false, lastObservedHeightRef.current); | ||
const handleResizeStop = () => { | ||
if (currentHeight <= minimumHeight) { | ||
setHeight(lastObservedHeightRef.current, false); | ||
} | ||
}; | ||
|
||
const draggable = resizable && ( | ||
<DraggableCore onDrag={handleDrag} onStart={handleResizeStart} onStop={handleResizeStop}> | ||
<DraggableCoreIFrameFix | ||
onDrag={handleDrag} | ||
onStart={handleResizeStart} | ||
onStop={handleResizeStop} | ||
> | ||
<div className="ocs-drawer__drag-handle" /> | ||
</DraggableCore> | ||
</DraggableCoreIFrameFix> | ||
); | ||
return ( | ||
<CSSTransition appear in timeout={225} classNames="ocs-drawer"> | ||
<div | ||
ref={drawerRef} | ||
className="ocs-drawer" | ||
style={{ | ||
height: open ? resizeHeight : minimumHeight, | ||
height: currentOpen ? currentHeight : minimumHeight, | ||
maxHeight, | ||
minHeight: minimumHeight, | ||
}} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as React from 'react'; | ||
import { shallow } from 'enzyme'; | ||
import DraggableCoreIFrameFix from '../DraggableCoreIFrameFix'; | ||
import { DraggableCore, DraggableEvent, DraggableData } from 'react-draggable'; | ||
|
||
describe('DraggableCoreIFrameFix', () => { | ||
it('should execute handlers and apply fix class', () => { | ||
const onStart = jest.fn(); | ||
const onStop = jest.fn(); | ||
const event = {} as DraggableEvent; | ||
const data = {} as DraggableData; | ||
const wrapper = shallow(<DraggableCoreIFrameFix onStart={onStart} onStop={onStop} />); | ||
|
||
wrapper | ||
.find(DraggableCore) | ||
.props() | ||
.onStart(event, data); | ||
expect(document.body.className).toBe('ocs-draggable-core-iframe-fix'); | ||
|
||
wrapper | ||
.find(DraggableCore) | ||
.props() | ||
.onStop(event, data); | ||
expect(document.body.className).toBe(''); | ||
|
||
expect(onStart).toHaveBeenCalledWith(event, data); | ||
expect(onStop).toHaveBeenCalledWith(event, data); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to return here? we can just call the onStart function without returning it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API from react-draggable is to return
false | void
this ensures that we allow theonStart
provided to have a return value.