@liquefy/cascade.ui 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/package.json +5 -5
- package/src/BasicTheme.js +14 -10
- package/src/Dropdown.js +84 -0
- package/src/Layout.js +153 -120
- package/src/Modal.js +136 -0
- package/src/Overlay.js +95 -85
- package/src/OverlayFrame.js +77 -39
- package/src/Popover.js +174 -158
- package/src/index.js +3 -2
- package/src/widgets.js +127 -109
- package/src/Portal.js +0 -132
package/src/Overlay.js
CHANGED
|
@@ -1,85 +1,95 @@
|
|
|
1
|
-
import { Component, withoutRecording } from "@liquefy/cascade.component";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Overlay - the other half of overlay.js's port (see OverlayFrame.js).
|
|
5
|
-
* Builds nothing at its own position; its whole job is to hand its
|
|
6
|
-
* single child to the nearest enclosing OverlayFrame (found via
|
|
7
|
-
* `this.inherit("overlayFrame")`) whenever `this.showing` is true, and
|
|
8
|
-
* take it back whenever it's false (or this component stops being shown
|
|
9
|
-
* altogether - see onHide()).
|
|
10
|
-
*
|
|
11
|
-
* Deliberately named `showing`, not flow's own `isVisible
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
|
-
export function overlay(...parameters) {
|
|
24
|
-
return new Overlay(...parameters);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class Overlay extends Component {
|
|
28
|
-
setProperties({ children, showing }) {
|
|
29
|
-
if (children && children.length > 1) {
|
|
30
|
-
throw new Error("Overlay accepts at most a single child.");
|
|
31
|
-
}
|
|
32
|
-
this.overlayChild = children && children.length > 0 ? children[0] : null;
|
|
33
|
-
this.showing = !!showing;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
initialUnobservables() {
|
|
37
|
-
return { visibleOnFrame: null, shownChild: null };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Build-only (like
|
|
41
|
-
// while something is showing this component, so building is where the
|
|
42
|
-
// content is handed to the frame - again whenever showing changes.
|
|
43
|
-
// Builds nothing where it stands.
|
|
44
|
-
build() {
|
|
45
|
-
this.update();
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Shown again (see Component.onShow()): an up-to-date build doesn't
|
|
50
|
-
// rerun, so hand the content over here. Read without recording - this
|
|
51
|
-
// runs inside whoever is rendering.
|
|
52
|
-
onShow() {
|
|
53
|
-
withoutRecording(() => this.update());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// No longer shown - its page switched away from, say: take the content
|
|
57
|
-
// back, whatever `showing` says.
|
|
58
|
-
onHide() {
|
|
59
|
-
const u = this.unobservable;
|
|
60
|
-
if (u.visibleOnFrame) {
|
|
61
|
-
u.visibleOnFrame.hideOverlay(this);
|
|
62
|
-
u.visibleOnFrame = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
update() {
|
|
67
|
-
const u = this.unobservable;
|
|
68
|
-
if (this.showing) {
|
|
69
|
-
const overlayFrame = this.inherit("overlayFrame");
|
|
70
|
-
if (overlayFrame && u.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
u.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
import { Component, withoutRecording } from "@liquefy/cascade.component";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Overlay - the other half of overlay.js's port (see OverlayFrame.js).
|
|
5
|
+
* Builds nothing at its own position; its whole job is to hand its
|
|
6
|
+
* single child to the nearest enclosing OverlayFrame (found via
|
|
7
|
+
* `this.inherit("overlayFrame")`) whenever `this.showing` is true, and
|
|
8
|
+
* take it back whenever it's false (or this component stops being shown
|
|
9
|
+
* altogether - see onHide()).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately named `showing`, not flow's own `isVisible`: whether a
|
|
12
|
+
* component is shown at all is cascade's onShow()/onHide() (see
|
|
13
|
+
* Component), which Overlay uses itself - an Overlay that stops being
|
|
14
|
+
* shown takes its content back. `showing` is Overlay's own, narrower
|
|
15
|
+
* concept: set it to true/false directly (or via the properties bag -
|
|
16
|
+
* `overlay(content, {showing: true})`) to control it, not the generic
|
|
17
|
+
* `.showIf(value)` helper (which just conditionally includes a component
|
|
18
|
+
* in a build() result at all).
|
|
19
|
+
*
|
|
20
|
+
* One overlay per frame: shown on a frame already showing another
|
|
21
|
+
* Overlay's content, this one evicts it (see OverlayFrame's class doc).
|
|
22
|
+
*/
|
|
23
|
+
export function overlay(...parameters) {
|
|
24
|
+
return new Overlay(...parameters);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Overlay extends Component {
|
|
28
|
+
setProperties({ children, showing }) {
|
|
29
|
+
if (children && children.length > 1) {
|
|
30
|
+
throw new Error("Overlay accepts at most a single child.");
|
|
31
|
+
}
|
|
32
|
+
this.overlayChild = children && children.length > 0 ? children[0] : null;
|
|
33
|
+
this.showing = !!showing;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
initialUnobservables() {
|
|
37
|
+
return { visibleOnFrame: null, shownChild: null, shownContext: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Build-only (like PortalSource - see cascade.dom's Portal.js): a build only runs
|
|
41
|
+
// while something is showing this component, so building is where the
|
|
42
|
+
// content is handed to the frame - again whenever showing changes.
|
|
43
|
+
// Builds nothing where it stands.
|
|
44
|
+
build() {
|
|
45
|
+
this.update();
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Shown again (see Component.onShow()): an up-to-date build doesn't
|
|
50
|
+
// rerun, so hand the content over here. Read without recording - this
|
|
51
|
+
// runs inside whoever is rendering.
|
|
52
|
+
onShow() {
|
|
53
|
+
withoutRecording(() => this.update());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// No longer shown - its page switched away from, say: take the content
|
|
57
|
+
// back, whatever `showing` says.
|
|
58
|
+
onHide() {
|
|
59
|
+
const u = this.unobservable;
|
|
60
|
+
if (u.visibleOnFrame) {
|
|
61
|
+
u.visibleOnFrame.hideOverlay(this);
|
|
62
|
+
u.visibleOnFrame = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
update() {
|
|
67
|
+
const u = this.unobservable;
|
|
68
|
+
if (this.showing) {
|
|
69
|
+
const overlayFrame = this.inherit("overlayFrame");
|
|
70
|
+
if (!overlayFrame && !u.warnedMissing) {
|
|
71
|
+
u.warnedMissing = true;
|
|
72
|
+
console.warn("overlay(): no overlayFrame() around it - what it shows isn't shown.");
|
|
73
|
+
}
|
|
74
|
+
// What its content is shown with: the context this Overlay passes on
|
|
75
|
+
// - so a dialog inherits from where it's opened (see OverlayFrame).
|
|
76
|
+
const context = u.childContext;
|
|
77
|
+
if (overlayFrame && u.visibleOnFrame !== overlayFrame) {
|
|
78
|
+
if (u.visibleOnFrame) u.visibleOnFrame.hideOverlay(this);
|
|
79
|
+
u.visibleOnFrame = overlayFrame;
|
|
80
|
+
u.shownChild = this.overlayChild;
|
|
81
|
+
u.shownContext = context;
|
|
82
|
+
overlayFrame.showOverlay(this, this.overlayChild, context);
|
|
83
|
+
} else if (overlayFrame && (u.shownChild !== this.overlayChild || u.shownContext !== context)) {
|
|
84
|
+
// Shown already, with other content: a modal window becoming full
|
|
85
|
+
// screen, say - hand the frame the new content. Or placed
|
|
86
|
+
// elsewhere: the content follows.
|
|
87
|
+
u.shownChild = this.overlayChild;
|
|
88
|
+
u.shownContext = context;
|
|
89
|
+
overlayFrame.showOverlay(this, this.overlayChild, context);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
this.onHide();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/OverlayFrame.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Component, flush, frozen } from "@liquefy/cascade.component";
|
|
2
|
-
import { div } from "@liquefy/cascade.dom";
|
|
1
|
+
import { Component, flush, frozen, withoutRecording } from "@liquefy/cascade.component";
|
|
2
|
+
import { div, elementBoundsProvider } from "@liquefy/cascade.dom";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* OverlayFrame - ported from flow.ui/basic/src/overlay.js's own
|
|
@@ -12,13 +12,19 @@ import { div } from "@liquefy/cascade.dom";
|
|
|
12
12
|
* Any component nested arbitrarily deep underneath an OverlayFrame -
|
|
13
13
|
* through any mix of build() and hardcoded child references - can find
|
|
14
14
|
* *this* one specifically via `this.inherit("overlayFrame")` (see
|
|
15
|
-
* Component.inherit()): every OverlayFrame provides itself
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* Component.inherit()): every OverlayFrame provides itself as
|
|
16
|
+
* `overlayFrame`, so a lookup always finds the *nearest* one, never
|
|
17
|
+
* skipping past it to an outer frame. That's what makes recursive modality
|
|
18
|
+
* work: a dialog opened from inside another modal's own content finds
|
|
19
|
+
* *that* modal's own frame, not the page's top-level one, so a third modal
|
|
20
|
+
* opened from inside it stacks correctly on top of the second, not the
|
|
21
|
+
* first.
|
|
22
|
+
*
|
|
23
|
+
* The content an Overlay shows keeps the context of where the Overlay
|
|
24
|
+
* stands - so a dialog inherits what its page provides - with the frame it
|
|
25
|
+
* is shown in on top: the sub-frame showing it enters the tree with the
|
|
26
|
+
* Overlay's context (see Component.enteredContext()), and provides itself
|
|
27
|
+
* as `overlayFrame` on top of that.
|
|
22
28
|
*
|
|
23
29
|
* A frame's own real children are its static content plus, when
|
|
24
30
|
* something is currently showing a modal on it, exactly one nested
|
|
@@ -27,22 +33,44 @@ import { div } from "@liquefy/cascade.dom";
|
|
|
27
33
|
* doesn't block clicks through the parts of itself nothing is actually
|
|
28
34
|
* using) - which is what lets an Overlay shown *inside* that sub-frame's
|
|
29
35
|
* own content recurse the exact same way, arbitrarily deep.
|
|
36
|
+
*
|
|
37
|
+
* One overlay per frame: a frame shows the content of the Overlay that
|
|
38
|
+
* showed it last. An Overlay shown on a frame already showing another's
|
|
39
|
+
* content evicts it - the other keeps its `showing`, but isn't shown again
|
|
40
|
+
* until it's shown anew - and hiding the newer one leaves the frame empty,
|
|
41
|
+
* not the evicted one back. Overlays stack across frames (one opened from
|
|
42
|
+
* inside another's content, above), not side by side on one. This is by
|
|
43
|
+
* design, not an oversight - and may change only should a use case turn up
|
|
44
|
+
* that needs several overlays on one frame.
|
|
30
45
|
*/
|
|
31
46
|
export function overlayFrame(...parameters) {
|
|
32
47
|
return new OverlayFrame(...parameters);
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
export class OverlayFrame extends Component {
|
|
36
|
-
setProperties({ style, children, overlayContent }) {
|
|
51
|
+
setProperties({ style, children, overlayContent, originContext, isLayer }) {
|
|
37
52
|
this.style = frozen(style || null);
|
|
38
53
|
this.staticContent = frozen(children || []);
|
|
39
54
|
// The alternative to showOverlay()/hideOverlay() below: an overlay
|
|
40
55
|
// frame can be handed its modal content directly as a property
|
|
41
56
|
// instead, for a frame that's dedicated to one specific modal rather
|
|
42
57
|
// than a general, shared one any Overlay can find via inherit().
|
|
43
|
-
this.
|
|
44
|
-
//
|
|
45
|
-
|
|
58
|
+
this.propertyContent = overlayContent || null;
|
|
59
|
+
// A sub-frame showing an Overlay's content: the context of where that
|
|
60
|
+
// Overlay stands - what the content inherits from (see build()).
|
|
61
|
+
this.originContext = originContext || null;
|
|
62
|
+
// One this frame opens itself, to show overlay content on (see build()).
|
|
63
|
+
this.isLayer = !!isLayer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Found by inherit("overlayFrame") from anywhere below - see
|
|
67
|
+
// Component.provide().
|
|
68
|
+
provide() {
|
|
69
|
+
return { overlayFrame: this };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
enteredContext(given) {
|
|
73
|
+
return this.originContext || given;
|
|
46
74
|
}
|
|
47
75
|
|
|
48
76
|
// What some Overlay has currently assigned to this frame is *state* (see
|
|
@@ -54,8 +82,8 @@ export class OverlayFrame extends Component {
|
|
|
54
82
|
// which is exactly where setState() in show/hideOverlay writes it back
|
|
55
83
|
// to - the same writing, reused, not a fresh one spliced in ahead of a
|
|
56
84
|
// reader that could never actually reach it (see their comment).
|
|
57
|
-
|
|
58
|
-
return {
|
|
85
|
+
initialState() {
|
|
86
|
+
return { shownContent: null, shownContext: null };
|
|
59
87
|
}
|
|
60
88
|
|
|
61
89
|
initialUnobservables() {
|
|
@@ -63,16 +91,16 @@ export class OverlayFrame extends Component {
|
|
|
63
91
|
}
|
|
64
92
|
|
|
65
93
|
// Called by an Overlay component (via inherit("overlayFrame")) when it
|
|
66
|
-
// becomes visible - see Overlay.
|
|
94
|
+
// becomes visible - see Overlay.update(). `contentProvider` is the
|
|
67
95
|
// Overlay itself, kept so a later hideOverlay() call from a *different*
|
|
68
96
|
// Overlay (e.g. one that's since taken over) can't clear content it
|
|
69
|
-
// never actually assigned.
|
|
97
|
+
// never actually assigned - one overlay per frame (see the class doc).
|
|
70
98
|
//
|
|
71
|
-
// Called from inside an Overlay's own
|
|
99
|
+
// Called from inside an Overlay's own update - which is to say, from
|
|
72
100
|
// *later* in this same pipeline than OverlayFrame's own build() (a
|
|
73
101
|
// descendant calling back up to an ancestor). A plain write here could
|
|
74
102
|
// never actually reach OverlayFrame's own already-registered dependency
|
|
75
|
-
// on `
|
|
103
|
+
// on `shownContent`: "time as tree position" means a
|
|
76
104
|
// later-positioned write can never overtake an earlier-positioned
|
|
77
105
|
// reader, by construction (see cascade.reactive's own
|
|
78
106
|
// migrateOvertakenObserversFor) - this is exactly the motivating case
|
|
@@ -81,45 +109,55 @@ export class OverlayFrame extends Component {
|
|
|
81
109
|
// frame, without flicker - see docs/plan-flagged-scheduling.md).
|
|
82
110
|
// setState() (Component.js) makes the write land at the baseline
|
|
83
111
|
// position (before everything), reusing the exact writing
|
|
84
|
-
//
|
|
112
|
+
// initialState() above set up rather than splicing a new one in
|
|
85
113
|
// ahead of it that could never reach OverlayFrame's own reader either
|
|
86
114
|
// way - it's also the only way a state property *may* be written from
|
|
87
115
|
// inside a repeater at all - and flush() makes sure the resulting
|
|
88
116
|
// rebuild happens within this same wave rather than waiting for the
|
|
89
117
|
// next one.
|
|
90
|
-
|
|
118
|
+
//
|
|
119
|
+
// `context` is where the Overlay stands: the content is shown with it
|
|
120
|
+
// (see the class doc).
|
|
121
|
+
showOverlay(contentProvider, overlayContent, context) {
|
|
91
122
|
this.unobservable.assigningContentProvider = contentProvider;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
hideOverlay(contentProvider) {
|
|
96
|
-
if (this.unobservable.assigningContentProvider === contentProvider) {
|
|
97
|
-
flush(() => this.setState({ assignedOverlayContent: null }));
|
|
123
|
+
const current = withoutRecording(() => [this.shownContent, this.shownContext]);
|
|
124
|
+
if (current[0] !== overlayContent || current[1] !== (context || null)) {
|
|
125
|
+
flush(() => this.setState({ shownContent: overlayContent, shownContext: context || null }));
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
setStaticContent(staticContent) {
|
|
107
|
-
this.staticContent = frozen(staticContent instanceof Array ? staticContent : [staticContent]);
|
|
129
|
+
hideOverlay(contentProvider) {
|
|
130
|
+
const u = this.unobservable;
|
|
131
|
+
if (u.assigningContentProvider !== contentProvider) return;
|
|
132
|
+
u.assigningContentProvider = null;
|
|
133
|
+
flush(() => this.setState({ shownContent: null, shownContext: null }));
|
|
108
134
|
}
|
|
109
135
|
|
|
110
136
|
build() {
|
|
111
|
-
if (this.
|
|
137
|
+
if (this.shownContent && this.propertyContent) {
|
|
112
138
|
throw new Error("Cannot both assign overlay content via showOverlay() and set it as a property on the same overlay frame.");
|
|
113
139
|
}
|
|
114
|
-
const overlayContent = this.
|
|
115
|
-
|
|
116
|
-
// const u = this.unobservable;
|
|
140
|
+
const overlayContent = this.shownContent || this.propertyContent;
|
|
117
141
|
|
|
118
|
-
|
|
142
|
+
// A layer (a sub-frame showing what an overlay assigned - see below)
|
|
143
|
+
// shows its content on a measured element, covering the frame: what's
|
|
144
|
+
// shown there reads the frame's size with this.fromTarget() (see
|
|
145
|
+
// modalAssembly(), which goes full screen below a width).
|
|
146
|
+
const children = this.isLayer
|
|
147
|
+
? this.staticContent.map((content) => elementBoundsProvider({
|
|
148
|
+
style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" },
|
|
149
|
+
child: content,
|
|
150
|
+
}))
|
|
151
|
+
: [...this.staticContent];
|
|
119
152
|
|
|
120
153
|
if (overlayContent) {
|
|
121
154
|
children.push(new OverlayFrame(overlayContent, {
|
|
122
155
|
key: "modalSubFrame",
|
|
156
|
+
isLayer: true,
|
|
157
|
+
// Content an Overlay assigned keeps the context of where it came
|
|
158
|
+
// from; content handed over as a property is this frame's creator's
|
|
159
|
+
// own, and is simply shown with this frame's.
|
|
160
|
+
originContext: this.shownContent ? this.shownContext : null,
|
|
123
161
|
style: {
|
|
124
162
|
position: "absolute", top: 0, left: 0, width: "100%", height: "100%",
|
|
125
163
|
pointerEvents: "none",
|