@inglorious/ui 7.4.0 → 7.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "7.4.0",
3
+ "version": "7.5.0",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -1,44 +1,9 @@
1
1
  /**
2
2
  * @typedef {import("../../../types/navigation/view-transitions").TransitionsStore} Store
3
3
  * @typedef {import("../../../types/navigation/view-transitions").ViewTransitionType} ViewTransitionType
4
+ * @typedef {import("../../../types/navigation/view-transitions").TransitionsOptions} TransitionsOptions
4
5
  */
5
6
 
6
- /** The path the router matches against. Hashes are deliberately excluded: they
7
- * change the fragment, not the page, so they must not animate one. */
8
- function getPath() {
9
- return window.location.pathname + window.location.search
10
- }
11
-
12
- /** The position of the current entry in the session history, stamped by the
13
- * Router as it navigates. Comparing it to the previous one is what tells a push
14
- * from a traversal, and in which direction. */
15
- function getIndex() {
16
- return window.history.state?.idx ?? 0
17
- }
18
-
19
- function supportsViewTransitions() {
20
- return typeof document.startViewTransition === "function"
21
- }
22
-
23
- /** `types` landed after `startViewTransition` itself, so an engine can have the
24
- * method and still throw when handed the object form of the argument. */
25
- function supportsTransitionTypes() {
26
- return (
27
- typeof ViewTransition !== "undefined" && "types" in ViewTransition.prototype
28
- )
29
- }
30
-
31
- /**
32
- * @param {number} index - The index of the entry now current.
33
- * @param {number} previousIndex - The index of the entry before this render.
34
- * @returns {ViewTransitionType}
35
- */
36
- function getTransitionType(index, previousIndex) {
37
- if (index < previousIndex) return "back"
38
- if (index > previousIndex) return "forward"
39
- return "replace"
40
- }
41
-
42
7
  /**
43
8
  * Wraps a store so that the renders which follow a navigation are run inside a
44
9
  * view transition, tagged with the direction they navigated in. Pair it with
@@ -57,11 +22,28 @@ function getTransitionType(index, previousIndex) {
57
22
  *
58
23
  * mount(withTransitions(store), renderApp, document.getElementById("root"))
59
24
  *
25
+ * Pass `routes` to choose which navigations animate; the rest change page
26
+ * without a transition. A list of route names animates every navigation into
27
+ * or out of one of them. A function receives `{ type, from, to }`, where `type`
28
+ * is the direction read from the history and `from` / `to` carry the `path`,
29
+ * `index` and `route` of each side, and returns whether to animate.
30
+ *
31
+ * @example
32
+ * // Slide into and out of detail pages; switch tabs instantly.
33
+ * withTransitions(store, { routes: ["eventDetail", "userDetail"] })
34
+ *
35
+ * @example
36
+ * // Slide only when going deeper or coming back up, never sideways.
37
+ * withTransitions(store, {
38
+ * routes: ({ from, to }) => from.path.startsWith(to.path) || to.path.startsWith(from.path),
39
+ * })
40
+ *
60
41
  * @param {Store} store - The store to wrap.
42
+ * @param {TransitionsOptions} [options]
61
43
  * @returns {Store} The same store, with a wrapped `subscribe`.
62
44
  * @throws {Error} If the store has no `Router` type.
63
45
  */
64
- export function withTransitions(store) {
46
+ export function withTransitions(store, { routes } = {}) {
65
47
  if (!store.getType("Router")) {
66
48
  throw new Error(
67
49
  '[withTransitions] Requires the "Router" type from @inglorious/web. The ' +
@@ -71,38 +53,60 @@ export function withTransitions(store) {
71
53
  }
72
54
 
73
55
  const subscribe = store.subscribe
56
+ const shouldTransition = toPredicate(routes)
74
57
  const listeners = new Set()
75
58
 
76
59
  let currentPath = ""
77
60
  let currentIndex = 0
61
+ let currentRoute
78
62
  let unsubscribeStore = null
79
63
 
80
- /* The update callback of a view transition does not run synchronously: the
81
- * browser captures the old state first and calls it on a later task. Reading
82
- * the state here rather than in the subscription keeps a listener from
83
- * replaying whatever the store held when the transition started. */
84
- function notify() {
85
- const state = store.getState()
86
- // Copied, so that a listener subscribing during this pass waits for the
87
- // next one instead of being notified by the pass that added it.
88
- ;[...listeners].forEach((listener) => listener(state))
64
+ store.subscribe = (listener) => {
65
+ /* Taken here rather than when the store is wrapped, so that anything that
66
+ navigated in between — an initial redirect, a renderer mounting late — is
67
+ part of the baseline instead of a navigation in its own right. It also
68
+ keeps `window` untouched until something subscribes, so wrapping a store
69
+ during a server render is harmless. */
70
+ if (!unsubscribeStore) {
71
+ currentPath = getPath()
72
+ currentIndex = getIndex()
73
+ currentRoute = getRoute(store.getState())
74
+ unsubscribeStore = subscribe.call(store, onStateChange)
75
+ }
76
+
77
+ listeners.add(listener)
78
+
79
+ return () => {
80
+ listeners.delete(listener)
81
+
82
+ if (!listeners.size) {
83
+ unsubscribeStore?.()
84
+ unsubscribeStore = null
85
+ }
86
+ }
89
87
  }
90
88
 
91
89
  function onStateChange() {
92
90
  const path = getPath()
93
91
  const index = getIndex()
92
+ const route = getRoute(store.getState())
94
93
  /* Both are compared against the baseline before it moves forward,
95
94
  * otherwise the comparison would always be against itself and every
96
95
  * navigation would read as a replacement. */
97
96
  const hasNavigated = path !== currentPath
98
- const previousIndex = currentIndex
97
+ const from = { path: currentPath, index: currentIndex, route: currentRoute }
98
+ const to = { path, index, route }
99
99
 
100
100
  currentPath = path
101
101
  currentIndex = index
102
+ currentRoute = route
102
103
 
103
104
  if (!hasNavigated || !supportsViewTransitions()) return notify()
104
105
 
105
- const types = [getTransitionType(index, previousIndex)]
106
+ const type = getTransitionType(to.index, from.index)
107
+ if (!shouldTransition({ type, from, to })) return notify()
108
+
109
+ const types = [type]
106
110
 
107
111
  if (supportsTransitionTypes()) {
108
112
  document.startViewTransition({ update: notify, types })
@@ -113,29 +117,67 @@ export function withTransitions(store) {
113
117
  }
114
118
  }
115
119
 
116
- store.subscribe = (listener) => {
117
- /* Taken here rather than when the store is wrapped, so that anything that
118
- navigated in between — an initial redirect, a renderer mounting late — is
119
- part of the baseline instead of a navigation in its own right. It also
120
- keeps `window` untouched until something subscribes, so wrapping a store
121
- during a server render is harmless. */
122
- if (!unsubscribeStore) {
123
- currentPath = getPath()
124
- currentIndex = getIndex()
125
- unsubscribeStore = subscribe.call(store, onStateChange)
126
- }
120
+ /* The update callback of a view transition does not run synchronously: the
121
+ * browser captures the old state first and calls it on a later task. Reading
122
+ * the state here rather than in the subscription keeps a listener from
123
+ * replaying whatever the store held when the transition started. */
124
+ function notify() {
125
+ const state = store.getState()
126
+ // Copied, so that a listener subscribing during this pass waits for the
127
+ // next one instead of being notified by the pass that added it.
128
+ ;[...listeners].forEach((listener) => listener(state))
129
+ }
127
130
 
128
- listeners.add(listener)
131
+ return store
132
+ }
129
133
 
130
- return () => {
131
- listeners.delete(listener)
134
+ /** `routes` as a predicate over a navigation. Without it every navigation
135
+ * animates. */
136
+ function toPredicate(routes) {
137
+ if (!routes) return () => true
138
+ if (typeof routes === "function") return routes
132
139
 
133
- if (!listeners.size) {
134
- unsubscribeStore?.()
135
- unsubscribeStore = null
136
- }
137
- }
138
- }
140
+ const names = new Set(routes)
141
+ return ({ from, to }) => names.has(from.route) || names.has(to.route)
142
+ }
139
143
 
140
- return store
144
+ /** The path the router matches against. Hashes are deliberately excluded: they
145
+ * change the fragment, not the page, so they must not animate one. */
146
+ function getPath() {
147
+ return window.location.pathname + window.location.search
148
+ }
149
+
150
+ /** The position of the current entry in the session history, stamped by the
151
+ * Router as it navigates. Comparing it to the previous one is what tells a push
152
+ * from a traversal, and in which direction. */
153
+ function getIndex() {
154
+ return window.history.state?.idx ?? 0
155
+ }
156
+
157
+ /** The route the Router entity resolved, whatever the entity is called. */
158
+ function getRoute(state) {
159
+ return Object.values(state).find((entity) => entity?.type === "Router")?.route
160
+ }
161
+
162
+ function supportsViewTransitions() {
163
+ return typeof document.startViewTransition === "function"
164
+ }
165
+
166
+ /**
167
+ * @param {number} index - The index of the entry now current.
168
+ * @param {number} previousIndex - The index of the entry before this render.
169
+ * @returns {ViewTransitionType}
170
+ */
171
+ function getTransitionType(index, previousIndex) {
172
+ if (index < previousIndex) return "back"
173
+ if (index > previousIndex) return "forward"
174
+ return "replace"
175
+ }
176
+
177
+ /** `types` landed after `startViewTransition` itself, so an engine can have the
178
+ * method and still throw when handed the object form of the argument. */
179
+ function supportsTransitionTypes() {
180
+ return (
181
+ typeof ViewTransition !== "undefined" && "types" in ViewTransition.prototype
182
+ )
141
183
  }
@@ -10,8 +10,33 @@ export interface TransitionsStore<S = unknown> {
10
10
  getType(name: string): unknown
11
11
  }
12
12
 
13
+ /** One side of a navigation. */
14
+ export interface NavigationSide {
15
+ path: string
16
+ /** The history index the Router stamped on the entry. */
17
+ index: number
18
+ /** The route the Router resolved, if any. */
19
+ route?: string
20
+ }
21
+
22
+ export interface Navigation {
23
+ /** The direction read from the history. */
24
+ type: ViewTransitionType
25
+ from: NavigationSide
26
+ to: NavigationSide
27
+ }
28
+
29
+ export interface TransitionsOptions {
30
+ /** Which navigations animate: those into or out of the named routes, or
31
+ * those the function accepts. Omit it to animate every navigation. */
32
+ routes?: string[] | ((navigation: Navigation) => boolean)
33
+ }
34
+
13
35
  export interface WithTransitions {
14
- <S>(store: TransitionsStore<S>): TransitionsStore<S>
36
+ <S>(
37
+ store: TransitionsStore<S>,
38
+ options?: TransitionsOptions,
39
+ ): TransitionsStore<S>
15
40
  }
16
41
 
17
42
  export declare const withTransitions: WithTransitions