-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
UiFramework: Modals in react #11500
UiFramework: Modals in react #11500
Conversation
493c364
to
6b2224b
Compare
ui_framework/components/index.js
Outdated
@@ -4,9 +4,15 @@ export { | |||
KuiButtonIcon, | |||
KuiLinkButton, | |||
KuiSubmitButton, | |||
} from './button'; | |||
} from './button/index'; |
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.
Is there a reason you explicitly have to include index
here? This doesn't seem to be related to this PR at all...
const app = uiModules.get('app/kibana', ['react']); | ||
app.directive('toolBarSearchBox', function (reactDirective) { | ||
return reactDirective(KuiToolBarSearchBox); | ||
}); | ||
app.directive('confirmModal', function (reactDirective) { |
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.
Are there any future plans for removing this and being able to use React components like we would use Angular components? Or, I'm assuming this gets auto-loaded somewhere in the Angular bootup, so is the plan to no longer import
the components we need wherever we need them? I'm a bit out of touch with our "React in Angular" plans...
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.
It gets imported in public/kibana
, so anyone can use them. For ui_framework components, which are generic, it's nice to include them once so you can use them everywhere. For react components that are specific to a certain area, then I would import them where you need them.
So no, no plans to remove this.
ui_framework/components/index.js
Outdated
export { | ||
KuiConfirmModal, | ||
KuiModalOverlay | ||
} from './modal/index'; |
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.
Here as well, why do you need to include the index
?
} | ||
|
||
KuiConfirmModal.propTypes = { | ||
message: React.PropTypes.string, |
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.
Note: React.PropTypes is deprecated as of React v15.5. Please use the prop-types library instead.
React.PropTypes has been depricated, so we shouldn't be using them in new code. Use the prop-types
module instead.
|
||
export function KuiModal({ children, ...rest }) { | ||
return ( | ||
<div className="kuiModal" { ...rest }> |
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.
I really dislike this "rest" pattern. I much prefer to be explicit about the arguments you pass along, or possibly pull them off context
or something (though I'll admit I don't even know if that's a thing, I haven't ever used context
before...).
But, that conversation aside, doing this means that you can clobber the className
on components. Here's an example. Is this intentional? Do you want the className to be overridden?
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.
I agree with you @w33ble, but we've had this discussion in a previous PR for React buttons. Here's some context:
- Original comment: [UI Framework] Create Button React components in UI Framework. #10646 (comment)
- Discussion: [UI Framework] Create Button React components in UI Framework. #10646 (comment)
That being said, you're right about the problem with clobbering the className
. We should be concatenating these strings, not overriding them:
const {
className,
children,
...rest
} = props
const classes = classnames('my-class', className)
return <a className={ classes } { ...rest }>{ children }</a>
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.
I mostly dislike the "rest" pattern unless it's for low-level components that are basically just "simple DOM wrappers" where we usually want to push multiple attributes through (like a wrapper around a
, where we don't actually do anything to href
etc in the component).
(Though, I've never built components in this way where they are all just "one-line wrappers all the way down", so I don't have any experience in this context to base my opinions on)
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.
Pulled out className to avoid overwriting them.
@kjbekkelund... so you are saying use ...rest
for this component because it's a simple wrapper, but for the likes of KuiConfirmModal, which is more complicated, manually pass them in?
I'm fine with whatever.
What I think would be nice is a helper function to build these components, where all they are doing is using a className with children. There are so many of them that look identical except for a class name. But I hear a lot of feedback that I tend to over-DRY so I'll leave that alone for now. :)
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.
FYI, these required html props, as far as I can tell, have to be handled somewhat specially because of the -
in their name. So instead of doing:
KuiConfirmModal({ stuff, aria-label, data-test-subj}) {
....
You have to do:
KuiConfirmModal({ stuff, ...rest }) {
const ariaLabel = rest['aria-label'];
const dataTestSubj = rest['data-test-subj'];
....
Unless anyone else knows a better way to handle that?
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.
@stacey-gammon Not sure what I think of this myself, but this was my thinking: My main "concern/thought" around wrappers is that if it's a DOM wrapper you know what can be in rest
(any relevant html attribute), but if it's a React component that becomes a bit more unclear (it also makes the api unclear, as it's not super-clear what you actually expect to receive and pass down into lower-level components).
Also: React will let you know if DOM elements receives the wrong attribute (https://facebook.github.io/react/warnings/unknown-prop.html).
However, I also think there are edge-cases to this. E.g. on Cloud we had a couple different Button helpers that receive rest
even if they weren't "pure DOM components" (they passed on to lower-level button helpers, e.g. DangerButton
wrapped SpinButton
that wrapped regular DOM buttons).
I think rest
is a good approach if you're expected to see it as a "DOM component" from the outside (e.g. a DangerButton
that might have some extra fields, but you can give it everything you can give a normal DOM button
).
Historically I've added these rest
params based on actual need in the code, but that might be a bit different in this context as 1) the team is much larger, 2) we're building a UI framework.
import { KuiModalFooter } from './modal_footer'; | ||
import { KuiModalHeader } from './modal_header'; | ||
import { KuiModalBody } from './modal_body'; | ||
import { KuiModalBodyText } from './modal_body_text'; |
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.
All of these are exposed in ../modal
, why not pull them all from there? Or, pull them all from ../index
since they also exist in there? I don't like the root level index personally, but you're already using it below for the button...
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.
index.js
also exports this module... doesn't it create a circular dependency if you import from that module while also being exported from 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.
Ah, yes, I suppose it would. One more reason for me to dislike the root level index.js that exports all the modules then. 🏆
My point about just using ../modal
still stands though.
</button> | ||
</div> | ||
</div> | ||
<confirm-modal |
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.
I assume this works because of the react_components.js
file, which I'm assuming is auto loaded somewhere else. It's really odd that the src/ui/public/modals/confirm_modal.*
code doesn't actually contain a reference to the React component it's using. That's a fair bit of indirection, and someone unfamiliar with the codebase probably won't know to go digging into the ui-framework code to find this, since it's not import
ed anywhere close to where it's being used.
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.
Correct, and yea, it's a good point, but you have to create the angular directives that map to the react component, and I believe there can only be one. So if the react confirm modals in a couple different places, which one is going to register the directive? @weltenwort does that sound accurate?
I tried to find the original discussion for where this model came from, and it appears in the very first react icons
PR (#10247), but couldn't find the discussion. Perhaps it was offline - @kjbekkelund or @cjcenizal, do you remember us discussing this?
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.
Not sure — I'm not that knowledgeable about our Angular setup. I also agree with @w33ble (if it's not too painful to move in that direction)
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.
yes, the angular directive is globally registered in the body of
react_component.js
:
app.directive('confirmModal', function (reactDirective) {
return reactDirective(KuiConfirmModal);
});
Since javascript modules are singletons, it will only be executed once, no
matter how often that file is imported. If a directive of the same name is
registered again in some other place, the order of execution of the two
statements matters. The last directive definition wins.
All directives are global in an angular 1 app (as are services, filters,
etc...). I don't think there's a way around that. That's one of the things
angular 2 does better (and react, of course). The import and the usage will
always be in separate files, because you cannot import anything in an angular
template.
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.
In that case, I think we should stick with this path, so we ensure modules from the uiFramework are only morphed into directives once.
In either case, if we want to have a longer discussion about this, I say we move it out of this specific PR since it's not something new that this PR introduced.
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.
I say we move it out of this specific PR since it's not something new that this PR introduced
Agreed. I'm very much against auto-loading modules on startup, we used to do that, and it got very messy; explicit imports are much easier to understand. I don't know why the auto-loading stuff has come back and why there hasn't been a strong push against it. But that's not a conversation for this PR, and I'm glad to see it hasn't stopped it from progressing. I was just trying to understand our plans around the Angular+React stuff.
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.
This is awesome, just had a few requests and questions.
@@ -4,6 +4,7 @@ | |||
border: 1px solid $modalBorderColor; | |||
border-radius: $globalBorderRadius; | |||
box-shadow: 0 5px 22px rgba(#000000, 0.25); | |||
z-index: 1001; |
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.
Can you create a variable called $globalModalDepth
in the variables file and assign this value there?
@snide Just thought you'd like to see me making this kind of snideresque comment. Cherish this moment!
import { KuiModalFooter } from './modal_footer'; | ||
import { KuiModalHeader } from './modal_header'; | ||
import { KuiModalBody } from './modal_body'; | ||
import { KuiModalBodyText } from './modal_body_text'; |
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.
index.js
also exports this module... doesn't it create a circular dependency if you import from that module while also being exported from it?
</div> | ||
</div> | ||
<confirm-modal | ||
className="kui" |
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.
What does this do? I don't see this class defined anywhere.
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.
whoops, removed
import { KuiModalBodyText } from './modal_body_text'; | ||
import { KuiButton } from '../index'; | ||
|
||
export function KuiConfirmModal({ message, title, onCancel, onConfirm, cancelButtonText, confirmButtonText, ...rest }) { |
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.
Would you mind putting each of these these variables on its own line? I find it much easier to scan when there are so many properties.
export function KuiConfirmModal({
message,
title,
onCancel,
onConfirm,
cancelButtonText,
confirmButtonText,
...rest,
}) {
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.
If you're going to do that, this looks cleaner:
export function KuiConfirmModal(props) {
const {
message,
title,
onCancel,
onConfirm,
cancelButtonText,
confirmButtonText,
...rest,
} = props;
//...
}
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.
I prefer @cjcenizal's approach instead of receiving props
then immediately destructuring (so keeping as-is, just splitting over multiple lines).
{ | ||
title ? | ||
<KuiModalHeader> | ||
<div className="kuiModalHeader__title" data-test-subj="confirmModalTitleText"> |
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.
Can we create a KuiModalHeaderTitle
component to wrap this?
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.
Yea. Just to confirm, you are opposed to refactoring these div wrappers that have just a className? There are so many of them and creating a separate file + test for each one creates so much noise. So much copy/pasting when creating one.
expect(component).toMatchSnapshot(); | ||
|
||
component.find('[data-test-subj="confirmModalConfirmButton"]').simulate('click'); | ||
sinon.assert.calledOnce(onConfirm); |
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.
Do we need this assertion here if we have it in the onConfirm
test?
{ ...requiredProps } | ||
/>); | ||
onConfirm.reset(); | ||
onCancel.reset(); |
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.
After looking through https://facebook.github.io/jest/docs/setup-teardown.html, I think we should take advantage of the beforeEach
setup function.
Can we change the spies to be let
variables, and just reassign them inside of a beforeEach
?
let onConfirm, onCancel;
beforeEach(() => {
onConfirm = sinon.spy();
onCancel = sinon.spy();
});
|
||
export function KuiModal({ children, ...rest }) { | ||
return ( | ||
<div className="kuiModal" { ...rest }> |
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.
I agree with you @w33ble, but we've had this discussion in a previous PR for React buttons. Here's some context:
- Original comment: [UI Framework] Create Button React components in UI Framework. #10646 (comment)
- Discussion: [UI Framework] Create Button React components in UI Framework. #10646 (comment)
That being said, you're right about the problem with clobbering the className
. We should be concatenating these strings, not overriding them:
const {
className,
children,
...rest
} = props
const classes = classnames('my-class', className)
return <a className={ classes } { ...rest }>{ children }</a>
> | ||
<KuiModal | ||
aria-label="aria-label" | ||
className="testClass1 testClass2" |
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.
missing modal class
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.
good catch. Fixing the className collision fixed all these.
const onCancel = sinon.spy(); | ||
|
||
test('renders KuiConfirmModal', () => { | ||
const component = mount(<KuiConfirmModal |
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.
What does the snapshot look like if you shallow render this instead? (As it's built up of purely lower-level components it might make it include "less noise")
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.
Interesting, didn't know that, yep, looks much better. switched!
Just curious - was the |
@pugnascotia I haven't looked at that one. https://github.com/tajo/react-portal has been brough tup, but due to the react embedded in angular nature of this, I'm not sure we can take advantage of it just yet. Once we have a react component that needs to use the react modals, we'll take another look, but for right now, we can't get too fancy because of the angular side of things. At least that is my understanding of 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.
Down with React.PropTypes
} | ||
|
||
KuiModal.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalBody.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalBodyText.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalFooter.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalHeader.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalHeaderTitle.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
} | ||
|
||
KuiModalOverlay.propTypes = { | ||
className: React.PropTypes.string, |
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.
More React.PropTypes
, which we should stop using. Please use the prop-types
module.
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.
LGTM!
Build failure seems unrelated:
|
Up next: jest tests
- Relies on elastic#10821 getting checked in first for commonHtmlProps
- Can’t use the modalOverlay or it would be two nested react roots, due to the way it’s embedded in angular.
I have no idea why the introduction of react would cause this to fail as it was, but for some reason, grabbing the button reference has to be after the digest loop.
13adb11
to
7cbda77
Compare
* Reactify the confirmation modal Up next: jest tests * Add tests - Relies on elastic#10821 getting checked in first for commonHtmlProps * Don't include the overlay as part of the confirm modal component * Use the react version of a confirmation modal - Can’t use the modalOverlay or it would be two nested react roots, due to the way it’s embedded in angular. * Add snapshots * Fix tests * fix confirm_modal_promise tests I have no idea why the introduction of react would cause this to fail as it was, but for some reason, grabbing the button reference has to be after the digest loop. * Address code review comments * switch over the remaining React.PropType references (in the modals dir anyway)
* Reactify the confirmation modal Up next: jest tests * Add tests - Relies on #10821 getting checked in first for commonHtmlProps * Don't include the overlay as part of the confirm modal component * Use the react version of a confirmation modal - Can’t use the modalOverlay or it would be two nested react roots, due to the way it’s embedded in angular. * Add snapshots * Fix tests * fix confirm_modal_promise tests I have no idea why the introduction of react would cause this to fail as it was, but for some reason, grabbing the button reference has to be after the digest loop. * Address code review comments * switch over the remaining React.PropType references (in the modals dir anyway)
Add modals in the ui framework. Not used yet in angular.