Skip to content

Commit 4309525

Browse files
authored
Merge pull request #406 from yapplabs/glimmer-modal-dialog
Convert ModalDialog component to glimmer
2 parents f1b33e2 + c0f1eea commit 4309525

File tree

4 files changed

+211
-187
lines changed

4 files changed

+211
-187
lines changed

addon/components/modal-dialog.js

Lines changed: 0 additions & 157 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<this.whichModalDialogComponent
2+
@wrapperClass={{@wrapperClass}}
3+
@wrapperClassNames={{@wrapperClassNames}}
4+
@overlayClass={{@overlayClass}}
5+
@overlayClassNames={{@overlayClassNames}}
6+
@containerClass={{@containerClass}}
7+
@containerClassNames={{this.containerClassNamesVal}}
8+
@hasOverlay={{this.hasOverlay}}
9+
@translucentOverlay={{@translucentOverlay}}
10+
@clickOutsideToClose={{@clickOutsideToClose}}
11+
@destinationElementId={{this.destinationElementId}}
12+
@overlayPosition={{this.overlayPosition}}
13+
@tetherTarget={{@tetherTarget}}
14+
@legacyTarget={{@target}}
15+
@attachment={{@attachment}}
16+
@targetAttachment={{this.targetAttachment}}
17+
@targetModifier={{@targetModifier}}
18+
@targetOffset={{@targetOffset}}
19+
@offset={{@offset}}
20+
@tetherClassPrefix={{@tetherClassPrefix}}
21+
@constraints={{@constraints}}
22+
@attachmentClass={{this.attachmentClass}}
23+
@stack={{this.stack}}
24+
@value={{this.value}}
25+
@onClickOverlay={{this.onClickOverlayAction}}
26+
@onClose={{this.onCloseAction}}
27+
id={{this.stack}}
28+
...attributes
29+
>
30+
{{yield}}
31+
</this.whichModalDialogComponent>
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import Component from '@glimmer/component';
2+
import { inject as service } from '@ember/service';
3+
import { dasherize } from '@ember/string';
4+
import { typeOf } from '@ember/utils';
5+
import { assert, warn } from '@ember/debug';
6+
import { DEBUG } from '@glimmer/env';
7+
import { importSync } from '@embroider/macros';
8+
import { ensureSafeComponent } from '@embroider/util';
9+
import { guidFor } from '@ember/object/internals';
10+
11+
const VALID_OVERLAY_POSITIONS = ['parent', 'sibling'];
12+
13+
// Args:
14+
// `hasOverlay` | Toggles presence of overlay div in DOM
15+
// `translucentOverlay` | Indicates translucence of overlay, toggles presence of `translucent` CSS selector
16+
// `onClose` | The action handler for the dialog's `onClose` action. This action triggers when the user clicks the modal overlay.
17+
// `onClickOverlay` | An action to be called when the overlay is clicked. If this action is specified, clicking the overlay will invoke it instead of `onClose`.
18+
// `clickOutsideToClose` | Indicates whether clicking outside a modal *without* an overlay should close the modal. Useful if your modal isn't the focus of interaction, and you want hover effects to still work outside the modal.
19+
// `renderInPlace` | A boolean, when true renders the modal without wormholing or tethering, useful for including a modal in a style guide
20+
// `overlayPosition` | either `'parent'` or `'sibling'`, to control whether the overlay div is rendered as a parent element of the container div or as a sibling to it (default: `'parent'`)
21+
// `containerClass` | CSS class name(s) to append to container divs. Set this from template.
22+
// `containerClassNames` | CSS class names to append to container divs. If you subclass this component, you may define this in your subclass.)
23+
// `overlayClass` | CSS class name(s) to append to overlay divs. Set this from template.
24+
// `overlayClassNames` | CSS class names to append to overlay divs. If you subclass this component, you may define this in your subclass.)
25+
// `wrapperClass` | CSS class name(s) to append to wrapper divs. Set this from template.
26+
// `wrapperClassNames` | CSS class names to append to wrapper divs. If you subclass this component, you may define this in your subclass.)
27+
// `animatable` | A boolean, when `true` makes modal animatable using `liquid-fire` (requires `liquid-wormhole` to be installed, and for tethering situations `liquid-tether`. Having these optional dependencies installed and not specifying `animatable` will make `animatable=true` be the default.)
28+
// `tetherTarget` | If you specify a `tetherTarget`, you are opting into "tethering" behavior, and you must have either `ember-tether` or `liquid-tether` installed.
29+
// `destinationElementId`| optional
30+
// `targetAttachment` | Delegates to Hubspot Tether*
31+
// `tetherClassPrefix` | Delegates to Hubspot Tether*
32+
// `offset` | Delegates to Hubspot Tether*
33+
// `targetOffset` | Delegates to Hubspot Tether*
34+
// `constraints` | Delegates to Hubspot Tether*
35+
// `stack` | Delegates to liquid-wormhole/liquid-tether
36+
// `value` | pass a `value` to set a "value" to be passed to liquid-wormhole / liquid-tether
37+
38+
export default class ModalDialog extends Component {
39+
@service('modal-dialog') modalService;
40+
41+
get value() {
42+
// pass a `value` to set a "value" to be passed to liquid-wormhole / liquid-tether
43+
return this.args.value || 0;
44+
}
45+
get hasLiquidWormhole() {
46+
return this.modalService.hasLiquidWormhole;
47+
}
48+
49+
get hasLiquidTether() {
50+
return this.modalService.hasLiquidTether;
51+
}
52+
53+
get hasOverlay() {
54+
return this.args.hasOverlay ?? true;
55+
}
56+
57+
get stack() {
58+
// this `stack` string will be set as this element's ID and passed to liquid-wormhole / liquid-tether
59+
return guidFor(this);
60+
}
61+
62+
get containerClassNamesVal() {
63+
return this.args.containerClassNames || this.containerClassNames || null;
64+
}
65+
66+
get attachmentClass() {
67+
let { attachment } = this.args;
68+
if (!attachment) {
69+
return undefined;
70+
}
71+
return attachment
72+
.split(' ')
73+
.map((attachmentPart) => {
74+
return `emd-attachment-${dasherize(attachmentPart)}`;
75+
})
76+
.join(' ');
77+
}
78+
79+
get targetAttachment() {
80+
return this.args.targetAttachment || 'middle center';
81+
}
82+
83+
get whichModalDialogComponent() {
84+
let { hasLiquidTether, hasLiquidWormhole } = this;
85+
let { animatable, tetherTarget, renderInPlace } = this.args;
86+
let module = importSync('ember-modal-dialog/components/basic-dialog');
87+
88+
if (renderInPlace) {
89+
module = importSync('ember-modal-dialog/components/in-place-dialog');
90+
} else if (
91+
tetherTarget &&
92+
hasLiquidTether &&
93+
hasLiquidWormhole &&
94+
animatable === true
95+
) {
96+
module = importSync('ember-modal-dialog/components/liquid-tether-dialog');
97+
} else if (tetherTarget) {
98+
this.ensureEmberTetherPresent();
99+
module = importSync('ember-modal-dialog/components/tether-dialog');
100+
} else if (hasLiquidWormhole && animatable === true) {
101+
module = importSync('ember-modal-dialog/components/liquid-dialog');
102+
}
103+
104+
return ensureSafeComponent(module.default, this);
105+
}
106+
107+
get destinationElementId() {
108+
return (
109+
this.args.destinationElementId || this.modalService.destinationElementId
110+
);
111+
}
112+
113+
validateProps() {
114+
let overlayPosition = this.overlayPosition;
115+
if (VALID_OVERLAY_POSITIONS.indexOf(overlayPosition) === -1) {
116+
warn(
117+
`overlayPosition value '${overlayPosition}' is not valid (valid values [${VALID_OVERLAY_POSITIONS.join(
118+
', '
119+
)}])`,
120+
false,
121+
{ id: 'ember-modal-dialog.validate-overlay-position' }
122+
);
123+
}
124+
}
125+
126+
get overlayPosition() {
127+
let result = this.args.overlayPosition || 'parent';
128+
if (DEBUG && VALID_OVERLAY_POSITIONS.indexOf(result) === -1) {
129+
warn(
130+
`overlayPosition value '${result}' is not valid (valid values [${VALID_OVERLAY_POSITIONS.join(
131+
', '
132+
)}])`,
133+
false,
134+
{ id: 'ember-modal-dialog.validate-overlay-position' }
135+
);
136+
}
137+
return result;
138+
}
139+
140+
ensureEmberTetherPresent() {
141+
if (!this.modalService.hasEmberTether) {
142+
throw new Error(
143+
'Please install ember-tether in order to pass a tetherTarget to modal-dialog'
144+
);
145+
}
146+
}
147+
148+
onCloseAction = () => {
149+
const { onClose } = this.args;
150+
// we shouldn't warn if the callback is not provided at all
151+
if (!onClose) {
152+
return;
153+
}
154+
155+
assert(
156+
'onClose handler must be a function',
157+
typeOf(onClose) === 'function'
158+
);
159+
160+
onClose();
161+
};
162+
163+
onClickOverlayAction = (ev) => {
164+
ev.preventDefault();
165+
166+
const { onClickOverlay } = this.args;
167+
// we shouldn't warn if the callback is not provided at all
168+
if (!onClickOverlay) {
169+
this.onCloseAction();
170+
return;
171+
}
172+
173+
assert(
174+
'onClickOverlay handler must be a function',
175+
typeOf(onClickOverlay) === 'function'
176+
);
177+
178+
onClickOverlay();
179+
};
180+
}

0 commit comments

Comments
 (0)