Moving the conversation from #811 into a PR b/c I think it may help provide some visibility into the code changes I am thinking about here. be warned this is a really long note
We have two major API's that are Changing: Modals, Overlays (tooltips/popovers)
Consolidations
- deprecations
- OverlayMixin replaced by
Portal
component
- additions
Portal
component, just renders its children into a container
fixes #404
Modals and Overlays share some common functions underneath, and Users have come to use our OverlayMixin, the new Portal
Component offers the same functionality in a way that works for both createClass or plain class consumers
<Portal container={document.body}>
<div>rendered into the body!</div>
</Portal>
Modals
- deprecations
ModalTrigger
- fixes #397, fixes #616
title
and closeButton
props on Modal (moved to ModalHeader)
onRequestHide
prop renamed to onHide
(more idiomatic with our callbacks)
- additions - partly addresses #748
ModalHeader
ModalTitle
ModalBody
ModalFooter
Api
new components are exported on the Modal component Automatically for ease of use, or can be imported from lib if the user wants. Body, Footer, and Title are just convenient wrappers of <div className='modal-part'/>
and Modal Header will auto wire the closeButton click to the parent onHide
.
<Modal container={mountNode} onHide={function(){}} aria-labelledby='title'>
<Modal.Header closeButton>
<Modal.Title id='title'>Modal title</Modal.Title>
</Modal.Header>
<Modal.Body>
One fine body...
</Modal.Body>
<Modal.Footer>
<Button bsStyle='primary'>Save changes</Button>
</Modal.Footer>
</Modal>
I think its safe to remove (deprecated for now) ModalTrigger, it is a slight convenience over handling the state yourself ,but since you don't generally have 10s of modals in a component at once (as with tooltips potentially) I say we prefer the React philosophy of explicitness over abstraction here.
By explicitly specifying the Header, users get more customization options and better potential accessibility (aria-labelledby
above). We can gracefully deprecate the current api, by checking if a user provides a title
prop in the Modal and auto adding the new Header Component to the children, so no breaking changes.
Overlays
The tooltips and popovers (from here on out "Overlays") present some interesting issues Modals do not.
- You may have many Overlays on a page, and managing that manually in state is very verbose
- Overlays are targeted to a specific element, as well as potentially being rendered into a new subtree
Goals for the API
- maintain a similar level of ease-of-use that the OverlayTrigger offers, for wiring overlays to triggers and rendering custom overlays (non tooltips or Popovers)
- Allow better control over the provided Overlays so they can be tweaked for custom behavior, such as: #850, #816, #802, #611, #436
My Proposed path here is to ultmately end up with a declarative Overlay Component that can be completely controlled in terms of positioning, and display (show/hide). I think that looks something like the Modals above
<Button
bsStyle='default'
ref='btn'
onClick={e => this.setState({ show: !this.state.show })}
>
Holy guacamole!
</Button>
<Popover
show={this.state.show}
target={()=> this.refs.btn}
placement='bottom'
container={mountNode}
containerPadding={20}
title='Popover bottom'
>
<strong>Holy guacamole!</strong> Check this info.
</Popover>
To acheive this though, both the render-to-container (show/hide), and positioning logic need to be removed from the OverlayTrigger
Component and moved downstream. me and @taion have had a few long conversations on gitter about how far down to move it.
Before that though I have abstracted out that logic into seperate Components, we've already seen Portal
as used by Modal, and the to-be-better-named: Position
Component, for calculating an offset based on the target and container.
that leaves us with the following as the implementation of an Overlay:
class Popover extends React.Component {
render(){
let {
container
, containerPadding
, target
, placement
, ...props } = this.props;
return (
<Portal show={props.show} container={container}>
<Position {...{ container, containerPadding, target, placement }}>
<PopoverMarkup>
{ this.props.children }
</PopoverMarkup>
</Position>
</Portal>
);
}
}
Which feels like a lot of boilerplate for making custom Overlays. To help with that I am bundling both of these components up into an Overlay Component which makes it a bit better...
class Popover extends React.Component {
render() {
return (
<Overlay {...this.props}>
<PopoverMarkup {...this.props}>
{ this.props.children }
</PopoverMarkup>
</Overlay>
);
}
}
This feels a bit verbose to me but probably reasonable for Custom Overlays. It also means that the Popover and Tooltip Components are no longer "dumb" static markup components, which enables the declarative api way up page. I think that this is worth it, the Alternative is that we needto abandon the declartive api and put the logic back in OverlayTrigger or write this when we want the declarative syntax:
<Button bsStyle='default' ref='btn' onClick={e => this.setState({ show: !this.state.show })}>
Holy guacamole!
</Button>
<Overlay
show={this.state.show}
target={()=> this.refs.btn}
placement='bottom'
container={mountNode}
containerPadding={20}
>
<Popover title='Popover bottom'>
<strong>Holy guacamole!</strong> Check this info.
</Popover>
</Overlay>
I find that less optimal than just having the PopOver component but it does have an advantage that @taion thinks is worth it. If we do it this way OverlayTrigger can jsut wrap its overlay
prop in an Overlay
Component, and then you can specify static markup to it, without it needing to be a special kind of Component.
Simplier Custom Overlays
class MyOverlay {
render(){
let { positionLeft: left, positionTop: top } = this.props
return <div style={{left, top}}>my overlay</div>
}
}
<OverlayTrigger overlay={<MyOverlay/>}/>
which is less verbose than:
Simplier Declarative Overlays
class MyOverlayMarkup {
render(){
let { positionLeft: left, positionTop: top } = this.props
return <div style={{left, top}}>my overlay</div>
}
}
// we could alternatively make this a function instead of a wrapper component like:
// createOverlay(MyOverlayMarkup) or a decorator like @Overlay
class MyOverlay {
render(){
return (
<Overlay {...this.props}>
<MyOverlayMarkup {...this.props}/>
</Overlay>
);
}
}
<OverlayTrigger overlay={<MyOverlay/>}/>
GET TO THE POINT ALREADY THIS PR IS SOOOOOOOO LONG
Which Overlay Api do we go with?
- Simplier Custom Overlays
- Simplier declarative Popover/Tooltips
- Something completely different
- Just revert it all