@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/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` - flow's
12
- * `isVisible` is a much more general, base-Component concept (computed
13
- * from the render tree itself - whether a component is actually being
14
- * rendered at all at its own position right now, propagated down from
15
- * the root via renderParent - see flow.core's own reactiveRender()) that
16
- * cascade's direct-render model has no equivalent for yet. `showing` here
17
- * is Overlay's own, narrower concept: set this to true/false directly
18
- * (or via the properties bag - `overlay(content, {showing: true})`) to
19
- * control it, not flow's generic `.show(value)` helper (which just
20
- * conditionally includes a component in a build() result at all -
21
- * ported separately, unchanged, onto Component itself).
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 PortalContents - see 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.visibleOnFrame !== overlayFrame) {
71
- if (u.visibleOnFrame) u.visibleOnFrame.hideOverlay(this);
72
- u.visibleOnFrame = overlayFrame;
73
- u.shownChild = this.overlayChild;
74
- overlayFrame.showOverlay(this, this.overlayChild);
75
- } else if (overlayFrame && u.shownChild !== this.overlayChild) {
76
- // Shown already, with other content: a modal window becoming full
77
- // screen, say - hand the frame the new content.
78
- u.shownChild = this.overlayChild;
79
- overlayFrame.showOverlay(this, this.overlayChild);
80
- }
81
- } else {
82
- this.onHide();
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
+ }
@@ -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
- * (`this.overlayFrame = this`), so the walk always resolves to the
17
- * *nearest* one, never skipping past it to an outer frame. That's what
18
- * makes recursive modality work: a dialog opened from inside another
19
- * modal's own content finds *that* modal's own frame, not the page's
20
- * top-level one, so a third modal opened from inside it stacks correctly
21
- * on top of the second, not the first.
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.receivedOverlayContent = overlayContent || null;
44
- // Provides itself for inherit("overlayFrame") - see Component.provide().
45
- this.overlayFrame = this;
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
- initializeState() {
58
- return { assignedOverlayContent: null };
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.render(). `contentProvider` is the
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 render() - which is to say, from
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 `assignedOverlayContent`: "time as tree position" means a
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
- // initializeState() above set up rather than splicing a new one in
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
- showOverlay(contentProvider, overlayContent) {
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
- flush(() => this.setState({ assignedOverlayContent: overlayContent }));
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
- // Replace this frame's own static content in place - used when the
102
- // *same* modalSubFrame is reused for new overlay content (see build()
103
- // below) rather than tearing the sub-frame down and recreating it,
104
- // which would also retract and rebuild everything recursively nested
105
- // inside it for no reason.
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.assignedOverlayContent && this.receivedOverlayContent) {
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.assignedOverlayContent || this.receivedOverlayContent;
115
-
116
- // const u = this.unobservable;
140
+ const overlayContent = this.shownContent || this.propertyContent;
117
141
 
118
- const children = [...this.staticContent];
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",