add gs
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_cryo"]["cryo"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_otter"]["otter"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_roact"]["roact"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
--[[
|
||||
Package link auto-generated by Rotriever
|
||||
]]
|
||||
local PackageIndex = script.Parent.Parent
|
||||
|
||||
local package = PackageIndex["roblox_state-table"]["state-table"]
|
||||
|
||||
if package.ClassName == "ModuleScript" then
|
||||
return require(package)
|
||||
end
|
||||
|
||||
return package
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/roact-navigation"
|
||||
version = "0.2.8"
|
||||
commit = "6b5c6975e75ab68048a12f6f3b84ab4061afe496"
|
||||
source = "url+https://github.com/roblox/roact-navigation"
|
||||
dependencies = [
|
||||
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
|
||||
"Otter roblox/otter 0.1.2 url+https://github.com/roblox/otter",
|
||||
"Roact roblox/roact 1.3.0 url+https://github.com/roblox/roact",
|
||||
"StateTable roblox/state-table 0.1.0 url+https://github.com/roblox/state-table",
|
||||
]
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local NONE_TOKEN = NavigationSymbol("NONE")
|
||||
local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE")
|
||||
local ORDER_TOKEN = NavigationSymbol("ORDER")
|
||||
local HISTORY_TOKEN = NavigationSymbol("HISTORY")
|
||||
|
||||
--[[
|
||||
BackBehavior provides shared constants that are used to configure back
|
||||
action styles for different navigators. Note that not all routers support
|
||||
all BackBehaviors and they will fall back to appropriate defaults for
|
||||
those cases.
|
||||
]]
|
||||
local BackBehavior = {
|
||||
None = NONE_TOKEN,
|
||||
InitialRoute = INITIAL_ROUTE_TOKEN,
|
||||
Order = ORDER_TOKEN,
|
||||
History = HISTORY_TOKEN,
|
||||
}
|
||||
|
||||
return BackBehavior
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local BACK_TOKEN = NavigationSymbol("BACK")
|
||||
local INIT_TOKEN = NavigationSymbol("INIT")
|
||||
local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE")
|
||||
local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS")
|
||||
local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION")
|
||||
|
||||
--[[
|
||||
NavigationActions provides shared constants and methods to construct
|
||||
actions that are dispatched to routers to cause a change in the route.
|
||||
]]
|
||||
local NavigationActions = {
|
||||
Back = BACK_TOKEN,
|
||||
Init = INIT_TOKEN,
|
||||
Navigate = NAVIGATE_TOKEN,
|
||||
SetParams = SET_PARAMS_TOKEN,
|
||||
CompleteTransition = COMPLETE_TRANSITION_TOKEN,
|
||||
}
|
||||
|
||||
NavigationActions.__index = NavigationActions
|
||||
|
||||
-- Navigate back in the history (temporally).
|
||||
function NavigationActions.back(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = BACK_TOKEN,
|
||||
key = data.key,
|
||||
immediate = data.immediate,
|
||||
}
|
||||
end
|
||||
|
||||
-- Initialize the navigation history if not already defined.
|
||||
function NavigationActions.init(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = INIT_TOKEN,
|
||||
params = data.params,
|
||||
}
|
||||
end
|
||||
|
||||
-- Navigate to an existing or new route.
|
||||
function NavigationActions.navigate(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = NAVIGATE_TOKEN,
|
||||
routeName = data.routeName,
|
||||
params = data.params,
|
||||
action = data.action,
|
||||
key = data.key,
|
||||
}
|
||||
end
|
||||
|
||||
-- Swap out the params for an existing route, matched by the given key.
|
||||
function NavigationActions.setParams(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = SET_PARAMS_TOKEN,
|
||||
key = data.key,
|
||||
params = data.params,
|
||||
}
|
||||
end
|
||||
|
||||
-- For internal use. Triggers completion of a transition animation, if needed by the router.
|
||||
-- This would be sent on e.g. didMount of the new page, so the router knows that the new screen
|
||||
-- is ready to be displayed before it animates it in place.
|
||||
function NavigationActions.completeTransition(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = COMPLETE_TRANSITION_TOKEN,
|
||||
key = data.key,
|
||||
toChildKey = data.toChildKey,
|
||||
}
|
||||
end
|
||||
|
||||
return NavigationActions
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS")
|
||||
local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS")
|
||||
local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR")
|
||||
local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR")
|
||||
local ACTION_TOKEN = NavigationSymbol("ACTION")
|
||||
local REFOCUS_TOKEN = NavigationSymbol("REFOCUS")
|
||||
|
||||
--[[
|
||||
NavigationEvents provides shared constants that are used to register
|
||||
listeners for different RoactNavigation UI state changes.
|
||||
]]
|
||||
return {
|
||||
WillFocus = WILL_FOCUS_TOKEN,
|
||||
DidFocus = DID_FOCUS_TOKEN,
|
||||
WillBlur = WILL_BLUR_TOKEN,
|
||||
DidBlur = DID_BLUR_TOKEN,
|
||||
Action = ACTION_TOKEN,
|
||||
Refocus = REFOCUS_TOKEN,
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
-- Taken from Roact.Symbol and modified to produce exact string names
|
||||
-- to allow for serialization/pathing.
|
||||
|
||||
return function (name)
|
||||
assert(type(name) == "string", "Symbols must be created using a string name!")
|
||||
|
||||
local self = newproxy(true)
|
||||
|
||||
-- Unlike Symbols in Roact, we need the exact names.
|
||||
getmetatable(self).__tostring = function()
|
||||
return name
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
--[[
|
||||
RoactNavigation.None allows us to declare that certain values should be explicitly
|
||||
removed from navigation props, e.g. for stack navigation options where we want to
|
||||
remove the default header from a screen when drawing in a stack navigator with
|
||||
headerMode == StackHeaderMode.Screen.
|
||||
]]
|
||||
local NONE_SYMBOL = NavigationSymbol("NONE")
|
||||
|
||||
return NONE_SYMBOL
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local POP_TOKEN = NavigationSymbol("POP")
|
||||
local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP")
|
||||
local PUSH_TOKEN = NavigationSymbol("PUSH")
|
||||
local RESET_TOKEN = NavigationSymbol("RESET")
|
||||
local REPLACE_TOKEN = NavigationSymbol("REPLACE")
|
||||
|
||||
--[[
|
||||
StackActions provides shared constants and methods to construct
|
||||
actions that are dispatched to routers to cause a change in the route.
|
||||
These actions are specific to Stack navigation. See NavigationActions
|
||||
if you need to use more general APIs.
|
||||
]]
|
||||
local StackActions = {
|
||||
Pop = POP_TOKEN,
|
||||
PopToTop = POP_TO_TOP_TOKEN,
|
||||
Push = PUSH_TOKEN,
|
||||
Reset = RESET_TOKEN,
|
||||
Replace = REPLACE_TOKEN,
|
||||
}
|
||||
|
||||
StackActions.__index = StackActions
|
||||
|
||||
-- Pop the top-most item off the route stack, if any.
|
||||
function StackActions.pop(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = POP_TOKEN,
|
||||
n = data.n,
|
||||
}
|
||||
end
|
||||
|
||||
-- Pop all the items except the last one off the route stack.
|
||||
function StackActions.popToTop(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = POP_TO_TOP_TOKEN,
|
||||
key = data.key,
|
||||
}
|
||||
end
|
||||
|
||||
-- Push a new item onto the route stack.
|
||||
function StackActions.push(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = PUSH_TOKEN,
|
||||
routeName = data.routeName,
|
||||
params = data.params,
|
||||
action = data.action,
|
||||
}
|
||||
end
|
||||
|
||||
-- Reset the route stack and replace it with a new stack,
|
||||
-- specified by a list of actions to be applied.
|
||||
function StackActions.reset(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = RESET_TOKEN,
|
||||
index = data.index,
|
||||
actions = data.actions,
|
||||
key = data.key,
|
||||
}
|
||||
end
|
||||
|
||||
-- Replace the route for the given key with a new route.
|
||||
function StackActions.replace(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = REPLACE_TOKEN,
|
||||
key = data.key,
|
||||
newKey = data.newKey,
|
||||
routeName = data.routeName,
|
||||
params = data.params,
|
||||
action = data.action,
|
||||
immediate = data.immediate,
|
||||
}
|
||||
end
|
||||
|
||||
return StackActions
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
|
||||
--[[
|
||||
StateUtils provides utilities to read and write standard route data.
|
||||
Routes have the following general structure:
|
||||
{
|
||||
index = <required integer: current active route in list>,
|
||||
routes = [
|
||||
{
|
||||
routeName = <required string: navigation name for route>,
|
||||
key = <unique string: identifier for this route>,
|
||||
params = <optional dictionary: params for screen>,
|
||||
action = <optional dictionary: sub-action to be run by child routers>,
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
This structure is independent of the notion of stack, tab, drawer, or any
|
||||
other kind of navigation. It simply represents a list of pages and their
|
||||
parameters. Different kinds of routers can treat the data in their own way.
|
||||
]]
|
||||
local StateUtils = {}
|
||||
StateUtils.__index = StateUtils
|
||||
|
||||
-- Get the route matching the given key. Returns nil if no match is found.
|
||||
function StateUtils.get(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for _, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return route
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get the route at the given index. Returns nil if no match is found.
|
||||
function StateUtils.getAtIndex(state, index)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(index) == "number", "index must be a number")
|
||||
assert(index >= 0, "index must be non-negative")
|
||||
|
||||
return state.routes[index]
|
||||
end
|
||||
|
||||
-- Get the active route from state. Returns nil if no routes.
|
||||
function StateUtils.getActiveRoute(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
local index = state.index
|
||||
if index <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return state.routes[index]
|
||||
end
|
||||
|
||||
-- Get the index of the route matching the given key. Returns nil if no match is found.
|
||||
function StateUtils.indexOf(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for index, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return index
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Returns true if a route exists matching the given key, false otherwise.
|
||||
function StateUtils.has(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for _, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Push a new route into the navigation state. Makes the pushed route active.
|
||||
function StateUtils.push(state, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
assert(StateUtils.indexOf(state, route.key) == nil,
|
||||
string.format("route with key '%s' already exists", route.key))
|
||||
|
||||
local routes = Cryo.List.join(state.routes, { route })
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = #routes,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Pop the top-most route from the navigation state (NOT the active route).
|
||||
-- Makes the new top-most route active.
|
||||
function StateUtils.pop(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
if #state.routes == 0 then
|
||||
-- NOTE: Popping empty state is a no-op
|
||||
return state
|
||||
end
|
||||
|
||||
local routes = Cryo.List.removeIndex(state.routes, #state.routes)
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = #routes,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Sets the active route to match the given index.
|
||||
function StateUtils.jumpToIndex(state, index)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(index) == "number", "index must be a number")
|
||||
|
||||
if index == state.index then
|
||||
return state
|
||||
end
|
||||
|
||||
assert(state.routes[index] ~= nil,
|
||||
string.format("cannot jump to out-of-range index '%d'", index))
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
})
|
||||
end
|
||||
|
||||
-- Sets the active route to match the given key.
|
||||
function StateUtils.jumpTo(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Sets the active route to the previous route in the list.
|
||||
function StateUtils.back(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
local index = state.index - 1
|
||||
if not state.routes[index] then
|
||||
return state
|
||||
end
|
||||
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Sets the active route to the next route in the list.
|
||||
function StateUtils.forward(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
local index = state.index + 1
|
||||
if not state.routes[index] then
|
||||
return state
|
||||
end
|
||||
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Replace the route matching the given key. Sets the active route to the
|
||||
-- newly replaced entry. Prunes the old entries that follow the replaced one.
|
||||
function StateUtils.replaceAndPrune(state, key, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
local replaced = StateUtils.replaceAtIndex(state, index, route)
|
||||
|
||||
return Cryo.Dictionary.join(replaced, {
|
||||
routes = { unpack(replaced.routes, 1, index) }
|
||||
})
|
||||
end
|
||||
|
||||
-- Replace the route matching the given key without pruning the following routes.
|
||||
-- The active route will be updated to match the newly replaced one unless
|
||||
-- preserveIndex is true.
|
||||
function StateUtils.replaceAt(state, key, route, preserveIndex)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
assert(preserveIndex == nil or type(preserveIndex) == "boolean",
|
||||
"preserveIndex must be nil or a boolean")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
local nextIndex = preserveIndex and state.index or index
|
||||
local nextState = StateUtils.replaceAtIndex(state, index, route)
|
||||
nextState.index = nextIndex
|
||||
return nextState
|
||||
end
|
||||
|
||||
-- Replace the route at the given index. Updates the active route to point to
|
||||
-- the replaced entry.
|
||||
function StateUtils.replaceAtIndex(state, index, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(index) == "number", "index must be a number")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
assert(state.routes[index] ~= nil,
|
||||
string.format("index '%d' does not exist in route '%s'", index, route.key))
|
||||
|
||||
if state.routes[index] == route and index == state.index then
|
||||
return state
|
||||
end
|
||||
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[index] = route
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Wipe away the existing routes and replace them with new routes.
|
||||
-- Sets the active route to the provided index (if provided), otherwise
|
||||
-- sets the active route to the last one in the list.
|
||||
function StateUtils.reset(state, routes, index)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(routes) == "table" and #routes > 0,
|
||||
"routes must be a list with at least one element")
|
||||
assert(index == nil or type(index) == "number",
|
||||
"index must be a number or nil")
|
||||
|
||||
local nextIndex = not index and #routes or index
|
||||
|
||||
-- Bail out without replacing IFF index and routes all match
|
||||
if #state.routes == #routes and state.index == nextIndex then
|
||||
local routesAreEqual = true
|
||||
for i = 1, #routes, 1 do
|
||||
if state.routes[i] ~= routes[i] then
|
||||
routesAreEqual = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if routesAreEqual then
|
||||
return state
|
||||
end
|
||||
end
|
||||
|
||||
assert(routes[nextIndex] ~= nil,
|
||||
string.format("cannot reset index '%d' that does not exist", nextIndex))
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = nextIndex,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
return StateUtils
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
return function()
|
||||
local BackBehavior = require(script.Parent.Parent.BackBehavior)
|
||||
|
||||
describe("BackBehavior token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(BackBehavior.None).to.equal(BackBehavior.None)
|
||||
expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute)
|
||||
expect(BackBehavior.Order).to.equal(BackBehavior.Order)
|
||||
expect(BackBehavior.History).to.equal(BackBehavior.History)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(BackBehavior.None)).to.equal("NONE")
|
||||
expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE")
|
||||
expect(tostring(BackBehavior.Order)).to.equal("ORDER")
|
||||
expect(tostring(BackBehavior.History)).to.equal("HISTORY")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
return function()
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
|
||||
describe("NavigationActions token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(NavigationActions.Back).to.equal(NavigationActions.Back)
|
||||
expect(NavigationActions.Init).to.equal(NavigationActions.Init)
|
||||
expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate)
|
||||
expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams)
|
||||
expect(NavigationActions.CompleteTransition).to.equal(NavigationActions.CompleteTransition)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(NavigationActions.Back)).to.equal("BACK")
|
||||
expect(tostring(NavigationActions.Init)).to.equal("INIT")
|
||||
expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE")
|
||||
expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS")
|
||||
expect(tostring(NavigationActions.CompleteTransition)).to.equal("COMPLETE_TRANSITION")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("NavigationActions function tests", function()
|
||||
it("should return a back action with matching data for a call to back()", function()
|
||||
local backTable = NavigationActions.back({
|
||||
key = "the_key",
|
||||
immediate = true,
|
||||
})
|
||||
|
||||
expect(backTable.type).to.equal(NavigationActions.Back)
|
||||
expect(backTable.key).to.equal("the_key")
|
||||
expect(backTable.immediate).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return an init action with matching data for call to init()", function()
|
||||
local initTable = NavigationActions.init({
|
||||
params = "foo",
|
||||
})
|
||||
|
||||
expect(initTable.type).to.equal(NavigationActions.Init)
|
||||
expect(initTable.params).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return a navigate action with matching data for call to navigate()", function()
|
||||
local navigateTable = NavigationActions.navigate({
|
||||
routeName = "routeName",
|
||||
params = "foo",
|
||||
action = "action",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(navigateTable.type).to.equal(NavigationActions.Navigate)
|
||||
expect(navigateTable.routeName).to.equal("routeName")
|
||||
expect(navigateTable.params).to.equal("foo")
|
||||
expect(navigateTable.action).to.equal("action")
|
||||
expect(navigateTable.key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should return a set params action with matching data for call to setParams()", function()
|
||||
local setParamsTable = NavigationActions.setParams({
|
||||
key = "key",
|
||||
params = "foo",
|
||||
})
|
||||
|
||||
expect(setParamsTable.type).to.equal(NavigationActions.SetParams)
|
||||
expect(setParamsTable.key).to.equal("key")
|
||||
expect(setParamsTable.params).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return a complete transition action with matching data for call to completeTransition()", function()
|
||||
local completeTransitionTable = NavigationActions.completeTransition({
|
||||
key = "key",
|
||||
toChildKey = "toChildKey",
|
||||
})
|
||||
|
||||
expect(completeTransitionTable.type).to.equal(NavigationActions.CompleteTransition)
|
||||
expect(completeTransitionTable.key).to.equal("key")
|
||||
expect(completeTransitionTable.toChildKey).to.equal("toChildKey")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
return function()
|
||||
local NavigationEvents = require(script.Parent.Parent.NavigationEvents)
|
||||
|
||||
describe("NavigationEvents token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(NavigationEvents.WillFocus).to.equal(NavigationEvents.WillFocus)
|
||||
expect(NavigationEvents.DidFocus).to.equal(NavigationEvents.DidFocus)
|
||||
expect(NavigationEvents.WillBlur).to.equal(NavigationEvents.WillBlur)
|
||||
expect(NavigationEvents.DidBlur).to.equal(NavigationEvents.DidBlur)
|
||||
expect(NavigationEvents.Action).to.equal(NavigationEvents.Action)
|
||||
expect(NavigationEvents.Refocus).to.equal(NavigationEvents.Refocus)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(NavigationEvents.WillFocus)).to.equal("WILL_FOCUS")
|
||||
expect(tostring(NavigationEvents.DidFocus)).to.equal("DID_FOCUS")
|
||||
expect(tostring(NavigationEvents.WillBlur)).to.equal("WILL_BLUR")
|
||||
expect(tostring(NavigationEvents.DidBlur)).to.equal("DID_BLUR")
|
||||
expect(tostring(NavigationEvents.Action)).to.equal("ACTION")
|
||||
expect(tostring(NavigationEvents.Refocus)).to.equal("REFOCUS")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
return function()
|
||||
local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol)
|
||||
|
||||
it("should give an opaque object", function()
|
||||
local symbol = NavigationSymbol("foo")
|
||||
|
||||
expect(symbol).to.be.a("userdata")
|
||||
end)
|
||||
|
||||
it("should coerce to the given name", function()
|
||||
local symbol = NavigationSymbol("foo")
|
||||
|
||||
expect(tostring(symbol)).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should be unique when constructed", function()
|
||||
local symbolA = NavigationSymbol("abc")
|
||||
local symbolB = NavigationSymbol("abc")
|
||||
|
||||
expect(symbolA).never.to.equal(symbolB)
|
||||
end)
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
return function()
|
||||
local NoneSymbol = require(script.Parent.Parent.NoneSymbol)
|
||||
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(NoneSymbol).to.equal(NoneSymbol)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(NoneSymbol)).to.equal("NONE")
|
||||
end)
|
||||
end
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
return function()
|
||||
local StackActions = require(script.Parent.Parent.StackActions)
|
||||
|
||||
describe("StackActions token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(StackActions.Pop).to.equal(StackActions.Pop)
|
||||
expect(StackActions.PopToTop).to.equal(StackActions.PopToTop)
|
||||
expect(StackActions.Push).to.equal(StackActions.Push)
|
||||
expect(StackActions.Reset).to.equal(StackActions.Reset)
|
||||
expect(StackActions.Replace).to.equal(StackActions.Replace)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(StackActions.Pop)).to.equal("POP")
|
||||
expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP")
|
||||
expect(tostring(StackActions.Push)).to.equal("PUSH")
|
||||
expect(tostring(StackActions.Reset)).to.equal("RESET")
|
||||
expect(tostring(StackActions.Replace)).to.equal("REPLACE")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StackActions function tests", function()
|
||||
it("should return a pop action for pop()", function()
|
||||
local popTable = StackActions.pop({
|
||||
n = "n",
|
||||
})
|
||||
|
||||
expect(popTable.type).to.equal(StackActions.Pop)
|
||||
expect(popTable.n).to.equal("n")
|
||||
end)
|
||||
|
||||
it("should return a pop to top action for popToTop()", function()
|
||||
local popToTopTable = StackActions.popToTop()
|
||||
|
||||
expect(popToTopTable.type).to.equal(StackActions.PopToTop)
|
||||
end)
|
||||
|
||||
it("should return a push action for push()", function()
|
||||
local pushTable = StackActions.push({
|
||||
routeName = "routeName",
|
||||
params = "params",
|
||||
action = "action",
|
||||
})
|
||||
|
||||
expect(pushTable.type).to.equal(StackActions.Push)
|
||||
expect(pushTable.routeName).to.equal("routeName")
|
||||
expect(pushTable.params).to.equal("params")
|
||||
expect(pushTable.action).to.equal("action")
|
||||
end)
|
||||
|
||||
it("should return a reset action for reset()", function()
|
||||
local resetTable = StackActions.reset({
|
||||
index = "index",
|
||||
actions = "actions",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(resetTable.type).to.equal(StackActions.Reset)
|
||||
expect(resetTable.index).to.equal("index")
|
||||
expect(resetTable.key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should return a replace action for replace()", function()
|
||||
local replaceTable = StackActions.replace({
|
||||
key = "key",
|
||||
newKey = "newKey",
|
||||
routeName = "routeName",
|
||||
params = "params",
|
||||
action = "action",
|
||||
immediate = "immediate",
|
||||
})
|
||||
|
||||
expect(replaceTable.type).to.equal(StackActions.Replace)
|
||||
expect(replaceTable.key).to.equal("key")
|
||||
expect(replaceTable.newKey).to.equal("newKey")
|
||||
expect(replaceTable.routeName).to.equal("routeName")
|
||||
expect(replaceTable.params).to.equal("params")
|
||||
expect(replaceTable.action).to.equal("action")
|
||||
expect(replaceTable.immediate).to.equal("immediate")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+725
@@ -0,0 +1,725 @@
|
||||
return function()
|
||||
local StateUtils = require(script.Parent.Parent.StateUtils)
|
||||
|
||||
describe("StateUtils.get tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.get(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.get({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if key is not found in routes", function()
|
||||
local result = StateUtils.get({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
},
|
||||
},
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return route if key is found in routes", function()
|
||||
local result = StateUtils.get({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
},
|
||||
}, "foo-1")
|
||||
|
||||
expect(result.routeName).to.equal("foo")
|
||||
expect(result.key).to.equal("foo-1")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.getAtIndex tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.getAtIndex(nil, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is negative", function()
|
||||
expect(function()
|
||||
StateUtils.getAtIndex({}, -1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if index is not found", function()
|
||||
local result = StateUtils.getAtIndex({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo1",
|
||||
key = "foo-1",
|
||||
},
|
||||
{
|
||||
routeName = "foo2",
|
||||
key = "foo-2",
|
||||
}
|
||||
}
|
||||
}, 5)
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return a matching route", function()
|
||||
local result = StateUtils.getAtIndex({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo1",
|
||||
key = "foo-1",
|
||||
},
|
||||
{
|
||||
routeName = "foo2",
|
||||
key = "foo-2",
|
||||
}
|
||||
}
|
||||
}, 2)
|
||||
|
||||
expect(result.routeName).to.equal("foo2")
|
||||
expect(result.key).to.equal("foo-2")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.getActiveRoute tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.getActiveRoute(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if no routes", function()
|
||||
local result = StateUtils.getActiveRoute({
|
||||
index = 0,
|
||||
routes = {},
|
||||
})
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return active route", function()
|
||||
local result = StateUtils.getActiveRoute({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "active",
|
||||
key = "active-1",
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.routeName).to.equal("active")
|
||||
expect(result.key).to.equal("active-1")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.indexOf tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.indexOf(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.indexOf({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if key is not found in routes", function()
|
||||
local result = StateUtils.indexOf({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
},
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return index if key is found in routes", function()
|
||||
local result = StateUtils.indexOf({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
},
|
||||
{
|
||||
routeName = "foo2",
|
||||
key = "foo-2",
|
||||
}
|
||||
},
|
||||
}, "foo-2")
|
||||
|
||||
expect(result).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.has tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.has(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.has({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return false if key is not in routes", function()
|
||||
local result = StateUtils.has({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return true if key is found in routes", function()
|
||||
local result = StateUtils.has({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, "foo-1")
|
||||
|
||||
expect(result).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.push tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.push(nil, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.push({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route.key is already present", function()
|
||||
expect(function()
|
||||
StateUtils.push({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, {
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should insert new route if it doesn't exist", function()
|
||||
local newState = StateUtils.push({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "first",
|
||||
key = "foo-1",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
routeName = "second",
|
||||
key = "foo-2",
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[newState.index].key).to.equal("foo-2")
|
||||
expect(newState.routes[newState.index].routeName).to.equal("second")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.pop tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.pop(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return existing state if routes is empty", function()
|
||||
local initialState = {
|
||||
index = 0,
|
||||
routes = {},
|
||||
}
|
||||
|
||||
local newState = StateUtils.pop(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should return empty state if popping one route", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "route-1", },
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.pop(initialState)
|
||||
expect(newState.index).to.equal(0)
|
||||
expect(#newState.routes).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should remove top route if popping with more than one route", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "route-1", },
|
||||
{ routeName = "route", key = "route-2", },
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.pop(initialState)
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].key).to.equal("route-1")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.jumpToIndex tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex(nil, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex({}, "foo")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index does not match a route", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex({
|
||||
index = 1,
|
||||
routes = { { routeName = "first", key = "first-1" } }
|
||||
}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if index matches current", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { { routeName = "one", key = "1" } }
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpToIndex(initialState, 1)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should return updated state if index differs", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "route-1" },
|
||||
{ routeName = "route", key = "route-2" },
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpToIndex(initialState, 2)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.jumpTo tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.jumpTo(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.jumpTo({}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if key is already active route", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpTo(initialState, "key-1")
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should return state with new active route if key is not active", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpTo(initialState, "key-2")
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.back tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.back(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if route for new index does not exist", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.back(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should remove top state if there is somewhere to go", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.back(initialState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.forward tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.forward(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should not walk off the end of the route list", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.forward(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should move to next route if available", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.forward(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAndPrune tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune(nil, "key", {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune({}, 0, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune({}, "key", 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should replace matching route and prune following routes", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAndPrune(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAt tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt(nil, "key", {}, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, 0, {}, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, "key", 0, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if preserveIndex is not a boolean", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, "key", {}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should replace matching route, not prune, and update index", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAt(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
}, false)
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
|
||||
it("should replace matching route, not prune, and preserve existing index", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAt(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
}, true)
|
||||
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAtIndex tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex(nil, 0, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({}, nil, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({}, 5, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index does not exist", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({
|
||||
index = 0,
|
||||
routes = {}
|
||||
}, 5, { routeName = "name", key = "key" })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if inputs are same", function()
|
||||
local testRoute = { routeName = "name", key = "key" }
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { testRoute },
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should replace route at index if route is not equal", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "name", key = "key" }
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 1, {
|
||||
routeName = "newName",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].routeName).to.equal("newName")
|
||||
expect(newState.routes[1].key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should update index, if new index differs but route does not", function()
|
||||
local testRoute = { routeName = "name", key = "key-2" }
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "name", key = "key-1" },
|
||||
testRoute,
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute)
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.reset tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.reset(nil, {}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if routes is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, nil, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, {}, "foo")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should NOT assert if index is nil", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if index matches and all routes are same objects", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
route2,
|
||||
}, 2)
|
||||
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should update state if index is not specified and old index is not last route", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
route2,
|
||||
})
|
||||
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should update state if index matches but routes differ", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
{ routeName = "route3", key = "route-3" },
|
||||
}, 1)
|
||||
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(newState.routes[2].routeName).to.equal("route3")
|
||||
expect(newState.routes[2].key).to.equal("route-3")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local createAppContainer = require(script.Parent.Parent.createAppContainer)
|
||||
local createSwitchNavigator = require(script.Parent.Parent.navigators.createSwitchNavigator)
|
||||
|
||||
it("should be a function", function()
|
||||
expect(type(createAppContainer)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a valid component when mounting a switch navigator", function()
|
||||
local TestNavigator = createSwitchNavigator({
|
||||
routes = {
|
||||
Foo = function() end,
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local TestApp = createAppContainer(TestNavigator)
|
||||
local element = Roact.createElement(TestApp)
|
||||
local instance = Roact.mount(element)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should throw when navigator has both navigation and container props", function()
|
||||
local TestAppComponent = Roact.Component:extend("TestAppComponent")
|
||||
TestAppComponent.router = {}
|
||||
function TestAppComponent:render() end
|
||||
|
||||
local element = Roact.createElement(createAppContainer(TestAppComponent), {
|
||||
navigation = {},
|
||||
somePropThatShouldNotBeHere = true,
|
||||
})
|
||||
|
||||
local status, err = pcall(function()
|
||||
Roact.mount(element)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when not passed a table for AppComponent", function()
|
||||
local TestAppComponent = 5
|
||||
|
||||
local status, err = pcall(function()
|
||||
createAppContainer(TestAppComponent)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " ..
|
||||
"component with a 'router' field")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when passed a stateful component without router field", function()
|
||||
local TestAppComponent = Roact.Component:extend("TestAppComponent")
|
||||
|
||||
local status, err = pcall(function()
|
||||
createAppContainer(TestAppComponent)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " ..
|
||||
"component with a 'router' field")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should accept actions from externalDispatchConnector", function()
|
||||
local TestNavigator = createSwitchNavigator({
|
||||
routes = {
|
||||
Foo = function() end,
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local registeredCallback = nil
|
||||
local externalDispatchConnector = function(rnCallback)
|
||||
registeredCallback = rnCallback
|
||||
return function()
|
||||
registeredCallback = nil
|
||||
end
|
||||
end
|
||||
|
||||
local element = Roact.createElement(createAppContainer(TestNavigator), {
|
||||
externalDispatchConnector = externalDispatchConnector,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
expect(type(registeredCallback)).to.equal("function")
|
||||
|
||||
-- Make sure it processes action
|
||||
local result = registeredCallback(NavigationActions.navigate({
|
||||
routeName = "Foo",
|
||||
}))
|
||||
expect(result).to.equal(true)
|
||||
|
||||
local failResult = registeredCallback(NavigationActions.navigate({
|
||||
routeName = "Bar", -- should fail because not a valid route
|
||||
}))
|
||||
expect(failResult).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
expect(registeredCallback).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should correctly pass screenProps to pages", function()
|
||||
local passedScreenProps = nil
|
||||
local extractedValue1 = nil
|
||||
local extractedMissingValue1 = nil
|
||||
local extractedMissingValue2 = nil
|
||||
|
||||
local testScreenProps = {
|
||||
MyKey1 = "MyValue1",
|
||||
}
|
||||
|
||||
local TestNavigator = createSwitchNavigator({
|
||||
routes = {
|
||||
Foo = function(props)
|
||||
-- doing this in render is an abuse, but it's just a test
|
||||
passedScreenProps = props.navigation.getScreenProps()
|
||||
extractedValue1 = props.navigation.getScreenProps("MyKey1")
|
||||
extractedMissingValue1 = props.navigation.getScreenProps("MyMissingKey", 5)
|
||||
extractedMissingValue2 = props.navigation.getScreenProps("MyMissingKey")
|
||||
end,
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local TestApp = createAppContainer(TestNavigator)
|
||||
local element = Roact.createElement(TestApp, {
|
||||
screenProps = testScreenProps,
|
||||
})
|
||||
local instance = Roact.mount(element)
|
||||
|
||||
expect(passedScreenProps).to.equal(testScreenProps)
|
||||
expect(extractedValue1).to.equal("MyValue1")
|
||||
expect(extractedMissingValue1).to.equal(5)
|
||||
expect(extractedMissingValue2).to.equal(nil)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
return function()
|
||||
local NavigationEvents = require(script.Parent.Parent.NavigationEvents)
|
||||
local getChildEventSubscriber = require(script.Parent.Parent.getChildEventSubscriber)
|
||||
|
||||
local function dummyAddListener() end
|
||||
|
||||
local function makeListenerBundle()
|
||||
local testUpstreamListenerMap = {}
|
||||
local function testAddUpstreamListener(eventType, callback)
|
||||
testUpstreamListenerMap[eventType] = callback
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
testUpstreamListenerMap[eventType] = nil
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
listenerMap = testUpstreamListenerMap,
|
||||
addListener = testAddUpstreamListener,
|
||||
}
|
||||
end
|
||||
|
||||
local SIMPLE_TEST_KEY = "Foo"
|
||||
|
||||
local SIMPLE_TEST_STATE = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = SIMPLE_TEST_KEY }
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
lastState = {
|
||||
routes = {
|
||||
{ key = SIMPLE_TEST_KEY }
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
action = {
|
||||
type = "SomeAction"
|
||||
},
|
||||
}
|
||||
|
||||
it("should return a table with correct members", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(type(childSubscriber.addListener)).to.equal("function")
|
||||
expect(type(childSubscriber.emit)).to.equal("function")
|
||||
end)
|
||||
|
||||
describe("addListener tests", function()
|
||||
it("should throw on invalid eventType", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(function()
|
||||
childSubscriber.addListener("BadSymbol", function() end)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw on invalid eventHandler", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(function()
|
||||
childSubscriber.addListener(NavigationEvents.Action, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should allow disconnect of listener", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
local connection = childSubscriber.addListener(NavigationEvents.Refocus, function() end)
|
||||
connection.disconnect()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("emit tests", function()
|
||||
it("should throw when trying to emit any event besides Refocus", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.WillFocus)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.DidFocus)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.WillBlur)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.DidBlur)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.Action)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when payload is not a table", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(function()
|
||||
childSubscriber.emit(NavigationEvents.Refocus, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should allow external caller to emit a refocus event with valid payload", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
local testPayload = { a = 1 }
|
||||
local outputPayload = nil
|
||||
|
||||
childSubscriber.addListener(NavigationEvents.Refocus, function(payload)
|
||||
outputPayload = payload
|
||||
end)
|
||||
|
||||
childSubscriber.emit(NavigationEvents.Refocus, testPayload)
|
||||
expect(outputPayload.a).to.equal(1)
|
||||
expect(outputPayload.type).to.equal(NavigationEvents.Refocus)
|
||||
end)
|
||||
|
||||
it("should allow external caller to emit a refocus event with nil payload", function()
|
||||
local childSubscriber = getChildEventSubscriber(dummyAddListener, SIMPLE_TEST_KEY)
|
||||
|
||||
local outputPayload = nil
|
||||
|
||||
childSubscriber.addListener(NavigationEvents.Refocus, function(payload)
|
||||
outputPayload = payload
|
||||
end)
|
||||
|
||||
childSubscriber.emit(NavigationEvents.Refocus)
|
||||
expect(outputPayload.type).to.equal(NavigationEvents.Refocus)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("upstream event handling tests", function()
|
||||
it("should register subscriptions for supported event types", function()
|
||||
local testUpstreamListenerMap = {}
|
||||
local function testAddUpstreamListener(eventType, callback)
|
||||
expect(testUpstreamListenerMap[eventType]).to.equal(nil)
|
||||
testUpstreamListenerMap[eventType] = true
|
||||
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end
|
||||
|
||||
getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY)
|
||||
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(true)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(true)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(true)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(true)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(true)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should disconnect subscriptions on DidBlur when there is no new route", function()
|
||||
local testUpstreamListenerMap = {}
|
||||
local function testAddUpstreamListener(eventType, callback)
|
||||
testUpstreamListenerMap[eventType] = callback
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
testUpstreamListenerMap[eventType] = false
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local childSubscriber = getChildEventSubscriber(
|
||||
testAddUpstreamListener, SIMPLE_TEST_KEY, "Blurred")
|
||||
|
||||
childSubscriber.addListener(NavigationEvents.Action, function() end)
|
||||
|
||||
testUpstreamListenerMap[NavigationEvents.DidBlur]({
|
||||
state = {},
|
||||
action = {
|
||||
type = "SomeAction"
|
||||
}
|
||||
})
|
||||
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Action]).to.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should NOT disconnect subscriptions on DidBlur when there is a new route", function()
|
||||
local testUpstreamListenerMap = {}
|
||||
local function testAddUpstreamListener(eventType, callback)
|
||||
testUpstreamListenerMap[eventType] = callback
|
||||
|
||||
return {
|
||||
disconnect = function()
|
||||
testUpstreamListenerMap[eventType] = false
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local childSubscriber = getChildEventSubscriber(testAddUpstreamListener, SIMPLE_TEST_KEY, "Blurred")
|
||||
|
||||
childSubscriber.addListener(NavigationEvents.Action, function() end)
|
||||
|
||||
testUpstreamListenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Action]).to.never.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillFocus]).to.never.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidFocus]).to.never.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.WillBlur]).to.never.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.DidBlur]).to.never.equal(false)
|
||||
expect(testUpstreamListenerMap[NavigationEvents.Refocus]).to.never.equal(false)
|
||||
end)
|
||||
|
||||
it("should propagate refocus event from upstream", function()
|
||||
local bundle = makeListenerBundle()
|
||||
|
||||
local outputPayload = nil
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY)
|
||||
childSubscriber.addListener(NavigationEvents.Refocus, function(payload)
|
||||
outputPayload = payload
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Refocus]({ a = 1 })
|
||||
|
||||
expect(outputPayload.a).to.equal(1)
|
||||
expect(outputPayload.type).to.equal(NavigationEvents.Refocus)
|
||||
end)
|
||||
|
||||
it("should emit WillFocus on WillFocus event when previously blurred and child is current index", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY)
|
||||
|
||||
local willFocusPayload = nil
|
||||
childSubscriber.addListener(NavigationEvents.WillFocus, function(payload)
|
||||
willFocusPayload = payload
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.WillFocus](SIMPLE_TEST_STATE)
|
||||
|
||||
-- Detailed analysis of generated payload. Further tests will just check that functor was called.
|
||||
expect(willFocusPayload).to.never.equal(nil)
|
||||
expect(willFocusPayload.state).to.never.equal(nil)
|
||||
expect(willFocusPayload.lastState).to.never.equal(nil)
|
||||
expect(willFocusPayload.action.type).to.equal("SomeAction")
|
||||
expect(willFocusPayload.type).to.equal(NavigationEvents.WillFocus)
|
||||
end)
|
||||
|
||||
it("should emit WillFocus AND DidFocus on Action event when previously blurred and child is current index", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY)
|
||||
|
||||
local willFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.WillFocus, function()
|
||||
willFocusCalled = true
|
||||
end)
|
||||
|
||||
local didFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidFocus, function()
|
||||
didFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE)
|
||||
expect(willFocusCalled).to.equal(true)
|
||||
expect(didFocusCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should NOT emit WillFocus or DidFocus on Action event when previously blurred and child is NOT current index",
|
||||
function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY)
|
||||
|
||||
local willFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.WillFocus, function()
|
||||
willFocusCalled = true
|
||||
end)
|
||||
|
||||
local didFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidFocus, function()
|
||||
didFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action]({
|
||||
state = {
|
||||
routes = {
|
||||
{ key = SIMPLE_TEST_KEY },
|
||||
{ key = "NOT_SIMPLE_TEST_KEY" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
action = {
|
||||
"SomeAction"
|
||||
},
|
||||
})
|
||||
|
||||
expect(willFocusCalled).to.equal(false)
|
||||
expect(didFocusCalled).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should emit DidFocus on DidFocus event when previous event was WillFocus and child is current index", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing")
|
||||
|
||||
local didFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidFocus, function()
|
||||
didFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.DidFocus](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(didFocusCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should emit DidFocus on Action event when previous event was WillFocus and child is current index", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing")
|
||||
|
||||
local didFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidFocus, function()
|
||||
didFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(didFocusCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should NOT emit DidFocus on DidFocus event when previous event was WillFocus while transitioning", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focusing")
|
||||
|
||||
local didFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidFocus, function()
|
||||
didFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.DidFocus]({
|
||||
state = {
|
||||
routes = {
|
||||
{ key = SIMPLE_TEST_KEY }
|
||||
},
|
||||
index = 1,
|
||||
isTransitioning = true,
|
||||
},
|
||||
action = {
|
||||
"SomeAction"
|
||||
},
|
||||
})
|
||||
|
||||
expect(didFocusCalled).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should emit WillBlur on WillBlur event when previous event was DidFocus", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focused")
|
||||
|
||||
local willBlurCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.WillBlur, function()
|
||||
willBlurCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.WillBlur](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(willBlurCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should emit Action on Action event when previous event was DidFocus", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Focused")
|
||||
|
||||
local actionCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.Action, function()
|
||||
actionCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(actionCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should emit DidBlur on DidBlur event when previous event was WillBlur", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, SIMPLE_TEST_KEY, "Blurring")
|
||||
|
||||
local didBlurCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidBlur, function()
|
||||
didBlurCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.DidBlur](SIMPLE_TEST_STATE)
|
||||
|
||||
expect(didBlurCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should emit DidBlur on Action event when previous event was WillBlur and we've finished transitioning", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, "Foo", "Blurring")
|
||||
|
||||
local didBlurCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidBlur, function()
|
||||
didBlurCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action]({
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "Foo" }, -- Transitioned away from this route!
|
||||
{ key = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
lastState = {
|
||||
routes = {
|
||||
{ key = "Foo" },
|
||||
{ key = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
action = {
|
||||
type = "SomeAction"
|
||||
},
|
||||
})
|
||||
|
||||
expect(didBlurCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should emit WillFocus on Action event when previois event was WillBlur, while transitioning to child", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", "Blurring")
|
||||
|
||||
local willFocusCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.WillFocus, function()
|
||||
willFocusCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action]({
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "Foo" }, -- Transitioned away from this route!
|
||||
{ key = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
isTransitioning = true,
|
||||
},
|
||||
lastState = {
|
||||
routes = {
|
||||
{ key = "Foo" },
|
||||
{ key = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
action = {
|
||||
type = "SomeAction"
|
||||
},
|
||||
})
|
||||
|
||||
expect(willFocusCalled).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should disconnect from input events after finalizing blur if removed from nav state", function()
|
||||
local bundle = makeListenerBundle()
|
||||
local childSubscriber = getChildEventSubscriber(bundle.addListener, "Bar", "Blurring")
|
||||
|
||||
local didBlurCalled = false
|
||||
childSubscriber.addListener(NavigationEvents.DidBlur, function()
|
||||
didBlurCalled = true
|
||||
end)
|
||||
|
||||
bundle.listenerMap[NavigationEvents.Action]({
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "Foo" },
|
||||
},
|
||||
index = 1,
|
||||
isTransitioning = false,
|
||||
},
|
||||
lastState = {
|
||||
routes = {
|
||||
{ key = "Foo" },
|
||||
{ key = "Bar" }, -- Transitioning away from this route.
|
||||
},
|
||||
index = 1,
|
||||
isTransitioning = true,
|
||||
},
|
||||
action = {
|
||||
type = "SomeAction"
|
||||
},
|
||||
})
|
||||
|
||||
expect(bundle.listenerMap[NavigationEvents.Action]).to.equal(nil)
|
||||
expect(bundle.listenerMap[NavigationEvents.Refocus]).to.equal(nil)
|
||||
expect(didBlurCalled).to.equal(true) -- Event should still be propagated!
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
return function()
|
||||
local getChildNavigation = require(script.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return nil if there is no route matching requested key", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "invalid_child", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return cached child if its state is a top-level route", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
testNavigation._childrenNavigation = {
|
||||
a = {
|
||||
state = testNavigation.state.routes[1]
|
||||
}
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation.a)
|
||||
end)
|
||||
|
||||
it("should update cache and return new data when child's state has changed", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
router = {
|
||||
getComponentForRouteName = function(routeName)
|
||||
return function() end
|
||||
end,
|
||||
getActionCreators = function() end
|
||||
}
|
||||
}
|
||||
|
||||
local oldStateA = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
}
|
||||
|
||||
testNavigation._childrenNavigation = {
|
||||
a = oldStateA
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation["a"])
|
||||
expect(childNav.state).to.equal(testNavigation.state.routes[1])
|
||||
expect(type(childNav.getParam)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should create a new entry if cached child does not exist yet", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a", params = { a = 1 } },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
router = {
|
||||
getComponentForRouteName = function(routeName)
|
||||
return function() end
|
||||
end,
|
||||
getActionCreators = function() end,
|
||||
},
|
||||
addListener = function()
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end,
|
||||
isFocused = function()
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil)
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation["a"])
|
||||
expect(childNav.isFocused()).to.equal(true)
|
||||
|
||||
expect(childNav.getParam("a", 0)).to.equal(1)
|
||||
expect(childNav.getParam("b", 0)).to.equal(0)
|
||||
end)
|
||||
end
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
return function()
|
||||
local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache)
|
||||
|
||||
it("should return empty table if navigation arg not provided", function()
|
||||
expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should populate navigation._childrenNavigation as a side-effect", function()
|
||||
local navigation = { state = {} }
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result).to.never.equal(nil)
|
||||
expect(navigation._childrenNavigation).to.equal(result)
|
||||
end)
|
||||
|
||||
it("should delete children cache keys that are no longer valid", function()
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "one" },
|
||||
{ key = "two" },
|
||||
{ key = "three" },
|
||||
}
|
||||
},
|
||||
_childrenNavigation = {
|
||||
one = {},
|
||||
two = {},
|
||||
three = {},
|
||||
four = {},
|
||||
}
|
||||
}
|
||||
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result.one).to.never.equal(nil)
|
||||
expect(result.two).to.never.equal(nil)
|
||||
expect(result.three).to.never.equal(nil)
|
||||
expect(result.four).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should not delete children cache keys if in transitioning state", function()
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "one" },
|
||||
{ key = "two" },
|
||||
{ key = "three" },
|
||||
},
|
||||
isTransitioning = true,
|
||||
},
|
||||
_childrenNavigation = {
|
||||
one = {},
|
||||
two = {},
|
||||
three = {},
|
||||
four = {},
|
||||
}
|
||||
}
|
||||
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result.one).to.never.equal(nil)
|
||||
expect(result.two).to.never.equal(nil)
|
||||
expect(result.three).to.never.equal(nil)
|
||||
expect(result.four).to.never.equal(nil)
|
||||
end)
|
||||
end
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
return function()
|
||||
local NavigationEvents = require(script.Parent.Parent.NavigationEvents)
|
||||
local getNavigation = require(script.Parent.Parent.getNavigation)
|
||||
|
||||
local function makeTestBundle(testState)
|
||||
testState = testState or {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local testActions = {}
|
||||
local bundle = {
|
||||
testActions = testActions,
|
||||
testState = testState,
|
||||
testRouter = {
|
||||
getActionCreators = function()
|
||||
return testActions
|
||||
end
|
||||
},
|
||||
testDispatch = function() end,
|
||||
testActionSubscribers = {},
|
||||
testGetScreenProps = function() end,
|
||||
}
|
||||
|
||||
function bundle.testGetCurrentNavigation()
|
||||
return bundle.navigation
|
||||
end
|
||||
|
||||
bundle.navigation = getNavigation(
|
||||
bundle.testRouter,
|
||||
bundle.testState,
|
||||
bundle.testDispatch,
|
||||
bundle.testActionSubscribers,
|
||||
bundle.testGetScreenProps,
|
||||
bundle.testGetCurrentNavigation
|
||||
)
|
||||
|
||||
return bundle
|
||||
end
|
||||
|
||||
it("should build out correct public props", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
expect(bundle.navigation.actions).to.equal(bundle.testActions)
|
||||
expect(bundle.navigation.router).to.equal(bundle.testRouter)
|
||||
expect(bundle.navigation.state).to.equal(bundle.testState)
|
||||
expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch)
|
||||
expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps)
|
||||
expect(#bundle.navigation._childrenNavigation).to.equal(0)
|
||||
end)
|
||||
|
||||
describe("isFocused tests", function()
|
||||
it("should return focused=true for child key matching index", function()
|
||||
local bundle = makeTestBundle()
|
||||
expect(bundle.navigation.isFocused("a")).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return focused=false for child key not matching index", function()
|
||||
local bundle = makeTestBundle({
|
||||
routes = {
|
||||
{ key = "a" },
|
||||
{ key = "b" },
|
||||
},
|
||||
index = 2,
|
||||
})
|
||||
expect(bundle.navigation.isFocused("a")).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return focused=true if no child key provided (parent always focused)", function()
|
||||
local bundle = makeTestBundle()
|
||||
expect(bundle.navigation.isFocused()).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("addListener tests", function()
|
||||
it("should short-circuit subscriptions for non-Action events", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
local testHandler = function() end
|
||||
bundle.navigation.addListener(NavigationEvents.WillFocus, testHandler)
|
||||
expect(bundle.testActionSubscribers[testHandler]).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should add Action event handlers to actionSubscribers set", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
local testHandler = function() end
|
||||
bundle.navigation.addListener(NavigationEvents.Action, testHandler)
|
||||
expect(bundle.testActionSubscribers[testHandler]).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
return function()
|
||||
local RoactNavigation = require(script.Parent.Parent)
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local StackPresentationStyle = require(script.Parent.Parent.views.StackView.StackPresentationStyle)
|
||||
local NoneSymbol = require(script.Parent.Parent.NoneSymbol)
|
||||
|
||||
it("should load", function()
|
||||
require(script.Parent.Parent)
|
||||
end)
|
||||
|
||||
it("should return a function for createAppContainer", function()
|
||||
expect(type(RoactNavigation.createAppContainer)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getNavigation", function()
|
||||
expect(type(RoactNavigation.getNavigation)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return an appropriate table for Context", function()
|
||||
expect(type(RoactNavigation.Context)).to.equal("table")
|
||||
expect(type(RoactNavigation.Context.Provider)).to.equal("table")
|
||||
expect(type(RoactNavigation.Context.Consumer)).to.equal("table")
|
||||
expect(type(RoactNavigation.Context.connect)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a table for Provider", function()
|
||||
expect(type(RoactNavigation.Provider)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for Consumer", function()
|
||||
expect(type(RoactNavigation.Consumer)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a function for connect", function()
|
||||
expect(type(RoactNavigation.connect)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for withNavigation", function()
|
||||
expect(type(RoactNavigation.withNavigation)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for withNavigationFocus", function()
|
||||
expect(type(RoactNavigation.withNavigationFocus)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createSwitchNavigator", function()
|
||||
expect(type(RoactNavigation.createSwitchNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createStackNavigator", function()
|
||||
expect(type(RoactNavigation.createStackNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createNavigator", function()
|
||||
expect(type(RoactNavigation.createNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for StackRouter", function()
|
||||
expect(type(RoactNavigation.StackRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for SwitchRouter", function()
|
||||
expect(type(RoactNavigation.SwitchRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for TabRouter", function()
|
||||
expect(type(RoactNavigation.TabRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a table for Actions", function()
|
||||
expect(type(RoactNavigation.Actions)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for StackActions", function()
|
||||
expect(type(RoactNavigation.StackActions)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for BackBehavior", function()
|
||||
expect(type(RoactNavigation.BackBehavior)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for Events", function()
|
||||
expect(type(RoactNavigation.Events)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a valid component for EventsAdapter", function()
|
||||
expect(RoactNavigation.EventsAdapter.render).never.to.equal(nil)
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.EventsAdapter, {
|
||||
navigation = {
|
||||
addListener = function()
|
||||
return { disconnect = function() end }
|
||||
end
|
||||
}
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return StackPresentationStyle", function()
|
||||
expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle)
|
||||
end)
|
||||
|
||||
it("should return NoneSymbol", function()
|
||||
expect(RoactNavigation.None).to.equal(NoneSymbol)
|
||||
end)
|
||||
|
||||
it("should return a valid component for SceneView", function()
|
||||
expect(RoactNavigation.SceneView.render).never.to.equal(nil)
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, {
|
||||
navigation = {},
|
||||
component = function() end,
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return a valid component for SwitchView", function()
|
||||
expect(RoactNavigation.SwitchView.render).never.to.equal(nil)
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo", }
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
}
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.SwitchView, {
|
||||
descriptors = {
|
||||
Foo = {
|
||||
getComponent = function()
|
||||
return function() end
|
||||
end,
|
||||
navigation = testNavigation,
|
||||
}
|
||||
},
|
||||
navigation = testNavigation,
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return a function for createConfigGetter", function()
|
||||
expect(type(RoactNavigation.createConfigGetter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getScreenForRouteName", function()
|
||||
expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for validateRouteConfigMap", function()
|
||||
expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getActiveChildNavigationOptions", function()
|
||||
expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function")
|
||||
end)
|
||||
end
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
local Roact = require(script.Parent.Parent.Roact)
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local NavigationActions = require(script.Parent.NavigationActions)
|
||||
local NavigationEvents = require(script.Parent.NavigationEvents)
|
||||
local AppNavigationContext = require(script.Parent.views.AppNavigationContext)
|
||||
local getNavigation = require(script.Parent.getNavigation)
|
||||
local validate = require(script.Parent.utils.validate)
|
||||
|
||||
local function validateProps(props)
|
||||
if not props.navigation then
|
||||
return
|
||||
end
|
||||
|
||||
local errStr =
|
||||
"This navigator has both 'navigation' and container props. " ..
|
||||
"It is unclear if it should own its own state. Remove the " ..
|
||||
"container props or don't pass a 'navigation' prop."
|
||||
|
||||
for key in pairs(props) do
|
||||
validate(key == "screenProps" or key == "navigation", errStr)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Construct a container Roact component that will host the navigation hierarchy
|
||||
specified by your main AppComponent. AppComponent must be a navigator created by
|
||||
a Roact-Navigation helper function, or a stateful Roact component
|
||||
|
||||
If you are using a custom stateful Roact component, make sure to set the 'router'
|
||||
field so that it can be hooked into the navigation system. You must also pass your
|
||||
'navigation' prop to any child navigators.
|
||||
|
||||
Additional props:
|
||||
renderLoading - Roact component to render while the app is loading.
|
||||
externalDispatchConnector - Function that Roact Navigation can use to connect to
|
||||
externally triggered navigation Actions. This is useful
|
||||
for external UI or handling of the Android back button.
|
||||
|
||||
Ex:
|
||||
local connector = function(rnDispatch)
|
||||
-- You store rnDispatch and call it when you want to inject
|
||||
-- an event from outside RN.
|
||||
return function()
|
||||
-- You disconnect rnDispatch when RN calls this.
|
||||
end
|
||||
end
|
||||
|
||||
...
|
||||
Roact.createElement(MyRNAppContainer, {
|
||||
externalDispatchConnector = connector,
|
||||
})
|
||||
]]
|
||||
return function(AppComponent)
|
||||
validate(type(AppComponent) == "table" and AppComponent.router ~= nil,
|
||||
"AppComponent must be a navigator or a stateful Roact component with a 'router' field")
|
||||
|
||||
local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent))
|
||||
local NavigationContainer = Roact.Component:extend(containerName)
|
||||
|
||||
function NavigationContainer.getDerivedStateFromProps(nextProps)
|
||||
validateProps(nextProps)
|
||||
return nil
|
||||
end
|
||||
|
||||
function NavigationContainer:init()
|
||||
validateProps(self.props)
|
||||
|
||||
self._actionEventSubscribers = {}
|
||||
self._initialAction = NavigationActions.init()
|
||||
|
||||
local initialNav = nil
|
||||
local containerIsStateful = self:_isStateful()
|
||||
if containerIsStateful and not self.props.persistenceKey then
|
||||
initialNav = AppComponent.router.getStateForAction(self._initialAction)
|
||||
end
|
||||
|
||||
self.state = {
|
||||
nav = initialNav,
|
||||
}
|
||||
end
|
||||
|
||||
function NavigationContainer:_updateExternalDispatchConnector()
|
||||
local externalDispatchConnector = self.props.externalDispatchConnector
|
||||
if self._subs then
|
||||
self._subs()
|
||||
self._subs = nil
|
||||
end
|
||||
|
||||
if externalDispatchConnector ~= nil then
|
||||
self._subs = externalDispatchConnector(function(...)
|
||||
if self._isMounted then
|
||||
return self:dispatch(...)
|
||||
end
|
||||
|
||||
-- External dispatch while we're not mounted gets dropped on floor.
|
||||
return false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_renderLoading()
|
||||
local renderLoading = self.props.renderLoading
|
||||
if renderLoading then
|
||||
return renderLoading()
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:render()
|
||||
local navigation = self.props.navigation
|
||||
|
||||
if self:_isStateful() then
|
||||
local navState = self.state.nav
|
||||
if not navState then
|
||||
return self:_renderLoading()
|
||||
end
|
||||
|
||||
if not self._navigation or self._navigation.state ~= navState then
|
||||
self._navigation = getNavigation(
|
||||
AppComponent.router,
|
||||
navState,
|
||||
function(...)
|
||||
return self:dispatch(...)
|
||||
end,
|
||||
self._actionEventSubscribers,
|
||||
function(...)
|
||||
return self:_getScreenProps(...)
|
||||
end,
|
||||
function()
|
||||
return self._navigation
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
navigation = self._navigation
|
||||
end
|
||||
|
||||
validate(navigation ~= nil, "failed to get navigation")
|
||||
|
||||
return Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = navigation,
|
||||
}, {
|
||||
-- Provide navigation prop for top-level component so it doesn't have to connect.
|
||||
AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, {
|
||||
navigation = navigation,
|
||||
}))
|
||||
})
|
||||
end
|
||||
|
||||
function NavigationContainer:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
self:_updateExternalDispatchConnector()
|
||||
|
||||
if not self:_isStateful() then
|
||||
return
|
||||
end
|
||||
|
||||
local action = self._initialAction
|
||||
local startupState = self.state.nav
|
||||
|
||||
if not startupState then
|
||||
startupState = AppComponent.router.getStateForAction(action)
|
||||
end
|
||||
|
||||
local function dispatchActionEvents()
|
||||
-- _actionEventSubscribers is a table(handler, true), e.g. a Set container
|
||||
for subscriber in pairs(self._actionEventSubscribers) do
|
||||
subscriber({
|
||||
type = NavigationEvents.Action,
|
||||
action = action,
|
||||
state = self.state.nav,
|
||||
-- there is no lastState for initial mounting
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if startupState ~= self.state.nav then
|
||||
self:setState({
|
||||
nav = startupState
|
||||
})
|
||||
end
|
||||
|
||||
-- This must be spawned until we get async setState callback handler in Roact
|
||||
spawn(dispatchActionEvents)
|
||||
end
|
||||
|
||||
function NavigationContainer:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
-- TODO: Disconnect from from URL listener once implemented
|
||||
|
||||
if self._subs then
|
||||
self._subs()
|
||||
self._subs = nil
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:didUpdate(oldProps)
|
||||
-- Clear cached _navState every time we update.
|
||||
if self._navState == self.state.nav then
|
||||
self._navState = nil
|
||||
end
|
||||
|
||||
if self.props.externalDispatchConnector ~= oldProps.externalDispatchConnector then
|
||||
self:_updateExternalDispatchConnector()
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_isStateful()
|
||||
return not self.props.navigation
|
||||
end
|
||||
|
||||
-- NOTE: Not implementing _validateProps; it is duplicate
|
||||
|
||||
-- NOTE: Not implementing _handleOpenURL; app should have a component
|
||||
-- that transforms URLs into paths for AppContainer instead.
|
||||
|
||||
function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action)
|
||||
local onNavigationStateChange = self.props.onNavigationStateChange
|
||||
|
||||
if type(onNavigationStateChange) == "function" then
|
||||
onNavigationStateChange(prevNav, nextNav, action)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_getScreenProps(propKey, defaultValue)
|
||||
local screenProps = self.props.screenProps or {}
|
||||
if propKey ~= nil then
|
||||
return screenProps[propKey] or defaultValue
|
||||
end
|
||||
|
||||
-- Legacy: return original table if no args provided
|
||||
return screenProps
|
||||
end
|
||||
|
||||
function NavigationContainer:dispatch(action)
|
||||
if self.props.navigation then
|
||||
return self.props.navigation.dispatch(action)
|
||||
end
|
||||
|
||||
self._navState = self._navState or self.state.nav
|
||||
|
||||
local lastNavState = self._navState
|
||||
validate(lastNavState ~= nil, "navState should be set in constructor if stateful")
|
||||
|
||||
local reducedState = AppComponent.router.getStateForAction(action, lastNavState)
|
||||
local navState = reducedState
|
||||
if not navState then
|
||||
navState = lastNavState
|
||||
end
|
||||
|
||||
local function dispatchActionEvents()
|
||||
-- _actionEventSubscribers is a table(handler, true), e.g. a Set container
|
||||
for subscriber in pairs(self._actionEventSubscribers) do
|
||||
subscriber({
|
||||
type = NavigationEvents.Action,
|
||||
action = action,
|
||||
state = navState,
|
||||
lastState = lastNavState,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if reducedState == nil then
|
||||
-- Router returns nil when action has been handled and there is no state change.
|
||||
-- dispatch() must return true whenever something has been handled.
|
||||
dispatchActionEvents()
|
||||
return true
|
||||
end
|
||||
|
||||
if navState ~= lastNavState then
|
||||
-- Update cache to ensure that subsequent calls do not discard this change
|
||||
self._navState = navState
|
||||
|
||||
-- TODO: We have to dispatch events before or after setState (which mounts/unmounts components)
|
||||
-- based upon the specific event type, to ensure that pages get them in the correct order...
|
||||
|
||||
self:setState({
|
||||
nav = navState
|
||||
})
|
||||
|
||||
-- Must be spawned until we get async setState callback handler in Roact.
|
||||
spawn(function()
|
||||
self:_onNavigationStateChange(lastNavState, navState, action)
|
||||
dispatchActionEvents()
|
||||
-- TODO: Add call to persist navigation state here, if we ever implement it.
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
spawn(dispatchActionEvents)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return NavigationContainer
|
||||
end
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local NavigationEvents = require(script.Parent.NavigationEvents)
|
||||
local createSubscriberEventsStateTable = require(script.Parent.utils.createSubscriberEventsStateTable)
|
||||
local validate = require(script.Parent.utils.validate)
|
||||
|
||||
--[[
|
||||
This utility will fire focus and blur events for the child based upon action events
|
||||
and the current navigation state.
|
||||
|
||||
Args:
|
||||
addListener - Functor to add an event listener to navigation dispatch system.
|
||||
key - The key for the subscribing child.
|
||||
initialState - Initial state to set for the child event monitor (optional).
|
||||
]]
|
||||
return function(addListener, key, initialState)
|
||||
initialState = initialState or "Blurred"
|
||||
|
||||
local upstreamSubscribers = {}
|
||||
|
||||
local subscriberMap = {
|
||||
[NavigationEvents.Action] = {},
|
||||
[NavigationEvents.WillFocus] = {},
|
||||
[NavigationEvents.DidFocus] = {},
|
||||
[NavigationEvents.WillBlur] = {},
|
||||
[NavigationEvents.DidBlur] = {},
|
||||
[NavigationEvents.Refocus] = {},
|
||||
}
|
||||
|
||||
local function emit(subscriberType, payload)
|
||||
local payloadWithType = Cryo.Dictionary.join(payload or {}, { type = subscriberType })
|
||||
local subscribers = subscriberMap[subscriberType]
|
||||
if subscribers then
|
||||
for _, subs in ipairs(subscribers) do
|
||||
subs(payloadWithType)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function disconnectAll()
|
||||
for _, subscriberList in pairs(subscriberMap) do
|
||||
for x in pairs(subscriberList) do
|
||||
subscriberList[x] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for _, subs in pairs(upstreamSubscribers) do
|
||||
if subs then
|
||||
subs.disconnect()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Each event subscriber (e.g. screen) has its own state table that tracks the events which
|
||||
-- drive focused/unfocused transitions. Screens all start life blurred, including the initial pages
|
||||
-- displayed by a navigator. The latter need special treatment so they still receive their
|
||||
-- willFocus+didFocus events.
|
||||
local eventStateTable = createSubscriberEventsStateTable(key, initialState, emit, disconnectAll)
|
||||
|
||||
for eventType in pairs(subscriberMap) do
|
||||
upstreamSubscribers[eventType] = addListener(eventType, function(payload)
|
||||
-- Refocus events don't use a specialized child payload or T/A event key marks. Propagate immediately.
|
||||
-- We also allow arbitrary payloads with Refocus so we do not want to try to parse the table.
|
||||
if eventType == NavigationEvents.Refocus then
|
||||
eventStateTable:handleEvent(tostring(NavigationEvents.Refocus), payload)
|
||||
return
|
||||
end
|
||||
|
||||
local state = payload.state
|
||||
local lastState = payload.lastState
|
||||
local action = payload.action
|
||||
|
||||
local routes = state and state.routes
|
||||
local lastRoutes = lastState and lastState.routes
|
||||
|
||||
local focusKey = routes and routes[state.index].key or nil
|
||||
local isChildFocused = focusKey == key
|
||||
local isTransitioning = state and state.isTransitioning or false
|
||||
|
||||
local lastRoute = nil
|
||||
if lastRoutes then
|
||||
for _, route in ipairs(lastRoutes) do
|
||||
if route.key == key then
|
||||
lastRoute = route
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local newRoute = nil
|
||||
if routes then
|
||||
for _, route in ipairs(routes) do
|
||||
if route.key == key then
|
||||
newRoute = route
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local childPayload = {
|
||||
context = string.format("%s:%s_%s", key, tostring(action.type), payload.context or 'Root'),
|
||||
state = newRoute,
|
||||
lastState = lastRoute,
|
||||
action = action,
|
||||
type = eventType,
|
||||
}
|
||||
|
||||
-- State table figures out all the details of what conditions lead to event propagation based
|
||||
-- upon a string eventKey.
|
||||
local activeKey = isChildFocused and "A" or ""
|
||||
local transitioningKey = isTransitioning and "T" or ""
|
||||
local eventKey = tostring(eventType) .. activeKey .. transitioningKey
|
||||
|
||||
eventStateTable:handleEvent(eventKey, childPayload)
|
||||
|
||||
-- Regardless of what state transition we've propagated earlier, shut down the state machine
|
||||
-- if the route has been removed from the nav history because that means the page is gone.
|
||||
-- (This also disconnects the event listeners.)
|
||||
if not newRoute then
|
||||
eventStateTable.events.shutdown()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
addListener = function(eventType, eventHandler)
|
||||
local subscribers = subscriberMap[eventType]
|
||||
validate(subscribers ~= nil, "Invalid event type '%s'", tostring(eventType))
|
||||
validate(type(eventHandler) == "function",
|
||||
"eventHandler for '%s' must be a function", tostring(eventType))
|
||||
table.insert(subscribers, eventHandler)
|
||||
return {
|
||||
disconnect = function()
|
||||
for idx, subs in ipairs(subscribers) do
|
||||
if subs == eventHandler then
|
||||
table.remove(subscribers, idx)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
end,
|
||||
emit = function(eventType, payload)
|
||||
validate(eventType == NavigationEvents.Refocus,
|
||||
"navigation.emit only supports NavigationEvents.Refocus currently.")
|
||||
validate(payload == nil or type(payload) == "table",
|
||||
"navigation.emit payloads must be a table or nil")
|
||||
emit(eventType, payload)
|
||||
end,
|
||||
}
|
||||
end
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local getChildEventSubscriber = require(script.Parent.getChildEventSubscriber)
|
||||
local getChildRouter = require(script.Parent.routers.getChildRouter)
|
||||
local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators)
|
||||
local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache)
|
||||
|
||||
local function createParamGetter(route)
|
||||
return function(paramName, defaultValue)
|
||||
local params = route.params
|
||||
return params and params[paramName] or defaultValue
|
||||
end
|
||||
end
|
||||
|
||||
local function getChildNavigation(navigation, childKey, getCurrentParentNavigation)
|
||||
local children = getChildrenNavigationCache(navigation)
|
||||
|
||||
local childRoute = nil
|
||||
for _, route in ipairs(navigation.state.routes) do
|
||||
if route.key == childKey then
|
||||
childRoute = route
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not childRoute then
|
||||
return nil
|
||||
end
|
||||
|
||||
local requestedChild = children[childKey]
|
||||
|
||||
if requestedChild and requestedChild.state == childRoute then
|
||||
return requestedChild
|
||||
end
|
||||
|
||||
local childRouter = getChildRouter(navigation.router, childRoute.routeName)
|
||||
|
||||
-- If the route has children that match our routes schema then get a reference
|
||||
-- to the focused grandchild so we can pass the correct action creators to the
|
||||
-- child router so that any action that depends on the child route will behave
|
||||
-- as expected.
|
||||
local focusedGrandChildRoute = nil
|
||||
if childRoute.routes and type(childRoute.index) == "number" then
|
||||
focusedGrandChildRoute = childRoute.routes[childRoute.index]
|
||||
end
|
||||
|
||||
local childRouterActionCreators = childRouter and
|
||||
childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {}
|
||||
|
||||
local actionCreators = Cryo.Dictionary.join(
|
||||
navigation.actions or {},
|
||||
navigation.router.getActionCreators(childRoute, navigation.state.key) or {},
|
||||
childRouterActionCreators or {},
|
||||
getNavigationActionCreators(childRoute) or {})
|
||||
|
||||
local actionHelpers = {}
|
||||
for key, creator in pairs(actionCreators) do
|
||||
actionHelpers[key] = function(...)
|
||||
local action = creator(...)
|
||||
return navigation.dispatch(action)
|
||||
end
|
||||
end
|
||||
|
||||
if requestedChild then
|
||||
-- Update cache value for requestedChild because child's state has changed
|
||||
children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, {
|
||||
state = childRoute,
|
||||
router = childRouter,
|
||||
actions = actionCreators,
|
||||
getParam = createParamGetter(childRoute),
|
||||
})
|
||||
|
||||
return children[childKey]
|
||||
else
|
||||
-- No cached value for requestedChild. Create a new entry.
|
||||
local childSubscriber = getChildEventSubscriber(navigation.addListener, childKey)
|
||||
|
||||
children[childKey] = Cryo.Dictionary.join(actionHelpers, {
|
||||
state = childRoute,
|
||||
router = childRouter,
|
||||
actions = actionCreators,
|
||||
getParam = createParamGetter(childRoute),
|
||||
getChildNavigation = function(grandChildKey)
|
||||
return getChildNavigation(children[childKey], grandChildKey, function()
|
||||
local nav = getCurrentParentNavigation()
|
||||
return nav and nav.getChildNavigation(childKey) or nil
|
||||
end)
|
||||
end,
|
||||
isFocused = function()
|
||||
local currentNavigation = getCurrentParentNavigation()
|
||||
if not currentNavigation then
|
||||
return false
|
||||
end
|
||||
|
||||
local state = currentNavigation.state
|
||||
local routes = state.routes
|
||||
local index = state.index
|
||||
|
||||
if not currentNavigation.isFocused() then
|
||||
return false
|
||||
end
|
||||
|
||||
-- If we're transitioning to this state then we are NOT focused until the transition is over.
|
||||
return (routes[index].key == childKey and state.isTransitioning ~= true) or false
|
||||
end,
|
||||
dispatch = navigation.dispatch,
|
||||
getScreenProps = navigation.getScreenProps,
|
||||
addListener = childSubscriber.addListener,
|
||||
emit = childSubscriber.emit,
|
||||
})
|
||||
|
||||
return children[childKey]
|
||||
end
|
||||
end
|
||||
|
||||
return getChildNavigation
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
return function(navigation)
|
||||
if not navigation then
|
||||
return {}
|
||||
end
|
||||
|
||||
if not navigation._childrenNavigation then
|
||||
navigation._childrenNavigation = {}
|
||||
end
|
||||
|
||||
local childrenNavigationCache = navigation._childrenNavigation
|
||||
|
||||
local childKeys = {}
|
||||
for _, route in ipairs(navigation.state.routes or {}) do
|
||||
childKeys[route.key] = true
|
||||
end
|
||||
|
||||
if not navigation.state.isTransitioning then
|
||||
for cacheKey, _ in pairs(childrenNavigationCache) do
|
||||
if not childKeys[cacheKey] then
|
||||
childrenNavigationCache[cacheKey] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return navigation._childrenNavigation
|
||||
end
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local NavigationEvents = require(script.Parent.NavigationEvents)
|
||||
local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators)
|
||||
local getChildNavigation = require(script.Parent.getChildNavigation)
|
||||
local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache)
|
||||
|
||||
return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation)
|
||||
local actions = router.getActionCreators(state, nil)
|
||||
|
||||
local navigation = {
|
||||
actions = actions,
|
||||
router = router,
|
||||
state = state,
|
||||
dispatch = dispatch,
|
||||
getScreenProps = getScreenProps,
|
||||
_childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()),
|
||||
}
|
||||
|
||||
function navigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(navigation, childKey, getCurrentNavigation)
|
||||
end
|
||||
|
||||
function navigation.isFocused(childKey)
|
||||
local routes = getCurrentNavigation().state.routes
|
||||
local index = getCurrentNavigation().state.index
|
||||
|
||||
return not childKey or routes[index].key == childKey
|
||||
end
|
||||
|
||||
function navigation.addListener(event, handler)
|
||||
if event ~= NavigationEvents.Action then
|
||||
return { disconnect = function() end }
|
||||
else
|
||||
actionSubscribers[handler] = true
|
||||
return {
|
||||
disconnect = function()
|
||||
actionSubscribers[handler] = nil
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions)
|
||||
|
||||
for actionName, _ in pairs(actionCreators) do
|
||||
navigation[actionName] = function(...)
|
||||
navigation.dispatch(actionCreators[actionName](...))
|
||||
end
|
||||
end
|
||||
|
||||
return navigation
|
||||
end
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
-- Generator information:
|
||||
-- Human name: Roact Navigation
|
||||
-- Variable name: RoactNavigation
|
||||
-- Repo name: roact-navigation
|
||||
|
||||
return {
|
||||
-- Navigation container construction
|
||||
createAppContainer = require(script.createAppContainer),
|
||||
getNavigation = require(script.getNavigation),
|
||||
|
||||
-- Context Access
|
||||
Context = require(script.views.AppNavigationContext),
|
||||
Provider = require(script.views.AppNavigationContext).Provider,
|
||||
Consumer = require(script.views.AppNavigationContext).Consumer,
|
||||
connect = require(script.views.AppNavigationContext).connect,
|
||||
|
||||
withNavigation = require(script.views.withNavigation),
|
||||
withNavigationFocus = require(script.views.withNavigationFocus),
|
||||
|
||||
-- Navigators
|
||||
createStackNavigator = require(script.navigators.createStackNavigator),
|
||||
createSwitchNavigator = require(script.navigators.createSwitchNavigator),
|
||||
createNavigator = require(script.navigators.createNavigator),
|
||||
|
||||
-- Routers
|
||||
StackRouter = require(script.routers.StackRouter),
|
||||
SwitchRouter = require(script.routers.SwitchRouter),
|
||||
TabRouter = require(script.routers.TabRouter),
|
||||
|
||||
-- Navigation Actions
|
||||
Actions = require(script.NavigationActions),
|
||||
StackActions = require(script.StackActions),
|
||||
BackBehavior = require(script.BackBehavior),
|
||||
|
||||
-- Navigation Events
|
||||
Events = require(script.NavigationEvents),
|
||||
EventsAdapter = require(script.views.NavigationEventsAdapter),
|
||||
|
||||
-- Additional Types
|
||||
StackPresentationStyle = require(script.views.StackView.StackPresentationStyle),
|
||||
None = require(script.NoneSymbol),
|
||||
|
||||
-- Screen Views
|
||||
SceneView = require(script.views.SceneView),
|
||||
SwitchView = require(script.views.SwitchView),
|
||||
StackView = require(script.views.StackView.StackView),
|
||||
|
||||
-- Utilities
|
||||
createConfigGetter = require(script.routers.createConfigGetter),
|
||||
getScreenForRouteName = require(script.routers.getScreenForRouteName),
|
||||
validateRouteConfigMap = require(script.routers.validateRouteConfigMap),
|
||||
getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions),
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createNavigator = require(script.Parent.Parent.createNavigator)
|
||||
|
||||
local testRouter = {
|
||||
getScreenOptions = function() return nil end,
|
||||
}
|
||||
|
||||
it("should return a Roact component that exposes navigator fields", function()
|
||||
local testComponentMounted = nil
|
||||
local TestViewComponent = Roact.Component:extend("TestViewComponent")
|
||||
function TestViewComponent:render() end
|
||||
function TestViewComponent:didMount() testComponentMounted = true end
|
||||
function TestViewComponent:willUnmount() testComponentMounted = false end
|
||||
|
||||
local testNavOptions = {}
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = testNavOptions,
|
||||
})
|
||||
|
||||
expect(type(navigator.render)).to.equal("function")
|
||||
expect(navigator.router).to.equal(testRouter)
|
||||
expect(navigator.navigationOptions).to.equal(testNavOptions)
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
getChildNavigation = function() return nil end, -- stub
|
||||
}
|
||||
|
||||
-- Try to mount it
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
expect(testComponentMounted).to.equal(true)
|
||||
Roact.unmount(instance)
|
||||
expect(testComponentMounted).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should throw when trying to mount without navigation prop", function()
|
||||
local TestViewComponent = function() end
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = {}
|
||||
})
|
||||
|
||||
expect(function()
|
||||
Roact.mount(Roact.createElement(navigator))
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when trying to mount without routes", function()
|
||||
local TestViewComponent = function() end
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = {}
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
index = 1
|
||||
},
|
||||
getChildNavigation = function() return nil end, -- stub
|
||||
}
|
||||
|
||||
expect(function()
|
||||
Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createStackNavigator = require(script.Parent.Parent.createStackNavigator)
|
||||
local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return a mountable Roact component", function()
|
||||
local navigator = createStackNavigator({
|
||||
routes = {
|
||||
Foo = function() end
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
router = navigator.router
|
||||
}
|
||||
|
||||
function testNavigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(testNavigation, childKey, function()
|
||||
return testNavigation
|
||||
end)
|
||||
end
|
||||
|
||||
function testNavigation.addListener(symbol, callback)
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator)
|
||||
local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return a mountable Roact component", function()
|
||||
local navigator = createSwitchNavigator({
|
||||
routes = {
|
||||
Foo = function() end
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
router = navigator.router
|
||||
}
|
||||
|
||||
function testNavigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(testNavigation, childKey, function()
|
||||
return testNavigation
|
||||
end)
|
||||
end
|
||||
|
||||
function testNavigation.addListener(symbol, callback)
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(navigatorViewComponent, router, navigationConfig)
|
||||
local Navigator = Roact.Component:extend("Navigator")
|
||||
|
||||
-- These statics need to be accessible to routers
|
||||
Navigator.router = router
|
||||
Navigator.navigationOptions = navigationConfig.navigationOptions
|
||||
|
||||
function Navigator:init()
|
||||
local screenProps = self.props.screenProps
|
||||
|
||||
self.state = {
|
||||
descriptors = {},
|
||||
screenProps = screenProps
|
||||
}
|
||||
end
|
||||
|
||||
function Navigator.getDerivedStateFromProps(nextProps, prevState)
|
||||
local prevDescriptors = prevState.descriptors
|
||||
local navigation = nextProps.navigation
|
||||
local screenProps = nextProps.screenProps
|
||||
|
||||
validate(navigation ~= nil, "The navigation prop is missing for this navigator")
|
||||
|
||||
local routes = navigation.state.routes
|
||||
|
||||
validate(type(routes) == "table", "No 'routes' found in navigation state. " ..
|
||||
"Don't try to pass the navigation prop from a Roact component to a Navigator child.")
|
||||
|
||||
local descriptors = {}
|
||||
|
||||
for _, route in ipairs(routes) do
|
||||
if prevDescriptors and prevDescriptors[route.key] and
|
||||
route == prevDescriptors[route.key].state and
|
||||
screenProps == prevState.screenProps then
|
||||
descriptors[route.key] = prevDescriptors[route.key]
|
||||
else
|
||||
local getComponent = function()
|
||||
return router.getComponentForRouteName(route.routeName)
|
||||
end
|
||||
|
||||
local childNavigation = navigation.getChildNavigation(route.key)
|
||||
local options = router.getScreenOptions(childNavigation, screenProps)
|
||||
|
||||
descriptors[route.key] = {
|
||||
key = route.key,
|
||||
getComponent = getComponent,
|
||||
options = options,
|
||||
state = route,
|
||||
navigation = childNavigation,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
descriptors = descriptors,
|
||||
screenProps = screenProps,
|
||||
}
|
||||
end
|
||||
|
||||
function Navigator:render()
|
||||
local navigation = self.props.navigation
|
||||
local screenProps = self.state.screenProps
|
||||
local descriptors = self.state.descriptors
|
||||
|
||||
return Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, {
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
navigationConfig = navigationConfig,
|
||||
descriptors = descriptors,
|
||||
}))
|
||||
end
|
||||
|
||||
return Navigator
|
||||
end
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local createNavigator = require(script.Parent.createNavigator)
|
||||
local StackRouter = require(script.Parent.Parent.routers.StackRouter)
|
||||
local StackView = require(script.Parent.Parent.views.StackView.StackView)
|
||||
|
||||
return function(config)
|
||||
local router = StackRouter(config)
|
||||
return createNavigator(StackView, router, Cryo.Dictionary.join(config, {
|
||||
routes = Cryo.None, -- navigator config doesn't need routes
|
||||
}))
|
||||
end
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local createNavigator = require(script.Parent.createNavigator)
|
||||
local SwitchRouter = require(script.Parent.Parent.routers.SwitchRouter)
|
||||
local SwitchView = require(script.Parent.Parent.views.SwitchView)
|
||||
|
||||
--[[
|
||||
Creates a navigator component that provides simple screen "switcher" behavior.
|
||||
Each page is mutually exclusive and no transition animation is used.
|
||||
|
||||
Additional config options:
|
||||
order : List<string>
|
||||
Specifies the index order for page components, e.g. for use with tab bars.
|
||||
keepVisitedScreensMounted : Boolean (false)
|
||||
Set to true if you want to keep previously visited screens mounted for better performance.
|
||||
resetOnBlur : Boolean (true)
|
||||
Set to false if you want to preserve existing state for child navigators.
|
||||
backBehavior : BackBehavior (None)
|
||||
Set to BackBehavior.InitialRoute to allow a goBack() operation to return to the
|
||||
initial route name. By default, the SwitchNavigator will not do anything on a back action.
|
||||
]]
|
||||
return function(config)
|
||||
local router = SwitchRouter(config)
|
||||
return createNavigator(SwitchView, router, Cryo.Dictionary.join(config, {
|
||||
routes = Cryo.None, -- navigator config doesn't need routes, remove from props
|
||||
}))
|
||||
end
|
||||
|
||||
+565
@@ -0,0 +1,565 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local StackActions = require(script.Parent.Parent.StackActions)
|
||||
local KeyGenerator = require(script.Parent.Parent.utils.KeyGenerator)
|
||||
local StateUtils = require(script.Parent.Parent.StateUtils)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local createConfigGetter = require(script.Parent.createConfigGetter)
|
||||
local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol)
|
||||
local NoneSymbol = require(script.Parent.Parent.NoneSymbol)
|
||||
|
||||
local STACK_ROUTER_ROOT_KEY = "StackRouterRoot"
|
||||
local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN")
|
||||
|
||||
local defaultActionCreators = function() return {} end
|
||||
|
||||
local function behavesLikePushAction(action)
|
||||
return action.type == NavigationActions.Navigate or
|
||||
action.type == StackActions.Push
|
||||
end
|
||||
|
||||
local function isResetToRootStack(action)
|
||||
return action.type == StackActions.Reset and action.key == NoneSymbol
|
||||
end
|
||||
|
||||
return function(config)
|
||||
validate(type(config) == "table", "config must be a table")
|
||||
|
||||
local routeConfigs = validateRouteConfigMap(config.routes)
|
||||
local routeNames = Cryo.Dictionary.keys(routeConfigs)
|
||||
|
||||
-- find child child routers
|
||||
local childRouters = {}
|
||||
for _, routeName in ipairs(routeNames) do
|
||||
local screen = getScreenForRouteName(routeConfigs, routeName)
|
||||
if type(screen) == "table" and screen.router then
|
||||
-- if it has a router then it's a navigator
|
||||
childRouters[routeName] = screen.router
|
||||
else
|
||||
-- TODO: This is a hack to make this code behave like React-Navigation's usage of
|
||||
-- null and undefined values for childRouters. We should come up with a better approach.
|
||||
childRouters[routeName] = CHILD_IS_SCREEN
|
||||
end
|
||||
end
|
||||
|
||||
local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators
|
||||
|
||||
local initialRouteParams = config.initialRouteParams or {}
|
||||
local initialRouteName = validate(config.initialRouteName, "initialRouteName must be provided")
|
||||
|
||||
local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName)
|
||||
|
||||
-- dump an error if initialRouteName is not in routes.
|
||||
if initialRouteIndex == nil then
|
||||
local availableRouteStr = ""
|
||||
for _, name in ipairs(routeNames) do
|
||||
availableRouteStr = availableRouteStr .. name .. ","
|
||||
end
|
||||
|
||||
error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2)
|
||||
end
|
||||
|
||||
local initialChildRouter = childRouters[initialRouteName]
|
||||
|
||||
local function getInitialState()
|
||||
local route = {}
|
||||
|
||||
if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then
|
||||
route = initialChildRouter.getStateForAction(NavigationActions.init({
|
||||
params = initialRouteParams,
|
||||
}))
|
||||
end
|
||||
|
||||
local initialRouteConfig = routeConfigs[initialRouteName]
|
||||
local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params or {}
|
||||
|
||||
local params = Cryo.Dictionary.join(
|
||||
initialRouteConfigParams, -- params set in routes table!
|
||||
route.params or {},
|
||||
initialRouteParams or {} -- params provided at top level
|
||||
)
|
||||
|
||||
local initialRouteKey = config.initialRouteKey
|
||||
route = Cryo.Dictionary.join(route, params, {
|
||||
routeName = initialRouteName,
|
||||
key = initialRouteKey or KeyGenerator.generateKey()
|
||||
})
|
||||
|
||||
return {
|
||||
key = STACK_ROUTER_ROOT_KEY,
|
||||
isTransitioning = false,
|
||||
index = 1,
|
||||
routes = { route }
|
||||
}
|
||||
end
|
||||
|
||||
local function getParamsForRouteAndAction(routeName, action)
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
if type(routeConfig) == "table" and routeConfig.params then
|
||||
return Cryo.Dictionary.join(routeConfig.params, action.params)
|
||||
else
|
||||
return action.params
|
||||
end
|
||||
end
|
||||
|
||||
-- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly.
|
||||
local strippedChildRouters = {}
|
||||
for routerName, router in pairs(childRouters) do
|
||||
if router ~= CHILD_IS_SCREEN then
|
||||
strippedChildRouters[routerName] = router
|
||||
end
|
||||
end
|
||||
|
||||
local StackRouter = {
|
||||
childRouters = strippedChildRouters,
|
||||
getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions),
|
||||
_CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes
|
||||
}
|
||||
|
||||
function StackRouter.getComponentForState(state)
|
||||
local activeChildRoute = state.routes[state.index] or {}
|
||||
local routeName = activeChildRoute.routeName
|
||||
validate(routeName, "There is no route defined for index '%d'. " ..
|
||||
"Make sure that you passed in a navigation state with a " ..
|
||||
"valid stack index.", state.index)
|
||||
|
||||
local childRouter = childRouters[routeName]
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
return childRouters[routeName].getComponentForState(activeChildRoute)
|
||||
end
|
||||
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
function StackRouter.getComponentForRouteName(routeName)
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
function StackRouter.getActionCreators(route, navStateKey)
|
||||
return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), {
|
||||
pop = function(n, params)
|
||||
return StackActions.pop(Cryo.Dictionary.join({
|
||||
n = n,
|
||||
}, params or {}))
|
||||
end,
|
||||
popToTop = function(params)
|
||||
return StackActions.popToTop(params)
|
||||
end,
|
||||
push = function(routeName, params, action)
|
||||
return StackActions.push({
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
action = action,
|
||||
})
|
||||
end,
|
||||
replace = function(replaceWith, params, action, newKey)
|
||||
if type(replaceWith) == "string" then
|
||||
return StackActions.replace({
|
||||
routeName = replaceWith,
|
||||
params = params,
|
||||
action = action,
|
||||
key = route.key,
|
||||
newKey = newKey,
|
||||
})
|
||||
end
|
||||
|
||||
validate(type(replaceWith) == "table", "replaceWith must be a table or string")
|
||||
validate(params == nil, "params cannot be provided to .replace() when specifying a table")
|
||||
validate(action == nil, "Child action cannot be provided to .replace() when specifying a table")
|
||||
validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table")
|
||||
|
||||
return StackActions.replace(replaceWith)
|
||||
end,
|
||||
reset = function(actions, index)
|
||||
local resetIndex = index
|
||||
if index == nil then
|
||||
resetIndex = #actions
|
||||
end
|
||||
|
||||
return StackActions.reset({
|
||||
actions = actions,
|
||||
index = resetIndex,
|
||||
key = navStateKey,
|
||||
})
|
||||
end,
|
||||
dismiss = function()
|
||||
return NavigationActions.back({
|
||||
key = navStateKey,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function StackRouter.getStateForAction(action, state)
|
||||
-- Set up initial state if needed
|
||||
state = state or getInitialState()
|
||||
|
||||
local activeChildRoute = state.routes[state.index]
|
||||
|
||||
if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then
|
||||
local activeChildRouter = childRouters[activeChildRoute.routeName]
|
||||
if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then
|
||||
local route = activeChildRouter.getStateForAction(action, activeChildRoute)
|
||||
if route ~= nil and route ~= activeChildRoute then
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
activeChildRoute.key,
|
||||
route,
|
||||
action.type == NavigationActions.SetParams -- don't change index for setParam action
|
||||
)
|
||||
end
|
||||
end
|
||||
elseif action.type == NavigationActions.Navigate then
|
||||
-- Traverse routes from top of the stack to the bottom; active route has first opportunity
|
||||
for i = #state.routes, 1, -1 do
|
||||
local childRoute = state.routes[i]
|
||||
local childRouter = childRouters[childRoute.routeName]
|
||||
local childAction = action
|
||||
if action.routeName == childRoute.routeName and action.action then
|
||||
childAction = action.action
|
||||
end
|
||||
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local nextRouteState = childRouter.getStateForAction(childAction, childRoute)
|
||||
if nextRouteState == nil or nextRouteState ~= childRoute then
|
||||
local newState = StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState and nextRouteState.key or childRoute.key,
|
||||
nextRouteState and nextRouteState or childRoute
|
||||
)
|
||||
|
||||
local newTransitioning = state.isTransitioning
|
||||
if state.index ~= newState.index then
|
||||
newTransitioning = action.immediate ~= true
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(newState, {
|
||||
isTransitioning = newTransitioning,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle push and navigation actions. This must happen after focused child router
|
||||
-- has had its chance to handle the action.
|
||||
if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then
|
||||
local childRouter = childRouters[action.routeName]
|
||||
validate(action.type ~= StackActions.Push or action.key == nil,
|
||||
"StackRouter does not support key on the push action")
|
||||
|
||||
-- Before pushing new route, try to find existing one in the stack.
|
||||
local lastRouteIndex = nil
|
||||
for idx, route in ipairs(state.routes) do
|
||||
if (action.key and route.key == action.key) or (route.routeName == action.routeName) then
|
||||
lastRouteIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- An instance of this route exists already and we're dealing with a Navigate action.
|
||||
if action.type ~= StackActions.Push and lastRouteIndex ~= nil then
|
||||
-- If index or params have not changed, leave state alone
|
||||
if state.index == lastRouteIndex and not action.params then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Remove unused routes at tail
|
||||
local tailIndex = state.index == lastRouteIndex and lastRouteIndex or lastRouteIndex + 1
|
||||
local routes = Cryo.List.removeRange(state.routes, tailIndex, #state.routes)
|
||||
|
||||
-- Apply params if provided
|
||||
if action.params then
|
||||
local route = state.routes[lastRouteIndex]
|
||||
routes[lastRouteIndex] = Cryo.Dictionary.join(route, {
|
||||
params = Cryo.Dictionary.join(route.params or {}, action.params)
|
||||
})
|
||||
end
|
||||
|
||||
-- Return state with new index, changing isTransitioning only if index has changed
|
||||
local newIsTransitioning = state.isTransitioning
|
||||
if state.index ~= lastRouteIndex then
|
||||
newIsTransitioning = action.immediate ~= true
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = newIsTransitioning,
|
||||
index = lastRouteIndex,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
local route
|
||||
if childRouter ~= CHILD_IS_SCREEN then
|
||||
-- Delegate to child router
|
||||
local childAction = action.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(action.routeName, action)
|
||||
})
|
||||
|
||||
route = Cryo.Dictionary.join({
|
||||
-- TODO: Does it make sense to wipe out the params here, or to incorporate params at all?
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
}, childRouter.getStateForAction(childAction), {
|
||||
routeName = action.routeName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
})
|
||||
else
|
||||
-- Create new route from scratch
|
||||
route = {
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
routeName = action.routeName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(StateUtils.push(state, route), {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
})
|
||||
elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then
|
||||
-- Return original state to bubble the action up
|
||||
return state
|
||||
end
|
||||
|
||||
-- Handle navigation to other child routers that are not pushed yet.
|
||||
if behavesLikePushAction(action) then
|
||||
local childRouterNames = Cryo.Dictionary.keys(childRouters)
|
||||
for _, childRouterName in ipairs(childRouterNames) do
|
||||
local childRouter = childRouters[childRouterName]
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
-- Start with blank state for each child router
|
||||
local initChildRoute = childRouter.getStateForAction(NavigationActions.init())
|
||||
|
||||
-- Check to see if it handles our action
|
||||
local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute)
|
||||
|
||||
local routeToPush = nil
|
||||
if navigatedChildRoute == nil then
|
||||
-- Push initial route if the router returned nil when handling action
|
||||
routeToPush = initChildRoute
|
||||
elseif navigatedChildRoute ~= initChildRoute then
|
||||
-- Push new route if state changed in response to this action
|
||||
routeToPush = navigatedChildRoute
|
||||
end
|
||||
|
||||
if routeToPush then
|
||||
local route = Cryo.Dictionary.join(routeToPush, {
|
||||
routeName = childRouterName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
})
|
||||
|
||||
return Cryo.Dictionary.join(StateUtils.push(state, route), {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle pop-to-top behavior. This must happen after children have had a chance to handle
|
||||
-- the action, so that the inner stack always pops first.
|
||||
if action.type == StackActions.PopToTop then
|
||||
-- Refuse to handle pop to top if a key is given that does not correspond to this router
|
||||
if action.key and state.key ~= action.key then
|
||||
return state
|
||||
end
|
||||
|
||||
-- If we're already at the top then return current state to allow action to bubble up.
|
||||
if state.index <= 1 then
|
||||
return state
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
index = 1,
|
||||
routes = { state.routes[1] }
|
||||
})
|
||||
end
|
||||
|
||||
if action.type == StackActions.Replace then
|
||||
local routeIndex = nil
|
||||
|
||||
-- If there is no key, set index to last route in stack
|
||||
if not action.key and #state.routes > 0 then
|
||||
routeIndex = #state.routes
|
||||
else
|
||||
for idx, route in ipairs(state.routes) do
|
||||
if route.key == action.key then
|
||||
routeIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if routeIndex then
|
||||
local childRouter = childRouters[action.routeName]
|
||||
local childState = {}
|
||||
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local childAction = action.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(action.routeName, action)
|
||||
})
|
||||
|
||||
childState = childRouter.getStateForAction(childAction)
|
||||
end
|
||||
|
||||
-- shallow copy and update routes
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[routeIndex] = Cryo.Dictionary.join({
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
}, childState, {
|
||||
routeName = action.routeName,
|
||||
key = action.newKey or KeyGenerator.generateKey(),
|
||||
})
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.CompleteTransition and
|
||||
(action.key == nil or action.key == state.key) and
|
||||
action.toChildKey == state.routes[state.index].key and
|
||||
state.isTransitioning then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = false,
|
||||
})
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.SetParams then
|
||||
local key = action.key
|
||||
|
||||
local lastRouteIndex = nil
|
||||
local lastRoute = nil
|
||||
for idx, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
lastRouteIndex = idx
|
||||
lastRoute = route
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if lastRoute then
|
||||
local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {})
|
||||
-- shallow copy and update routes
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[lastRouteIndex] = Cryo.Dictionary.join(lastRoute, {
|
||||
params = params,
|
||||
})
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == StackActions.Reset then
|
||||
-- Only handle reset actions with matching key (or none)
|
||||
if action.key ~= nil and action.key ~= state.key then
|
||||
return state
|
||||
end
|
||||
|
||||
local specifiedActions = action.actions or {}
|
||||
|
||||
local newRoutes = {}
|
||||
for _, newStackAction in ipairs(specifiedActions) do
|
||||
local router = childRouters[newStackAction.routeName]
|
||||
|
||||
local childState = {}
|
||||
if router ~= nil and router ~= CHILD_IS_SCREEN then
|
||||
local childAction = newStackAction.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction),
|
||||
})
|
||||
|
||||
childState = router.getStateForAction(childAction)
|
||||
end
|
||||
|
||||
table.insert(newRoutes, Cryo.Dictionary.join({
|
||||
params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction)
|
||||
}, childState, {
|
||||
routeName = newStackAction.routeName,
|
||||
key = newStackAction.key or KeyGenerator.generateKey(),
|
||||
}))
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = newRoutes,
|
||||
index = action.index or #specifiedActions,
|
||||
})
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.Back or
|
||||
action.type == StackActions.Pop then
|
||||
local key = action.key
|
||||
local n = action.n
|
||||
local immediate = action.immediate
|
||||
|
||||
local backRouteIndex = state.index -- index to go back *FROM*
|
||||
if action.type == StackActions.Pop and n ~= nil then
|
||||
backRouteIndex = math.max(1, state.index - n + 1)
|
||||
elseif key and key ~= NoneSymbol then
|
||||
-- If key is specified and is not ours, we should NOT try to navigate back
|
||||
-- because it might be intended for our parent! (So clear the backRouteIndex.)
|
||||
backRouteIndex = 0
|
||||
for idx, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
backRouteIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if backRouteIndex > 1 then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = Cryo.List.removeRange(state.routes, backRouteIndex, #state.routes),
|
||||
index = backRouteIndex - 1,
|
||||
isTransitioning = immediate ~= true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- At this point, we've handled the behavior of the active route and any
|
||||
-- stack actions. Now we allow non-active child routers to try to process the action,
|
||||
-- and switch to them if they can handle it.
|
||||
local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil
|
||||
|
||||
-- Traverse from top of stack to bottom.
|
||||
for i = #state.routes, 1, -1 do
|
||||
local childRoute = state.routes[i]
|
||||
-- Skip over the active route since we already let it try.
|
||||
-- Also, skip calling getStateForAction on other child routers
|
||||
-- if the provided key is in the route's state
|
||||
if (childRoute.key ~= activeChildRoute.key) and
|
||||
(not keyIndex or childRoute.key == action.key) then
|
||||
local childRouter = childRouters[childRoute.routeName]
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local route = childRouter.getStateForAction(action, childRoute)
|
||||
if not route then
|
||||
return state
|
||||
end
|
||||
|
||||
if route ~= childRoute then
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
childRoute.key,
|
||||
route,
|
||||
-- don't change index for these action types
|
||||
action.type == NavigationActions.SetParams or
|
||||
action.type == NavigationActions.CompleteTransition
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
-- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support
|
||||
-- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support
|
||||
|
||||
return StackRouter
|
||||
end
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local BackBehavior = require(script.Parent.Parent.BackBehavior)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local createConfigGetter = require(script.Parent.createConfigGetter)
|
||||
local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local defaultActionCreators = function() return {} end
|
||||
|
||||
-- Until Cryo has a List function to do this, provide shallow copy+replace index
|
||||
local function immutableReplaceListIndex(list, index, value)
|
||||
local result = {}
|
||||
for i, ival in ipairs(list) do
|
||||
result[i] = ival
|
||||
end
|
||||
|
||||
result[index] = value
|
||||
return result
|
||||
end
|
||||
|
||||
local function childrenUpdateWithoutSwitchingIndex(actionType)
|
||||
return actionType == NavigationActions.SetParams or
|
||||
actionType == NavigationActions.CompleteTransition
|
||||
end
|
||||
|
||||
local function collectChildRouters(routeConfigs)
|
||||
local childRouters = {}
|
||||
for routeName, _ in pairs(routeConfigs) do
|
||||
local screen = getScreenForRouteName(routeConfigs, routeName)
|
||||
if type(screen) == "table" and screen.router then
|
||||
childRouters[routeName] = screen.router
|
||||
end
|
||||
end
|
||||
|
||||
return childRouters
|
||||
end
|
||||
|
||||
local function getParamsForRoute(routeConfigs, routeName, initialParams)
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
if type(routeConfig) == "table" and routeConfig.params then
|
||||
return Cryo.Dictionary.join(routeConfig.params, initialParams)
|
||||
else
|
||||
return initialParams
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return function(config)
|
||||
validate(type(config) == "table", "config must be a table")
|
||||
|
||||
local routeConfigs = validateRouteConfigMap(config.routes)
|
||||
|
||||
-- Order is how we map the active index into the list of possible routes.
|
||||
-- Lua does not guarantee any sense of order of table keys in dictionaries, so
|
||||
-- we have to require the initialRouteName parameter instead defaulting to the
|
||||
-- first route in the map.
|
||||
local order = config.order or Cryo.Dictionary.keys(routeConfigs)
|
||||
|
||||
local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators
|
||||
local initialRouteParams = config.initialRouteParams or {}
|
||||
|
||||
local initialRouteName = validate(config.initialRouteName,
|
||||
"initialRouteName must be provided")
|
||||
|
||||
local backBehavior = config.backBehavior or BackBehavior.None
|
||||
local backShouldNavigateToInitialRoute = backBehavior == BackBehavior.InitialRoute
|
||||
|
||||
local resetOnBlur = true
|
||||
if type(config.resetOnBlur) == "boolean" then
|
||||
resetOnBlur = config.resetOnBlur
|
||||
end
|
||||
|
||||
local initialRouteIndex = Cryo.List.find(order, initialRouteName)
|
||||
if initialRouteIndex == nil then
|
||||
local availableRouteStr = ""
|
||||
for _, name in ipairs(order) do
|
||||
availableRouteStr = availableRouteStr .. name .. ","
|
||||
end
|
||||
|
||||
error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2)
|
||||
end
|
||||
|
||||
local childRouters = collectChildRouters(routeConfigs)
|
||||
|
||||
local function resetChildRoute(routeName)
|
||||
-- TODO: Do we want to merge initialRouteParams on TOP of route-specific params?
|
||||
-- There is a comment in RoactNavigation that this is incorrect behavior, but they
|
||||
-- do it to be consistent with their StackRouter. Do we even need this feature?
|
||||
local initialParams = routeName == initialRouteName and initialRouteParams or {}
|
||||
local params = getParamsForRoute(routeConfigs, routeName, initialParams)
|
||||
local childRouter = childRouters[routeName]
|
||||
if childRouter then
|
||||
local childAction = NavigationActions.init()
|
||||
return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), {
|
||||
key = routeName,
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
})
|
||||
else
|
||||
return {
|
||||
key = routeName,
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function getNextState(prevState, possibleNextState)
|
||||
if not prevState then
|
||||
return possibleNextState
|
||||
end
|
||||
|
||||
if prevState.index ~= possibleNextState.index and resetOnBlur then
|
||||
local prevRouteName = prevState.routes[prevState.index].routeName
|
||||
local nextRoutes = immutableReplaceListIndex(
|
||||
possibleNextState.routes,
|
||||
prevState.index,
|
||||
resetChildRoute(prevRouteName))
|
||||
|
||||
return Cryo.Dictionary.join(possibleNextState, {
|
||||
routes = nextRoutes,
|
||||
})
|
||||
else
|
||||
return possibleNextState
|
||||
end
|
||||
end
|
||||
|
||||
local function getInitialState()
|
||||
return {
|
||||
routes = Cryo.List.map(order, resetChildRoute),
|
||||
index = initialRouteIndex,
|
||||
isTransitioning = false,
|
||||
}
|
||||
end
|
||||
|
||||
local SwitchRouter = {
|
||||
childRouters = childRouters,
|
||||
getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions)
|
||||
}
|
||||
|
||||
function SwitchRouter.getActionCreators(route, stateKey)
|
||||
return getCustomActionCreators(route, stateKey)
|
||||
end
|
||||
|
||||
function SwitchRouter.getStateForAction(action, inputState)
|
||||
local prevState = inputState and Cryo.Dictionary.join(inputState) or nil
|
||||
local state = inputState or getInitialState()
|
||||
local activeChildIndex = state.index
|
||||
|
||||
if action.type == NavigationActions.Init then
|
||||
-- TODO: React-Navigation has a comment that wonders why we merge params into child routes.
|
||||
-- Need to understand if we really want to do this.
|
||||
local params = action.params
|
||||
if params then
|
||||
state.routes = Cryo.List.map(state.routes, function(route)
|
||||
local initialParams = route.routeName == initialRouteName and initialRouteParams or {}
|
||||
return Cryo.Dictionary.join(route, {
|
||||
params = Cryo.Dictionary.join(route.params, params, initialParams)
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Let the active child try to handle the action first.
|
||||
local activeChildLastState = state.routes[state.index]
|
||||
local activeChildRouter = childRouters[order[state.index]]
|
||||
if activeChildRouter then
|
||||
local activeChildState = activeChildRouter.getStateForAction(action, activeChildLastState)
|
||||
if not activeChildState and inputState then
|
||||
-- Child ran into error with known inputState. Propagate to caller.
|
||||
return nil
|
||||
end
|
||||
|
||||
if activeChildState and activeChildState ~= activeChildLastState then
|
||||
local routes = immutableReplaceListIndex(state.routes, state.index, activeChildState)
|
||||
return getNextState(prevState, Cryo.Dictionary.join(state, {
|
||||
routes = routes
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
-- Child did not handle it, so try to process the action ourselves.
|
||||
local isBackEligible = not action.key or action.key == activeChildLastState.key
|
||||
if action.type == NavigationActions.Back then
|
||||
if isBackEligible and backShouldNavigateToInitialRoute then
|
||||
activeChildIndex = initialRouteIndex
|
||||
else
|
||||
return state
|
||||
end
|
||||
end
|
||||
|
||||
local didNavigate = false
|
||||
if action.type == NavigationActions.Navigate then
|
||||
for index, childId in ipairs(order) do
|
||||
if childId == action.routeName then
|
||||
activeChildIndex = index
|
||||
didNavigate = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if didNavigate then
|
||||
local childState = state.routes[activeChildIndex]
|
||||
local childRouter = childRouters[action.routeName]
|
||||
local newChildState = childState
|
||||
|
||||
if action.action and childRouter then
|
||||
local childStateUpdate = childRouter.getStateForAction(action.action, childState)
|
||||
if childStateUpdate then
|
||||
newChildState = childStateUpdate
|
||||
end
|
||||
end
|
||||
|
||||
if action.params then
|
||||
newChildState = Cryo.Dictionary.join(newChildState, {
|
||||
params = Cryo.Dictionary.join(newChildState.params or {}, action.params)
|
||||
})
|
||||
end
|
||||
|
||||
if newChildState ~= childState then
|
||||
local routes = immutableReplaceListIndex(state.routes, activeChildIndex, newChildState)
|
||||
local nextState = Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
index = activeChildIndex,
|
||||
})
|
||||
|
||||
return getNextState(prevState, nextState)
|
||||
elseif newChildState == childState and state.index == activeChildIndex and prevState then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.SetParams then
|
||||
local key = action.key
|
||||
local lastIndex, lastRoute
|
||||
for index, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
lastIndex = index
|
||||
lastRoute = route
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if lastRoute then
|
||||
local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params)
|
||||
local mergedRoute = Cryo.Dictionary.join(lastRoute, {
|
||||
params = params
|
||||
})
|
||||
|
||||
local routes = immutableReplaceListIndex(state.routes, lastIndex, mergedRoute)
|
||||
return getNextState(prevState, Cryo.Dictionary.join(state, {
|
||||
routes = routes
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
if activeChildIndex ~= state.index then
|
||||
return getNextState(prevState, Cryo.Dictionary.join(state, { index = activeChildIndex }))
|
||||
elseif didNavigate and not inputState then
|
||||
return state
|
||||
elseif didNavigate then
|
||||
return Cryo.Dictionary.join(state)
|
||||
end
|
||||
|
||||
-- Let other children handle it and switch to first child that returns a new state
|
||||
local index = state.index
|
||||
local routes = state.routes
|
||||
|
||||
for i, childId in ipairs(order) do
|
||||
if i ~= index then
|
||||
local childRouter = childRouters[childId]
|
||||
local childState = routes[i]
|
||||
if childRouter then
|
||||
childState = childRouter.getStateForAction(action, childState)
|
||||
end
|
||||
|
||||
if not childState then
|
||||
index = i
|
||||
break
|
||||
end
|
||||
|
||||
if childState ~= routes[i] then
|
||||
routes = immutableReplaceListIndex(routes, i, childState)
|
||||
index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Nested routers can be updated after switching children with actions such as SetParams
|
||||
-- and CompleteTransition
|
||||
if childrenUpdateWithoutSwitchingIndex(action.type) then
|
||||
index = state.index
|
||||
end
|
||||
|
||||
if index ~= state.index or routes ~= state.routes then
|
||||
return getNextState(prevState, Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
routes = routes,
|
||||
}))
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
function SwitchRouter.getComponentForState(state)
|
||||
local activeRoute = state.routes[state.index] or {}
|
||||
local routeName = activeRoute.routeName
|
||||
validate(routeName, "There is no route defined for index '%d'. " ..
|
||||
"Make sure that you passed in a navigation state with a " ..
|
||||
"valid tab/screen index.", state.index)
|
||||
|
||||
local childRouter = childRouters[routeName]
|
||||
if childRouter then
|
||||
return childRouter.getComponentForState(state.routes[state.index])
|
||||
else
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
end
|
||||
|
||||
function SwitchRouter.getComponentForRouteName(routeName)
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
-- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support
|
||||
-- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support
|
||||
|
||||
return SwitchRouter
|
||||
end
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local SwitchRouter = require(script.Parent.SwitchRouter)
|
||||
local BackBehavior = require(script.Parent.Parent.BackBehavior)
|
||||
|
||||
return function(config)
|
||||
-- Provide defaults suitable for tab routing.
|
||||
local modifiedConfig = Cryo.Dictionary.join({
|
||||
resetOnBlur = false,
|
||||
backBehavior = BackBehavior.InitialRoute,
|
||||
}, config)
|
||||
|
||||
return SwitchRouter(modifiedConfig)
|
||||
end
|
||||
+1435
File diff suppressed because it is too large
Load Diff
+691
@@ -0,0 +1,691 @@
|
||||
return function()
|
||||
local SwitchRouter = require(script.Parent.Parent.SwitchRouter)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
local BackBehavior = require(script.Parent.Parent.Parent.BackBehavior)
|
||||
|
||||
local function expectError(functor, msg)
|
||||
local status, err = pcall(functor)
|
||||
|
||||
if status ~= false then
|
||||
error("expectError: Test function should have thrown error, but it passed", 2)
|
||||
end
|
||||
if string.find(err, msg) == nil then
|
||||
error(string.format("expectError: Expected error message '%s' not found in actual message: '%s'", msg, err), 2)
|
||||
end
|
||||
end
|
||||
|
||||
it("should be a function", function()
|
||||
expect(type(SwitchRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should throw when passed a non-table", function()
|
||||
expectError(function()
|
||||
SwitchRouter(5)
|
||||
end, "config must be a table")
|
||||
end)
|
||||
|
||||
it("should throw for invalid routes config", function()
|
||||
expect(function()
|
||||
SwitchRouter({ routes = 5 })
|
||||
end).to.throw() -- throw is from validateRouteConfigs, so do not depend on message
|
||||
end)
|
||||
|
||||
it("should throw if initialRouteName is not provided", function()
|
||||
expectError(function()
|
||||
SwitchRouter({ routes = {
|
||||
Foo = function() end,
|
||||
} })
|
||||
end, "initialRouteName must be provided")
|
||||
end)
|
||||
|
||||
it("should throw if initialRouteName is not found in routes table", function()
|
||||
expectError(function()
|
||||
SwitchRouter({
|
||||
routes = {
|
||||
Foo = function() end,
|
||||
Bar = function() end,
|
||||
},
|
||||
initialRouteName = "MyRoute",
|
||||
})
|
||||
end, "Invalid initialRouteName 'MyRoute'. Must be one of %[Bar,Foo,%]")
|
||||
end)
|
||||
|
||||
it("should expose childRouters as a member", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = "A",
|
||||
},
|
||||
},
|
||||
Bar = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = "B",
|
||||
},
|
||||
},
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
expect(router.childRouters.Foo).to.equal("A")
|
||||
expect(router.childRouters.Bar).to.equal("B")
|
||||
end)
|
||||
|
||||
describe("getScreenOptions tests", function()
|
||||
it("should correctly configure default screen options", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
}
|
||||
}
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("FooTitle")
|
||||
end)
|
||||
|
||||
it("should correctly configure route-specified screen options", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
},
|
||||
navigationOptions = { title = "RouteFooTitle" },
|
||||
}
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("RouteFooTitle")
|
||||
end)
|
||||
|
||||
it("should correctly configure component-specified screen options", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentFooTitle" },
|
||||
},
|
||||
}
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("ComponentFooTitle")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getActionCreators tests", function()
|
||||
it("should return empty action creators table if none are provided", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key")
|
||||
|
||||
local fieldCount = 0
|
||||
for _ in pairs(actionCreators) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
|
||||
expect(fieldCount).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should call custom action creators function if provided", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
getCustomActionCreators = function()
|
||||
return { a = 1 }
|
||||
end,
|
||||
})
|
||||
|
||||
local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key")
|
||||
expect(actionCreators.a).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getComponentForState tests", function()
|
||||
it("should return component matching requested state", function()
|
||||
local testComponent = function() end
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = testComponent },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local component = router.getComponentForState({
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
},
|
||||
index = 1,
|
||||
})
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
|
||||
it("should throw if there is no route matching active index", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
expectError(function()
|
||||
router.getComponentForState({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
},
|
||||
index = 2,
|
||||
})
|
||||
end, "There is no route defined for index '2'. " ..
|
||||
"Make sure that you passed in a navigation state with a " ..
|
||||
"valid tab/screen index.")
|
||||
|
||||
end)
|
||||
|
||||
it("should descend child router for requested route", function()
|
||||
local testComponent = function() end
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = { screen = testComponent }
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
}
|
||||
},
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local component = router.getComponentForState({
|
||||
routes = {
|
||||
{
|
||||
routeName = "Foo",
|
||||
routes = { -- Child router's routes
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
},
|
||||
index = 1,
|
||||
})
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getComponentForRouteName tests", function()
|
||||
it("should return a component that matches the given route name", function()
|
||||
local testComponent = function() end
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = testComponent },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local component = router.getComponentForRouteName("Foo")
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getStateForAction tests", function()
|
||||
it("should return initial state for init action", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(#state.routes).to.equal(2)
|
||||
expect(state.routes[state.index].routeName).to.equal("Foo")
|
||||
expect(state.isTransitioning).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should adjust initial state index to match initialRouteName's index", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state.routes[state.index].routeName).to.equal("Foo")
|
||||
|
||||
local router2 = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local state2 = router2.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state2.routes[state2.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should respect optional order property", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state.routes[1].routeName).to.equal("Foo")
|
||||
expect(state.routes[2].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should incorporate child router state", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
City = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
local activeState = state.routes[state.index]
|
||||
expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name
|
||||
expect(activeState.routes[activeState.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should let active child handle non-init action first", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = { screen = function() end },
|
||||
City = { screen = function() end },
|
||||
},
|
||||
order = { "Bar", "City" },
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
State = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "State" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }))
|
||||
expect(state.routes[1].index).to.equal(2)
|
||||
expect(state.index).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should go back to initial route index if BackBehavior.InitialRoute", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
backBehavior = BackBehavior.InitialRoute,
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should not change state on back action if BackBehavior.None", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState).to.equal(prevState)
|
||||
end)
|
||||
|
||||
it("should change active route on navigate", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }))
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(newState.routes[newState.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should pass sub-action to child router on navigate", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
City = { screen = function() end },
|
||||
State = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "City",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Bar",
|
||||
action = NavigationActions.navigate({ routeName = "State" }),
|
||||
}))
|
||||
|
||||
local activeRoute = newState.routes[newState.index]
|
||||
expect(activeRoute.routeName).to.equal("Bar")
|
||||
expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State")
|
||||
end)
|
||||
|
||||
it("should return initial state if navigating to active child without previous state", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
City = { render = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Foo",
|
||||
}))
|
||||
|
||||
expect(newState.routes[newState.index].routeName).to.equal("Foo")
|
||||
expect(newState.isTransitioning).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should reset state for deactivated route by default", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = { a = 1 } },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params.a).to.equal(nil) -- should be empty
|
||||
end)
|
||||
|
||||
it("should not reset state for deactivated route if resetOnBlur is false", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
resetOnBlur = false,
|
||||
})
|
||||
|
||||
local testParams = { a = 1 }
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = testParams },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params).to.equal(testParams)
|
||||
end)
|
||||
|
||||
it("should set params on route for setParams action", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.setParams({
|
||||
key = "Foo", -- By default, key == routeName
|
||||
params = { a = 1 },
|
||||
}))
|
||||
|
||||
expect(newState.routes[newState.index].params.a).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should preserve route configured params for child router", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = {
|
||||
screen = function() end,
|
||||
params = { a = 2 },
|
||||
},
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
router = childRouter,
|
||||
},
|
||||
City = { render = function() end },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init())
|
||||
expect(state.routes[state.index].params.a).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should merge initialRouteParams with initial route's own params", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
},
|
||||
Bar = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
},
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
initialRouteParams = { a = 2, b = 3 },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init())
|
||||
expect(state.routes[1].params.a).to.equal(2)
|
||||
expect(state.routes[1].params.b).to.equal(3)
|
||||
expect(state.routes[2].params.a).to.equal(1)
|
||||
expect(state.routes[2].params.b).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should merge init action params with initial route's own params and initialRouteParams", function()
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end, params = { a = 1 } }
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
initialRouteParams = { c = 3 },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } }))
|
||||
expect(state.routes[1].params.a).to.equal(1)
|
||||
expect(state.routes[1].params.b).to.equal(2)
|
||||
expect(state.routes[1].params.c).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should merge navigate action params for child router", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = {
|
||||
screen = function() end,
|
||||
params = { a = 2 },
|
||||
},
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Bar",
|
||||
params = { b = 3 },
|
||||
}))
|
||||
|
||||
expect(state.routes[1].routes[1].params.a).to.equal(2)
|
||||
expect(state.routes[1].routes[1].params.b).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should propagate a child router getStateForAction failure to caller", function()
|
||||
local childRouter = SwitchRouter({
|
||||
routes = {
|
||||
Bar = { screen = function() end },
|
||||
},
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
routes = {
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
-- need to properly initialize state because we're being abusive of getStateForAction
|
||||
local initialState = router.getStateForAction(NavigationActions.init())
|
||||
|
||||
childRouter.getStateForAction = function() return nil end
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState)
|
||||
expect(state).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
return function()
|
||||
local TabRouter = require(script.Parent.Parent.TabRouter)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
|
||||
-- NOTE: Most functional tests are covered by SwitchRouter.spec.lua
|
||||
-- We just check that we can mount a basic case, and check that our custom
|
||||
-- defaults for resetOnBlur and backBehavior work as expected.
|
||||
|
||||
it("should return a component that matches the given route name", function()
|
||||
local testComponent = function() end
|
||||
local router = TabRouter({
|
||||
routes = {
|
||||
Foo = { screen = testComponent },
|
||||
},
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local component = router.getComponentForRouteName("Foo")
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
|
||||
it("should not reset state for deactivated route", function()
|
||||
local router = TabRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local testParams = { a = 1 }
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = testParams },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params).to.equal(testParams)
|
||||
end)
|
||||
|
||||
it("should go back to initial route index", function()
|
||||
local router = TabRouter({
|
||||
routes = {
|
||||
Foo = { render = function() end },
|
||||
Bar = { render = function() end },
|
||||
},
|
||||
order = { "Foo", "Bar" },
|
||||
initialRouteName = "Foo",
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
end
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
return function()
|
||||
local createConfigGetter = require(script.Parent.Parent.createConfigGetter)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
|
||||
it("should return a function", function()
|
||||
local result = createConfigGetter({}, {})
|
||||
expect(type(result)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a screen config when called", function()
|
||||
local HomeScreen = Roact.Component:extend("HomeScreen")
|
||||
HomeScreen.navigationOptions = function(props)
|
||||
local username = props.navigation.state.params and
|
||||
props.navigation.state.params.user or "anonymous"
|
||||
|
||||
return {
|
||||
title = string.format("Welcome %s", username),
|
||||
gesturesEnabled = true,
|
||||
}
|
||||
end
|
||||
function HomeScreen:render() return nil end
|
||||
|
||||
local SettingsScreen = Roact.Component:extend("SettingsScreen")
|
||||
SettingsScreen.navigationOptions = {
|
||||
title = "Settings!!!",
|
||||
gesturesEnabled = false,
|
||||
}
|
||||
function SettingsScreen:render() return nil end
|
||||
|
||||
local NotificationScreen = Roact.Component:extend("NotificationScreen")
|
||||
NotificationScreen.navigationOptions = function(props)
|
||||
local gesturesEnabled = true
|
||||
if props.navigation.state.params then
|
||||
gesturesEnabled = not props.navigation.state.params.fullscreen
|
||||
end
|
||||
|
||||
return {
|
||||
title = "42",
|
||||
gesturesEnabled = gesturesEnabled
|
||||
}
|
||||
end
|
||||
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = { screen = HomeScreen },
|
||||
Settings = { screen = SettingsScreen },
|
||||
Notifications = {
|
||||
screen = NotificationScreen,
|
||||
navigationOptions = {
|
||||
title = "10 new notifications",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
local routes = {
|
||||
{ key = "A", routeName = "Home", },
|
||||
{ key = "B", routeName = "Home", params = { user = "jane"} },
|
||||
{ key = "C", routeName = "Settings", },
|
||||
{ key = "D", routeName = "Notifications", },
|
||||
{ key = "E", routeName = "Notifications", params = { fullscreen = true } },
|
||||
}
|
||||
|
||||
expect(getScreenOptions({ state = routes[1] }, {}).title
|
||||
).to.equal("Welcome anonymous")
|
||||
|
||||
expect(getScreenOptions({ state = routes[2] }, {}).title
|
||||
).to.equal("Welcome jane")
|
||||
|
||||
expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled
|
||||
).to.equal(true)
|
||||
|
||||
expect(getScreenOptions({ state = routes[3] }, {}).title
|
||||
).to.equal("Settings!!!")
|
||||
|
||||
expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled
|
||||
).to.equal(false)
|
||||
|
||||
expect(getScreenOptions({ state = routes[4] }, {}).title
|
||||
).to.equal("10 new notifications")
|
||||
|
||||
expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled
|
||||
).to.equal(true)
|
||||
|
||||
expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled
|
||||
).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should override default config with component-specific config", function()
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentHome" },
|
||||
},
|
||||
},
|
||||
defaultNavigationOptions = { title = "DefaultTitle" },
|
||||
})
|
||||
|
||||
expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome")
|
||||
end)
|
||||
|
||||
it("should override component-specific config with route-specific config", function()
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentHome" },
|
||||
},
|
||||
navigationOptions = { title = "RouteHome" },
|
||||
},
|
||||
defaultNavigationOptions = { title = "DefaultTitle" },
|
||||
})
|
||||
|
||||
expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome")
|
||||
end)
|
||||
end
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
return function()
|
||||
local getChildRouter = require(script.Parent.Parent.getChildRouter)
|
||||
|
||||
it("should throw if router is not a table", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter(5, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "router must be a table")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw if routeName is not a string", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter({}, 5)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "routeName must be a string")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return child router if found", function()
|
||||
local childRouter = {}
|
||||
local result = getChildRouter({
|
||||
childRouters = {
|
||||
myRoute = childRouter,
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(childRouter)
|
||||
end)
|
||||
|
||||
it("should look up component router if no child router is found", function()
|
||||
local component = { router = {} }
|
||||
|
||||
local result = getChildRouter({
|
||||
getComponentForRouteName = function(routeName)
|
||||
if routeName == "myRoute" then
|
||||
return component
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(component.router)
|
||||
end)
|
||||
|
||||
it("should throw if no child routers are specified and getComponentForRouteName is not a function", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter({
|
||||
getComponentForRouteName = 5
|
||||
}, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified")
|
||||
).to.never.equal(nil)
|
||||
end)
|
||||
end
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
return function()
|
||||
local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
|
||||
local function expectError(functor, msg)
|
||||
local status, err = pcall(functor)
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, msg)).to.never.equal(nil)
|
||||
end
|
||||
|
||||
it("should return a table with correct functions when called", function()
|
||||
local result = getNavigationActionCreators()
|
||||
expect(type(result.goBack)).to.equal("function")
|
||||
expect(type(result.navigate)).to.equal("function")
|
||||
expect(type(result.setParams)).to.equal("function")
|
||||
end)
|
||||
|
||||
describe("goBack tests", function()
|
||||
it("should return a Back action when called", function()
|
||||
local result = getNavigationActionCreators().goBack("theKey")
|
||||
expect(result.type).to.equal(NavigationActions.Back)
|
||||
expect(result.key).to.equal("theKey")
|
||||
end)
|
||||
|
||||
it("should throw when route.key is not a string", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators({ key = 5 }).goBack()
|
||||
end, "%.goBack%(%): key should be a string")
|
||||
end)
|
||||
|
||||
it("should fall back to route.key if key is not provided", function()
|
||||
local result = getNavigationActionCreators({ key = "routeKey" }).goBack()
|
||||
expect(result.key).to.equal("routeKey")
|
||||
end)
|
||||
|
||||
it("should override route.key if key is provided", function()
|
||||
local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey")
|
||||
expect(result.key).to.equal("theKey")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("navigate tests", function()
|
||||
it("should return a Navigate action when called", function()
|
||||
local theParams = {}
|
||||
local childAction = {}
|
||||
local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction)
|
||||
expect(result.type).to.equal(NavigationActions.Navigate)
|
||||
expect(result.routeName).to.equal("theRoute")
|
||||
expect(result.params).to.equal(theParams)
|
||||
expect(result.action).to.equal(childAction)
|
||||
end)
|
||||
|
||||
it("should return a navigate action with matching properties when called with a table", function()
|
||||
local testNavigateTo = {
|
||||
routeName = "theRoute",
|
||||
params = {},
|
||||
action = {},
|
||||
}
|
||||
|
||||
local result = getNavigationActionCreators().navigate(testNavigateTo)
|
||||
expect(result.type).to.equal(NavigationActions.Navigate)
|
||||
expect(result.routeName).to.equal("theRoute")
|
||||
expect(result.params).to.equal(testNavigateTo.params)
|
||||
expect(result.action).to.equal(testNavigateTo.action)
|
||||
end)
|
||||
|
||||
it("should throw when navigateTo is not a valid type", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate(5)
|
||||
end, "%.navigate%(%): navigateTo must be a string or table")
|
||||
end)
|
||||
|
||||
it("should throw when params is provided with a table navigateTo", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate({}, {})
|
||||
end, "%.navigate%(%): params can only be provided with a string navigateTo value")
|
||||
end)
|
||||
|
||||
it("should throw when action is provided with a table navigateTo", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate({}, nil, {})
|
||||
end, "%.navigate%(%): child action can only be provided with a string navigateTo value")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setParams tests", function()
|
||||
it("should return a SetParams action when called", function()
|
||||
local theParams = {}
|
||||
local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams)
|
||||
expect(result.type).to.equal(NavigationActions.SetParams)
|
||||
expect(result.key).to.equal("theKey")
|
||||
expect(result.params).to.equal(theParams)
|
||||
end)
|
||||
|
||||
it("should throw when called by a root navigator", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators({}).setParams({})
|
||||
end, "%.setParams%(%): cannot be called by the root navigator")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
return function()
|
||||
local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName)
|
||||
|
||||
it("should throw for invalid arg types", function()
|
||||
local status, err = pcall(function()
|
||||
getScreenForRouteName("", "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "routeConfigs must be a table")).to.never.equal(nil)
|
||||
|
||||
status, err = pcall(function()
|
||||
getScreenForRouteName({}, 5)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "routeName must be a string")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw if requested route is not present within table", function()
|
||||
local status, err = pcall(function()
|
||||
getScreenForRouteName({
|
||||
notMyRoute = function() return "foo" end
|
||||
}, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "There is no route defined for key 'myRoute'.")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return raw table if screen and getScreen are not props", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = screenComponent
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should return screen prop if it is set in route data table", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = {
|
||||
screen = screenComponent
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should return object returned by getScreen function if object is valid Roact element", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = {
|
||||
getScreen = function() return screenComponent end
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should throw if getScreen does not return a valid Roact element", function()
|
||||
local status, err = pcall(function()
|
||||
getScreenForRouteName({
|
||||
myRoute = {
|
||||
getScreen = function() return nil end
|
||||
}
|
||||
}, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "The getScreen function defined for route 'myRoute'" ..
|
||||
" did not return a valid screen or navigator")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw if screen is not a valid Roact element", function()
|
||||
local status, err = pcall(function()
|
||||
getScreenForRouteName({
|
||||
myRoute = {
|
||||
screen = 5,
|
||||
}
|
||||
}, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "screen param for key 'myRoute' must be a valid Roact component.")).to.never.equal(nil)
|
||||
end)
|
||||
end
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap)
|
||||
|
||||
local function expectError(functor, msg)
|
||||
local status, err = pcall(functor)
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, msg)).to.never.equal(nil)
|
||||
end
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
it("should throw if routeConfigs is not a table", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap(5)
|
||||
end, "routeConfigs must be a table")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs is empty", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({})
|
||||
end, "Please specify at least one route when configuring a navigator%.")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs contains an invalid Roact element", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = 5,
|
||||
})
|
||||
end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.")
|
||||
end)
|
||||
|
||||
it("should throw if getScreen returns invalid Roact element", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
getScreen = function() end,
|
||||
},
|
||||
})
|
||||
end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.")
|
||||
end)
|
||||
|
||||
it("should throw when both screen and getScreen are provided for same component", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
screen = "TheScreen",
|
||||
getScreen = function() return TestComponent end,
|
||||
}
|
||||
})
|
||||
end, "Route 'myRoute' should provide 'screen' or 'getScreen', but not both%.")
|
||||
end)
|
||||
|
||||
it("should throw for a simple table where screen is not a Roact Function/Stateful component", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
screen = {},
|
||||
}
|
||||
})
|
||||
end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.")
|
||||
end)
|
||||
|
||||
it("should throw for a non-function getScreen", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
getScreen = 5
|
||||
}
|
||||
})
|
||||
end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.")
|
||||
end)
|
||||
|
||||
it("should throw for a Host Component", function()
|
||||
expectError(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
aFrame = "Frame"
|
||||
}
|
||||
})
|
||||
end, "The component for route 'myRoute' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.")
|
||||
end)
|
||||
|
||||
|
||||
it("should pass for valid basic routeConfigs", function()
|
||||
validateRouteConfigMap({
|
||||
basicComponentRoute = TestComponent,
|
||||
functionalComponentRoute = function() end,
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid screen prop type routeConfigs", function()
|
||||
validateRouteConfigMap({
|
||||
basicComponentRoute = {
|
||||
screen = TestComponent,
|
||||
},
|
||||
functionalComponentRoute = {
|
||||
screen = function() end,
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid getScreen route configs", function()
|
||||
validateRouteConfigMap({
|
||||
getScreenRoute = {
|
||||
getScreen = function() return TestComponent end,
|
||||
}
|
||||
})
|
||||
end)
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions)
|
||||
|
||||
it("should not throw when there are no problems", function()
|
||||
validateScreenOptions({ title = "foo" }, { routeName = "foo" })
|
||||
end)
|
||||
|
||||
it("should throw error if no routeName is provided", function()
|
||||
local status, err = pcall(function()
|
||||
validateScreenOptions({ title = "bar" }, {})
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw error for options with function for title", function()
|
||||
expect(function()
|
||||
validateScreenOptions({
|
||||
title = function() end,
|
||||
}, { routeName = "foo" })
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local validateScreenOptions = require(script.Parent.validateScreenOptions)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local function applyConfig(configurer, navigationOptions, configProps)
|
||||
navigationOptions = navigationOptions or {}
|
||||
|
||||
local configurerType = type(configurer)
|
||||
if configurerType == "function" then
|
||||
return Cryo.Dictionary.join(navigationOptions,
|
||||
configurer(Cryo.Dictionary.join(configProps or {}, {
|
||||
navigationOptions = navigationOptions
|
||||
})))
|
||||
elseif configurerType == "table" then
|
||||
return Cryo.Dictionary.join(navigationOptions, configurer)
|
||||
else
|
||||
return navigationOptions
|
||||
end
|
||||
end
|
||||
|
||||
return function(routeConfigs, navigatorScreenConfig)
|
||||
return function(navigation, screenProps)
|
||||
screenProps = screenProps or {}
|
||||
local route = navigation.state
|
||||
|
||||
validate(type(route) == "table", "navigation.state must be a table")
|
||||
validate(type(route.routeName == "string"), "routeName must be a string")
|
||||
|
||||
local component = getScreenForRouteName(routeConfigs, route.routeName)
|
||||
local routeConfig = routeConfigs[route.routeName]
|
||||
|
||||
local routeScreenConfig = nil
|
||||
if routeConfig ~= component then
|
||||
routeScreenConfig = routeConfig.navigationOptions
|
||||
end
|
||||
|
||||
local componentScreenConfig = type(component) == "table"
|
||||
and component.navigationOptions or {}
|
||||
|
||||
local configOptions = {
|
||||
navigation = navigation,
|
||||
screenProps = screenProps,
|
||||
}
|
||||
|
||||
local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions)
|
||||
outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions)
|
||||
outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions)
|
||||
|
||||
validateScreenOptions(outputConfig, route)
|
||||
return outputConfig
|
||||
end
|
||||
end
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(router, routeName)
|
||||
validate(type(router) == "table", "router must be a table")
|
||||
validate(type(routeName) == "string", "routeName must be a string")
|
||||
|
||||
if router.childRouters and router.childRouters[routeName] then
|
||||
return router.childRouters[routeName]
|
||||
end
|
||||
|
||||
validate(type(router.getComponentForRouteName) == "function",
|
||||
"router.getComponentForRouteName must be a function if no child routers are specified")
|
||||
local component = router.getComponentForRouteName(routeName)
|
||||
if type(component) == "table" then
|
||||
return component.router
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(route)
|
||||
local result = {}
|
||||
|
||||
-- Go back FROM screen identified by 'key', or the default for current route.
|
||||
function result.goBack(key)
|
||||
if key == nil and route.key then
|
||||
validate(type(route.key) == "string", ".goBack(): key should be a string")
|
||||
key = route.key
|
||||
end
|
||||
|
||||
return NavigationActions.back({ key = key })
|
||||
end
|
||||
|
||||
-- Navigate to a different screen, either by route name+params+action, or
|
||||
-- by passing a raw navigation table.
|
||||
function result.navigate(navigateTo, params, action)
|
||||
if type(navigateTo) == "string" then
|
||||
return NavigationActions.navigate({
|
||||
routeName = navigateTo,
|
||||
params = params,
|
||||
action = action,
|
||||
})
|
||||
else
|
||||
validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table")
|
||||
validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value")
|
||||
validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value")
|
||||
|
||||
return NavigationActions.navigate(navigateTo)
|
||||
end
|
||||
end
|
||||
|
||||
-- Change navigation params for current route
|
||||
function result.setParams(params)
|
||||
validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator")
|
||||
return NavigationActions.setParams({ params = params, key = route.key })
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent)
|
||||
|
||||
-- Extract a single screen Roact component/navigator from
|
||||
-- a navigator's config.
|
||||
return function(routeConfigs, routeName)
|
||||
validate(type(routeConfigs) == "table", "routeConfigs must be a table")
|
||||
validate(type(routeName) == "string", "routeName must be a string")
|
||||
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
validate(routeConfig ~= nil, "There is no route defined for key '%s'.", routeName)
|
||||
|
||||
local routeConfigType = type(routeConfig)
|
||||
|
||||
if routeConfigType == "table" then
|
||||
if routeConfig.screen ~= nil then
|
||||
validate(isValidScreenComponent(routeConfig.screen),
|
||||
"screen param for key '%s' must be a valid Roact component.", routeName)
|
||||
return routeConfig.screen
|
||||
elseif type(routeConfig.getScreen) == "function" then
|
||||
local screen = routeConfig.getScreen()
|
||||
validate(isValidScreenComponent(screen),
|
||||
"The getScreen function defined for route '%s' did not return a valid screen or navigator", routeName)
|
||||
return screen
|
||||
end
|
||||
end
|
||||
|
||||
validate(isValidScreenComponent(routeConfig),
|
||||
"Value for key '%s' must be a route config table or a valid Roact component.", routeName)
|
||||
|
||||
return routeConfig
|
||||
end
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent)
|
||||
|
||||
--[[
|
||||
This utility checks to make sure that configs passed to a
|
||||
router are in the correct format.
|
||||
|
||||
Example:
|
||||
routeConfigs = {
|
||||
routeNameEx1 = Roact.Function/Stateful_Component,
|
||||
routeNameEx2 = {
|
||||
screen = Roact.Function/Stateful_Component,
|
||||
},
|
||||
routeNameEx3 = {
|
||||
getScreen = function()
|
||||
return Roact.Function/Stateful_Component
|
||||
end
|
||||
}
|
||||
routeNameEx4 = AnotherRoactNavigator -- this is a Stateful Component
|
||||
}
|
||||
]]
|
||||
return function(routeConfigs)
|
||||
validate(type(routeConfigs) == "table", "routeConfigs must be a table")
|
||||
|
||||
local atLeastOne = false
|
||||
for routeName, routeConfig in pairs(routeConfigs) do
|
||||
local configIsTable = type(routeConfig) == "table" or false
|
||||
local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen
|
||||
local screenComponent = configIsTable and routeConfig.screen or routeConfig
|
||||
validate(isValidScreenComponent(screenComponent) or
|
||||
(type(screenConfig.getScreen) == "function" and isValidScreenComponent(screenConfig.getScreen())),
|
||||
"The component for route '%s' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.",
|
||||
routeName)
|
||||
|
||||
validate(screenConfig.screen == nil or screenConfig.getScreen == nil,
|
||||
"Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName)
|
||||
atLeastOne = true
|
||||
end
|
||||
|
||||
validate(atLeastOne, "Please specify at least one route when configuring a navigator.")
|
||||
|
||||
return routeConfigs
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(screenOptions, route)
|
||||
validate(type(screenOptions) == "table", "screenOptions must be a table")
|
||||
validate(type(route) == "table", "route must be a table")
|
||||
validate(type(route.routeName) == "string", "route.routeName must be a string")
|
||||
validate(type(screenOptions.title) ~= "function",
|
||||
"title cannot be defined as a function in navigation options for screen '%s'",
|
||||
route.routeName)
|
||||
end
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000))
|
||||
local uuidCount = 0
|
||||
|
||||
local KeyGenerator = {}
|
||||
|
||||
-- NOTE: FOR TESTING ONLY.
|
||||
-- Normalize keys so that tests can be consistent.
|
||||
function KeyGenerator.normalizeKeys()
|
||||
uniqueBaseId = "id-test-"
|
||||
uuidCount = 0
|
||||
end
|
||||
|
||||
-- Get a string key that is unique for this session.
|
||||
function KeyGenerator.generateKey()
|
||||
uuidCount = uuidCount + 1
|
||||
return uniqueBaseId .. tostring(uuidCount)
|
||||
end
|
||||
|
||||
return KeyGenerator
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local validate = require(script.Parent.validate)
|
||||
|
||||
|
||||
local PageNavigationEvent = {}
|
||||
PageNavigationEvent.__index = PageNavigationEvent
|
||||
|
||||
function PageNavigationEvent.new(pageName, event)
|
||||
validate(typeof(pageName) == "string", "pageName should be string")
|
||||
validate(typeof(event) == "userdata", "event should be RoactNavigation.Event")
|
||||
local self = {
|
||||
event = event,
|
||||
pageName = pageName,
|
||||
}
|
||||
|
||||
setmetatable(self, PageNavigationEvent)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PageNavigationEvent.isPageNavigationEvent(instance)
|
||||
return getmetatable(instance).__index == PageNavigationEvent
|
||||
end
|
||||
|
||||
|
||||
PageNavigationEvent.__tostring = function (pageNavigationEvent)
|
||||
return string.format( "%-15s - %s", tostring(pageNavigationEvent.event), pageNavigationEvent.pageName)
|
||||
end
|
||||
|
||||
|
||||
function PageNavigationEvent:equalTo(anotherPageNavigationEvent)
|
||||
validate(PageNavigationEvent.isPageNavigationEvent(anotherPageNavigationEvent), "should be PageNavigationEvent")
|
||||
return self.pageName == anotherPageNavigationEvent.pageName and self.event == anotherPageNavigationEvent.event
|
||||
end
|
||||
|
||||
|
||||
return PageNavigationEvent
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
--[[
|
||||
Provides functions for comparing and printing lua tables.
|
||||
]]
|
||||
|
||||
local TableUtilities = {}
|
||||
local defaultIgnore = {}
|
||||
|
||||
local function makeKeyString(key)
|
||||
if type(key) == "string" then
|
||||
return string.format("%s", key)
|
||||
else
|
||||
return string.format("[%s]", tostring(key))
|
||||
end
|
||||
end
|
||||
|
||||
local function makeValueString(value)
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return string.format("%q", value)
|
||||
elseif valueType == "function" or valueType == "table" then
|
||||
return string.format("<%s>", tostring(value))
|
||||
else
|
||||
return string.format("%s", tostring(value))
|
||||
end
|
||||
end
|
||||
|
||||
local function printKeypair(key, value, indentStr, comment)
|
||||
local keyString = makeKeyString(key)
|
||||
local valueString = makeValueString(value)
|
||||
|
||||
local commentStr = comment and string.format(" -- %s", comment) or ""
|
||||
print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr))
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns if they have the same key-value pairs
|
||||
Except ignored keys
|
||||
]]
|
||||
function TableUtilities.ShallowEqual(A, B, ignore)
|
||||
if not A or not B then
|
||||
return false
|
||||
elseif A == B then
|
||||
return true
|
||||
end
|
||||
|
||||
if not ignore then
|
||||
ignore = defaultIgnore
|
||||
end
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(B) do
|
||||
if A[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A, B and a key, returns if two tables have the same value at key
|
||||
]]
|
||||
function TableUtilities.EqualKey(A, B, key)
|
||||
if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns a new table with elements of A
|
||||
which are either not keys in B or have a different value in B
|
||||
]]
|
||||
function TableUtilities.TableDifference(A, B)
|
||||
local new = {}
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= A[key] then
|
||||
new[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a list and returns a table whose
|
||||
keys are elements of the list and whose
|
||||
values are all true
|
||||
]]
|
||||
local function membershipTable(list)
|
||||
local result = {}
|
||||
for i = 1, #list do
|
||||
result[list[i]] = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a table and returns a list of keys in that table
|
||||
]]
|
||||
local function listOfKeys(t)
|
||||
local result = {}
|
||||
for key,_ in pairs(t) do
|
||||
table.insert(result, key)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes two lists A and B, returns a new list of elements of A
|
||||
which are not in B
|
||||
]]
|
||||
function TableUtilities.ListDifference(A, B)
|
||||
return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B)))
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging. Returns false if the given table has any of the following:
|
||||
- a key that is neither a number or a string
|
||||
- a mix of number and string keys
|
||||
- number keys which are not exactly 1..#t
|
||||
]]
|
||||
function TableUtilities.CheckListConsistency(t)
|
||||
local containsNumberKey = false
|
||||
local containsStringKey = false
|
||||
local numberConsistency = true
|
||||
|
||||
local index = 1
|
||||
for x, _ in pairs(t) do
|
||||
if type(x) == 'string' then
|
||||
containsStringKey = true
|
||||
elseif type(x) == 'number' then
|
||||
if index ~= x then
|
||||
numberConsistency = false
|
||||
end
|
||||
containsNumberKey = true
|
||||
else
|
||||
return false
|
||||
end
|
||||
|
||||
if containsStringKey and containsNumberKey then
|
||||
return false
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
if containsNumberKey then
|
||||
return numberConsistency
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging, serializes the given table to a reasonable string that might even interpret as lua.
|
||||
]]
|
||||
function TableUtilities.RecursiveToString(t, indent)
|
||||
indent = indent or ''
|
||||
|
||||
if type(t) == 'table' then
|
||||
local result = ""
|
||||
if not TableUtilities.CheckListConsistency(t) then
|
||||
result = result .. "-- WARNING: this table fails the list consistency test\n"
|
||||
end
|
||||
result = result .. "{\n"
|
||||
for k,v in pairs(t) do
|
||||
if type(k) == 'string' then
|
||||
result = result
|
||||
.. " "
|
||||
.. indent
|
||||
.. tostring(k)
|
||||
.. " = "
|
||||
.. TableUtilities.RecursiveToString(v, " "..indent)
|
||||
..";\n"
|
||||
end
|
||||
if type(k) == 'number' then
|
||||
result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n"
|
||||
end
|
||||
end
|
||||
result = result .. indent .. "}"
|
||||
return result
|
||||
else
|
||||
return tostring(t)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
For debugging. Prints the table on multiple lines to overcome log-line length
|
||||
limitations which are otherwise necessary for performance. Use sparingly.
|
||||
]]
|
||||
function TableUtilities.Print(t, indent)
|
||||
indent = indent or ' '
|
||||
|
||||
if type(t) ~= "table" then
|
||||
error("TableUtilities.Print must be passed a table", 2)
|
||||
end
|
||||
|
||||
-- For cycle detection
|
||||
local printedTables = {}
|
||||
|
||||
local function recurse(subTable, tableKey, level)
|
||||
-- Prevent cycles by keeping track of what tables we have printed
|
||||
printedTables[subTable] = true
|
||||
|
||||
local indentStr = string.rep(indent, level)
|
||||
local valueIndentStr = string.rep(indent, level + 1)
|
||||
|
||||
if tableKey then
|
||||
print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable)))
|
||||
else
|
||||
print(string.format("%s%s {", indentStr, makeValueString(subTable)))
|
||||
end
|
||||
|
||||
for key, value in pairs(subTable) do
|
||||
if type(value) == "table" then
|
||||
if printedTables[value] then
|
||||
printKeypair(key, value, valueIndentStr, "Possible cycle")
|
||||
else
|
||||
recurse(value, key, level + 1)
|
||||
end
|
||||
else
|
||||
printKeypair(key, value, valueIndentStr)
|
||||
end
|
||||
end
|
||||
|
||||
print(string.format("%s}%s", indentStr, (level > 0 and "," or "")))
|
||||
end
|
||||
|
||||
recurse(t, nil, 0)
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes a table and returns the field count
|
||||
]]
|
||||
function TableUtilities.FieldCount(t)
|
||||
local fieldCount = 0
|
||||
for _ in pairs(t) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
return fieldCount
|
||||
end
|
||||
|
||||
return TableUtilities
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local RoactNavigation = require(script.Parent.Parent)
|
||||
local NavigationEventsAdapter = require(script.Parent.Parent.views.NavigationEventsAdapter)
|
||||
local validate = require(script.Parent.validate)
|
||||
local PageNavigationEvent = require(script.Parent.PageNavigationEvent)
|
||||
|
||||
local TrackNavigationEvents = {}
|
||||
TrackNavigationEvents.__index = TrackNavigationEvents
|
||||
|
||||
function TrackNavigationEvents.new()
|
||||
local self = {
|
||||
navigationEvents = {},
|
||||
}
|
||||
|
||||
setmetatable(self, TrackNavigationEvents)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:getNavigationEvents()
|
||||
return self.navigationEvents
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:printNavigationEvents()
|
||||
print("Total Events: ", #self.navigationEvents)
|
||||
for _, navigationEvent in ipairs(self.navigationEvents) do
|
||||
print(navigationEvent)
|
||||
end
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:waitForNumberEventsMaxWaitTime(numberOfEvents, maxWaitTimeInSeconds)
|
||||
local secondsWaitedFor = 0
|
||||
local waitDurationPerIteration = 0.33
|
||||
while #self.navigationEvents < numberOfEvents
|
||||
and secondsWaitedFor <= maxWaitTimeInSeconds
|
||||
do
|
||||
wait(waitDurationPerIteration)
|
||||
-- print("waiting for number of events to reach:", numberOfEvents, "waited for:", secondsWaitedFor)
|
||||
secondsWaitedFor = secondsWaitedFor + waitDurationPerIteration
|
||||
end
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:resetNavigationEvents()
|
||||
self.navigationEvents = {}
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:createNavigationAdapter(pageName, components)
|
||||
local events = {}
|
||||
for _, event in pairs(RoactNavigation.Events) do
|
||||
events[event] = function()
|
||||
PageNavigationEvent.new(pageName, event)
|
||||
table.insert(self.navigationEvents, PageNavigationEvent.new(pageName, event))
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement(NavigationEventsAdapter, events, components)
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:equalTo(pageNavigationEventList)
|
||||
validate(typeof(pageNavigationEventList) == "table", "should be a list")
|
||||
local numberOfEvents = #self.navigationEvents
|
||||
local equal = numberOfEvents == #pageNavigationEventList
|
||||
local eventIndex = 1
|
||||
while eventIndex <= numberOfEvents and equal do
|
||||
equal = self.navigationEvents[eventIndex]:equalTo(pageNavigationEventList[eventIndex])
|
||||
eventIndex = eventIndex + 1
|
||||
end
|
||||
|
||||
return equal
|
||||
end
|
||||
|
||||
return TrackNavigationEvents
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
return function()
|
||||
local KeyGenerator = require(script.Parent.Parent.KeyGenerator)
|
||||
|
||||
it("should generate a new string key when called", function()
|
||||
KeyGenerator.normalizeKeys()
|
||||
|
||||
expect(KeyGenerator.generateKey()).to.equal("id-test-1")
|
||||
expect(KeyGenerator.generateKey()).to.equal("id-test-2")
|
||||
end)
|
||||
|
||||
it("should generate unique string keys without being normalized", function()
|
||||
expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey())
|
||||
end)
|
||||
end
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
local RoactNavigation = require(script.Parent.Parent.Parent)
|
||||
local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent)
|
||||
|
||||
return function()
|
||||
local testPage = "TEST PAGE"
|
||||
local willFocusEvent = RoactNavigation.Events.WillFocus
|
||||
|
||||
it("should validate constructor inputs", function()
|
||||
expect(function () PageNavigationEvent.new(testPage, willFocusEvent) end).never.to.throw()
|
||||
|
||||
expect(function () PageNavigationEvent.new(testPage, 1) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, "event") end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, nil) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, {some = "junk"}) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(1, willFocusEvent) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(nil, willFocusEvent) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new({"bogus"}, willFocusEvent) end).to.throw()
|
||||
end)
|
||||
|
||||
it("should be constructed from page name and RoactNavigation.Events", function()
|
||||
for _, event in pairs(RoactNavigation.Events) do
|
||||
local pageName = testPage .. tostring(event)
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(pageName, event)
|
||||
expect(testPageNavigationEvent.pageName).to.be.equal(pageName)
|
||||
expect(testPageNavigationEvent.event).to.be.equal(event)
|
||||
expect(PageNavigationEvent.isPageNavigationEvent(testPageNavigationEvent)).to.be.equal(true)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should implement tostring and eq", function()
|
||||
for _, event in pairs(RoactNavigation.Events) do
|
||||
local pageName = testPage .. tostring(event)
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(pageName, event)
|
||||
expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(pageName, event))).to.be.equal(true)
|
||||
expect(tostring(testPageNavigationEvent)).to.be.equal(string.format("%-15s - %s",tostring(event), pageName))
|
||||
end
|
||||
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(testPage, willFocusEvent)
|
||||
expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage, willFocusEvent))).to.be.equal(true)
|
||||
expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage .. "bogus", willFocusEvent))).to.be.equal(false)
|
||||
expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur))).to.be.equal(false)
|
||||
end)
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local RoactNavigation = require(script.Parent.Parent.Parent)
|
||||
local TrackNavigationEvents = require(script.Parent.Parent.TrackNavigationEvents)
|
||||
local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent)
|
||||
|
||||
|
||||
return function()
|
||||
local testPage = "TEST PAGE"
|
||||
local testPageWillFocus = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillFocus)
|
||||
local testPageWillBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur)
|
||||
|
||||
local trackNavigationEvents = TrackNavigationEvents.new()
|
||||
it("should implement equalTo function", function()
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(true)
|
||||
|
||||
local navigationEvents = trackNavigationEvents:getNavigationEvents()
|
||||
table.insert(navigationEvents, testPageWillFocus)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus})).to.be.equal(true)
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(false)
|
||||
|
||||
table.insert(navigationEvents, testPageWillBlur)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(true)
|
||||
|
||||
table.insert(navigationEvents, testPageWillFocus)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(false)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur, testPageWillFocus})).to.be.equal(true)
|
||||
end)
|
||||
|
||||
it("should be empty after reset", function()
|
||||
trackNavigationEvents:resetNavigationEvents()
|
||||
local navigationEvents = trackNavigationEvents:getNavigationEvents()
|
||||
expect(typeof(navigationEvents)).to.be.equal('table')
|
||||
expect(#navigationEvents).to.be.equal(0)
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(true)
|
||||
end)
|
||||
|
||||
end
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
return function()
|
||||
local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions)
|
||||
|
||||
it("should return a function", function()
|
||||
expect(type(getActiveChildNavigationOptions)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should ask router for current screen options and return them", function()
|
||||
local testInputScreenOpts = {}
|
||||
local testScreenOpts = {}
|
||||
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "123" }
|
||||
},
|
||||
index = 1,
|
||||
|
||||
},
|
||||
router = {}, -- stub
|
||||
}
|
||||
|
||||
function navigation.getChildNavigation(key)
|
||||
if key == "123" then
|
||||
return navigation
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local testOutputScreenOpts = nil
|
||||
function navigation.router.getScreenOptions(activeNav, screenProps)
|
||||
testOutputScreenOpts = screenProps
|
||||
|
||||
if activeNav == navigation then
|
||||
return testScreenOpts
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts))
|
||||
.to.equal(testScreenOpts)
|
||||
expect(testOutputScreenOpts).to.equal(testInputScreenOpts)
|
||||
end)
|
||||
end
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local isValidScreenComponent = require(script.Parent.Parent.isValidScreenComponent)
|
||||
local TestComponent = Roact.Component:extend("TestFoo")
|
||||
it("should return true for valid element types", function()
|
||||
-- Function Component is valid
|
||||
expect(isValidScreenComponent(function() end)).to.equal(true)
|
||||
-- Stateful Component is valid
|
||||
expect(isValidScreenComponent(TestComponent)).to.equal(true)
|
||||
expect(isValidScreenComponent(
|
||||
{ render = function() return TestComponent end })).to.equal(true)
|
||||
expect(isValidScreenComponent( -- we do not test if render function returns valid component
|
||||
{ render = function() end })).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return false for invalid element types", function()
|
||||
expect(isValidScreenComponent("foo")).to.equal(false)
|
||||
expect(isValidScreenComponent(Roact.createElement("Frame"))).to.equal(false)
|
||||
expect(isValidScreenComponent(5)).to.equal(false)
|
||||
expect(isValidScreenComponent(Roact.Portal)).to.equal(false)
|
||||
expect(isValidScreenComponent({ render = "bad" })).to.equal(false)
|
||||
expect(isValidScreenComponent(
|
||||
{ notRender = function() return "foo" end })).to.equal(false)
|
||||
end)
|
||||
end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
return function()
|
||||
local lerp = require(script.Parent.Parent.lerp)
|
||||
|
||||
it("should return bottom of range for bottom input", function()
|
||||
expect(lerp(0, 1, 0)).to.equal(0)
|
||||
expect(lerp(1, 0, 0)).to.equal(1)
|
||||
expect(lerp(-1, 0, 0)).to.equal(-1)
|
||||
end)
|
||||
|
||||
it("should return middle of range for middle input", function()
|
||||
expect(lerp(0, 1, 0.5)).to.equal(0.5)
|
||||
expect(lerp(1, 0, 0.5)).to.equal(0.5)
|
||||
expect(lerp(-1, 0, 0.5)).to.equal(-0.5)
|
||||
end)
|
||||
|
||||
it("should return top of range for top input", function()
|
||||
expect(lerp(0, 1, 1)).to.equal(1)
|
||||
expect(lerp(1, 0, 1)).to.equal(0)
|
||||
expect(lerp(-1, 0, 1)).to.equal(0)
|
||||
end)
|
||||
end
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
local StateTable = require(script.Parent.Parent.Parent.StateTable)
|
||||
local Events = require(script.Parent.Parent.NavigationEvents)
|
||||
local validate = require(script.Parent.validate)
|
||||
|
||||
local WILL_FOCUS_KEY = tostring(Events.WillFocus)
|
||||
local DID_FOCUS_KEY = tostring(Events.DidFocus)
|
||||
local WILL_BLUR_KEY = tostring(Events.WillBlur)
|
||||
local DID_BLUR_KEY = tostring(Events.DidBlur)
|
||||
local ACTION_KEY = tostring(Events.Action)
|
||||
local REFOCUS_KEY = tostring(Events.Refocus)
|
||||
|
||||
--[[
|
||||
Provides a StateTable for sending events to user screens on screen state changes.
|
||||
|
||||
Args:
|
||||
name - Name for state table debugging.
|
||||
emit(type, payload) - Function to emit a single raw event.
|
||||
disconnectAll() - Function to disconnect all listeners.
|
||||
|
||||
States:
|
||||
Blurred - Screen is not visible, but was at least animated on/off before.
|
||||
Focusing - Screen is moving to being visible.
|
||||
Focused - Screen is visible.
|
||||
Blurring - Screen is moving to being hidden.
|
||||
Disconnected - Screen is no longer part of the hierarchy and will not propagate events to user.
|
||||
|
||||
Accepted Event Types (always sent using Normal Events Pattern):
|
||||
NavigationEvents.WillFocus
|
||||
NavigationEvents.DidFocus
|
||||
NavigationEvents.WillBlur
|
||||
NavigationEvents.DidBlur
|
||||
NavigationEvents.Action
|
||||
NavigationEvents.Refocus - Note that Refocus events do NOT support the pattern. Send them without adornment!
|
||||
|
||||
Normal Events Pattern:
|
||||
[<Event> .. "A"] - Received event while screen is active child and not currently transitioning.
|
||||
[<Event> .. "T"] - Received event while screen is not active child, but is transitioning.
|
||||
[<Event> .. "AT"] - Received event while screen is active child and currently transitioning.
|
||||
[<Event>] - Received event while screen is not active child, and not currently transitioning.
|
||||
|
||||
Special Events:
|
||||
shutdown - Page has been removed from hierarchy, so shut down further event handling.
|
||||
]]
|
||||
return function(name, initialState, emit, disconnectAll)
|
||||
validate(type(name) == "string", "name must be a string")
|
||||
validate(type(initialState) == "string", "initialState must be a string")
|
||||
validate(type(emit) == "function", "emitAction must be a function")
|
||||
validate(type(disconnectAll) == "function", "disconnectAllAction must be a function")
|
||||
|
||||
local function doEmit(...)
|
||||
local eventList = {...}
|
||||
return function(_, _, payload)
|
||||
for _, event in ipairs(eventList) do
|
||||
emit(event, payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return StateTable.new("SubscriberEventsTable(" .. name .. ")", initialState, nil, {
|
||||
Blurred = {
|
||||
-- If child is active and not transitioning then we jump immediately to Focused state
|
||||
-- while sending the complete event sequence.
|
||||
[WILL_FOCUS_KEY .. "A"] = {
|
||||
nextState = "Focused",
|
||||
action = doEmit(Events.WillFocus, Events.DidFocus),
|
||||
},
|
||||
[ACTION_KEY .. "A"] = {
|
||||
nextState = "Focused",
|
||||
action = doEmit(Events.WillFocus, Events.DidFocus),
|
||||
},
|
||||
-- If child is active and we're in transition, we are visibly moving to it and
|
||||
-- a completion event will fire, so move to Focusing state.
|
||||
[WILL_FOCUS_KEY .. "AT"] = {
|
||||
nextState = "Focusing",
|
||||
action = doEmit(Events.WillFocus),
|
||||
},
|
||||
[ACTION_KEY .. "AT"] = {
|
||||
nextState = "Focusing",
|
||||
action = doEmit(Events.WillFocus),
|
||||
},
|
||||
|
||||
-- Shutdown event can occur after any other event, but only has teeth in blurred state.
|
||||
-- TODO: Do we need to support this from any state and broadcast all of the events that
|
||||
-- would lead it to Blurred from the screen's current state?
|
||||
shutdown = {
|
||||
nextState = "Disconnected",
|
||||
action = disconnectAll,
|
||||
},
|
||||
|
||||
-- All Refocus events are simply passed through.
|
||||
[REFOCUS_KEY] = {
|
||||
action = doEmit(Events.Refocus),
|
||||
},
|
||||
},
|
||||
|
||||
Focusing = {
|
||||
-- Note that DidFocus is only propagated if child is active and we're NOT transitioning, since
|
||||
-- a child that is still transitioning is not finished yet.
|
||||
[DID_FOCUS_KEY .. "A"] = {
|
||||
nextState = "Focused",
|
||||
action = doEmit(Events.DidFocus),
|
||||
},
|
||||
[ACTION_KEY .. "A"] = {
|
||||
nextState = "Focused",
|
||||
action = doEmit(Events.DidFocus),
|
||||
},
|
||||
-- It's possible to blur while still transitioning on screen due to multiple user taps. Note that
|
||||
-- in this case, this child would not be active, and navigation state obviously must still be
|
||||
-- transitioning to the new screen.
|
||||
[WILL_BLUR_KEY .. "T"] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
|
||||
-- All Refocus events are simply passed through.
|
||||
[REFOCUS_KEY] = {
|
||||
action = doEmit(Events.Refocus),
|
||||
},
|
||||
},
|
||||
Focused = {
|
||||
-- Any WillBlur event gets propagated, regardless of active or transitioning states.
|
||||
[WILL_BLUR_KEY] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
[WILL_BLUR_KEY .. "T"] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
[WILL_BLUR_KEY .. "A"] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
[WILL_BLUR_KEY .. "AT"] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
-- Action events drive blurring. When the screen is Focused but loses its active mark in
|
||||
-- navigation state, it becomes blurred.
|
||||
[ACTION_KEY] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
[ACTION_KEY .. "T"] = {
|
||||
nextState = "Blurring",
|
||||
action = doEmit(Events.WillBlur),
|
||||
},
|
||||
-- Any Action event while screen is focused needs to be passed along to the children.
|
||||
[ACTION_KEY .. "A"] = {
|
||||
action = doEmit(Events.Action),
|
||||
},
|
||||
[ACTION_KEY .. "AT"] = {
|
||||
action = doEmit(Events.Action),
|
||||
},
|
||||
|
||||
-- All Refocus events are simply passed through.
|
||||
[REFOCUS_KEY] = {
|
||||
action = doEmit(Events.Refocus),
|
||||
},
|
||||
},
|
||||
Blurring = {
|
||||
-- We're done blurring on any action event once we are not focused and no longer transitioning.
|
||||
[ACTION_KEY] = {
|
||||
nextState = "Blurred",
|
||||
action = doEmit(Events.DidBlur),
|
||||
},
|
||||
-- If parent is fully blurred, then so are we, and so are our children.
|
||||
[DID_BLUR_KEY] = {
|
||||
nextState = "Blurred",
|
||||
action = doEmit(Events.DidBlur),
|
||||
},
|
||||
[DID_BLUR_KEY .. "A"] = {
|
||||
nextState = "Blurred",
|
||||
action = doEmit(Events.DidBlur),
|
||||
},
|
||||
[DID_BLUR_KEY .. "T"] = {
|
||||
nextState = "Blurred",
|
||||
action = doEmit(Events.DidBlur),
|
||||
},
|
||||
[DID_BLUR_KEY .. "AT"] = {
|
||||
nextState = "Blurred",
|
||||
action = doEmit(Events.DidBlur),
|
||||
},
|
||||
-- Action that results in screen being active and there is no transition... We go straight back to Focused state!
|
||||
[ACTION_KEY .. "A"] = {
|
||||
nextState = "Focused",
|
||||
action = doEmit(Events.DidFocus)
|
||||
},
|
||||
-- If we become active and nav state is transitioning then user took an action to refocus this screen.
|
||||
[ACTION_KEY .. "AT"] = {
|
||||
nextState = "Focusing",
|
||||
action = doEmit(Events.WillFocus),
|
||||
},
|
||||
|
||||
-- All Refocus events are simply passed through.
|
||||
[REFOCUS_KEY] = {
|
||||
action = doEmit(Events.Refocus),
|
||||
},
|
||||
},
|
||||
Disconnected = {},
|
||||
})
|
||||
end
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
return function(navigation, screenProps)
|
||||
local state = navigation.state
|
||||
local router = navigation.router
|
||||
local getChildNavigation = navigation.getChildNavigation
|
||||
|
||||
local activeRoute = state.routes[state.index]
|
||||
local activeNavigation = getChildNavigation(activeRoute.key)
|
||||
|
||||
return router.getScreenOptions(activeNavigation, screenProps)
|
||||
end
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
|
||||
return function(props)
|
||||
local scene = props.scene
|
||||
local scenes = props.scenes
|
||||
|
||||
local index = scene.index
|
||||
local lastSceneIndexInScenes = #scenes
|
||||
local isBack = not scenes[lastSceneIndexInScenes].isActive
|
||||
|
||||
if isBack then
|
||||
local currentSceneIndexInScenes = Cryo.List.find(scenes, scene)
|
||||
|
||||
local targetSceneIndexInScenes = nil
|
||||
for i, iScene in ipairs(scenes) do
|
||||
if iScene.isActive then
|
||||
targetSceneIndexInScenes = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local targetSceneIndex = scenes[targetSceneIndexInScenes].index
|
||||
local lastSceneIndex = scenes[lastSceneIndexInScenes].index
|
||||
|
||||
if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then
|
||||
return {
|
||||
first = math.min(targetSceneIndex, index - 1),
|
||||
last = index + 1,
|
||||
}
|
||||
elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then
|
||||
return {
|
||||
first = index - 1,
|
||||
last = math.max(lastSceneIndex, index + 1)
|
||||
}
|
||||
elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
first = index - 1,
|
||||
last = index + 1
|
||||
}
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- We have to do this using type because ElementKind is not exported by Roact
|
||||
return function (screenComponent)
|
||||
local componentType = type(screenComponent)
|
||||
local valid = componentType == "function" or -- Function Component
|
||||
(componentType == "table" and type(screenComponent.render) == "function") -- Stateful Component
|
||||
return valid
|
||||
end
|
||||
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Helper interpolates t with range [0,1] into the range [a,b].
|
||||
local function lerp(a, b, t)
|
||||
return a * (1 - t) + b * t
|
||||
end
|
||||
|
||||
return lerp
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
--[[
|
||||
Validate() provides a mechanism to validate input arguments and
|
||||
internal state for your components. You can call it like this:
|
||||
|
||||
function myFunc(arg1, arg2)
|
||||
validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!",
|
||||
tostring(arg1), tostring(arg2))
|
||||
return doSomething(arg1, arg2)
|
||||
end
|
||||
|
||||
The error will be surfaced at the *call site* of your function.
|
||||
]]
|
||||
return function(result, ...)
|
||||
if not result then
|
||||
error(string.format(...), 3)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local APP_NAVIGATION_CONTEXT = NavigationSymbol("APP_NAVIGATION_CONTEXT")
|
||||
|
||||
-- Provider
|
||||
local NavigationProvider = Roact.Component:extend("NavigationProvider")
|
||||
|
||||
function NavigationProvider:init()
|
||||
local navigation = self.props.navigation
|
||||
validate(navigation ~= nil, "AppNavigationContext.Provider requires a 'navigation' prop.")
|
||||
self._context[APP_NAVIGATION_CONTEXT] = { navigation = navigation }
|
||||
end
|
||||
|
||||
function NavigationProvider:render()
|
||||
return Roact.oneChild(self.props[Roact.Children])
|
||||
end
|
||||
|
||||
-- Consumer
|
||||
local NavigationConsumer = Roact.Component:extend("NavigationConsumer")
|
||||
|
||||
function NavigationConsumer:render()
|
||||
local renderProp = self.props.render
|
||||
local context = self._context[APP_NAVIGATION_CONTEXT] or {}
|
||||
local navigation = self.props.navigation or context.navigation
|
||||
|
||||
validate(renderProp ~= nil, "AppNavigationContext.Consumer requires 'render' prop.")
|
||||
validate(navigation ~= nil, "AppNavigationContext.Consumer requires a navigation prop or context entry.")
|
||||
|
||||
return renderProp(navigation)
|
||||
end
|
||||
|
||||
-- Static connector
|
||||
local function connect(innerComponent)
|
||||
local componentName = string.format("NavigationConnection(%s)", tostring(innerComponent))
|
||||
local Connection = Roact.Component:extend(componentName)
|
||||
|
||||
function Connection:render()
|
||||
local props = self.props
|
||||
|
||||
return Roact.createElement(NavigationConsumer, {
|
||||
navigation = props.navigation, -- can be passed directly to wrapper
|
||||
render = function(navigation)
|
||||
return Roact.createElement(innerComponent, Cryo.Dictionary.join({
|
||||
navigation = navigation
|
||||
}, props)) -- join other props last so someone can manually pass in 'navigation'
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return Connection
|
||||
end
|
||||
|
||||
return {
|
||||
Provider = NavigationProvider,
|
||||
Consumer = NavigationConsumer,
|
||||
connect = connect,
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local AppNavigationContext = require(script.Parent.AppNavigationContext)
|
||||
local NavigationEvents = require(script.Parent.Parent.NavigationEvents)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
--[[
|
||||
NavigationEventsAdapter providers a wrapper component that allows you to subscribe
|
||||
to the navigation lifecycle events without having to explicitly manage your own
|
||||
listener subscriptions.
|
||||
|
||||
Usage:
|
||||
|
||||
function MyComponent:init()
|
||||
self.willFocus = function()
|
||||
-- Do tasks that need to happen right before the component will appear on screen.
|
||||
end
|
||||
|
||||
self.didFocus = function()
|
||||
-- Do tasks that need to happen right after the component appears on screen.
|
||||
end
|
||||
end
|
||||
|
||||
function MyComponent:render()
|
||||
-- Note that you must capture the self reference lexically, if you need it.
|
||||
return Roact.createElement(RoactNavigation.EventsAdapter, {
|
||||
[RoactNavigation.Events.WillFocus] = self.willFocus,
|
||||
[RoactNavigation.Events.DidFocus] = self.didFocus,
|
||||
[RoactNavigation.Events.WillBlur] = self.willBlur,
|
||||
[RoactNavigation.Events.DidBlur] = self.didBlur,
|
||||
}, <remainder of component tree> )
|
||||
end
|
||||
|
||||
Remember that focus and blur events may be called more than once in the lifetime of a
|
||||
component. If you navigate away from a component and then come back later, it will receive
|
||||
willBlur/didBlur and then willFocus/didFocus events.
|
||||
|
||||
Also remember that your event handlers must capture any self reference lexically, if necessary.
|
||||
]]
|
||||
local NavigationEventsAdapter = Roact.Component:extend("NavigationEventsAdapter")
|
||||
|
||||
function NavigationEventsAdapter:init()
|
||||
self.subscriptions = {}
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:_subscribeAll()
|
||||
local navigation = self.props.navigation
|
||||
assert(navigation ~= nil, "NavigationEventsAdapter can only be used within the view hierarchy of a navigator.")
|
||||
|
||||
for _, symbol in pairs(NavigationEvents) do
|
||||
self.subscriptions[symbol] = navigation.addListener(symbol, function(...)
|
||||
-- Retrieve callback from props each time, in case props change.
|
||||
local callback = self.props[symbol] or nil
|
||||
if callback then
|
||||
validate(type(callback) == "function", "Value for event '%s' must be a function callback", tostring(symbol))
|
||||
callback(...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:_disconnectAll()
|
||||
for _, symbol in pairs(NavigationEvents) do
|
||||
local sub = self.subscriptions[symbol]
|
||||
if sub then
|
||||
sub.disconnect()
|
||||
self.subscriptions[symbol] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:didMount()
|
||||
self:_subscribeAll()
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:willUnmount()
|
||||
self:_disconnectAll()
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:didUpdate(prevProps)
|
||||
if self.props.navigation ~= prevProps.navigation then
|
||||
-- This component might get reused for different state, so we need to hook back up to events
|
||||
self:_disconnectAll()
|
||||
self:_subscribeAll()
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventsAdapter:render()
|
||||
return Roact.createElement("Folder", nil, self.props[Roact.Children])
|
||||
end
|
||||
|
||||
return AppNavigationContext.connect(NavigationEventsAdapter)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local AppNavigationContext = require(script.Parent.AppNavigationContext)
|
||||
|
||||
local SceneView = Roact.PureComponent:extend("SceneView")
|
||||
|
||||
function SceneView:render()
|
||||
local screenProps = self.props.screenProps
|
||||
local component = self.props.component
|
||||
local navigation = self.props.navigation
|
||||
|
||||
return Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = navigation,
|
||||
}, {
|
||||
Scene = Roact.createElement(component, {
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
return SceneView
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local TableUtilities = require(script.Parent.Parent.utils.TableUtilities)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local SCENE_KEY_PREFIX = "scene_"
|
||||
|
||||
-- Compare two scenes based upon index and view key.
|
||||
local function compareScenes(a, b)
|
||||
if a.index == b.index then
|
||||
-- compare the route keys
|
||||
local delta = #a.key - #b.key
|
||||
if delta == 0 then
|
||||
return a.key < b.key
|
||||
else
|
||||
return delta < 0
|
||||
end
|
||||
else
|
||||
-- rank by index first
|
||||
return a.index < b.index
|
||||
end
|
||||
end
|
||||
|
||||
local function routesAreShallowEqual(a, b)
|
||||
if not a or not b then
|
||||
return a == b
|
||||
end
|
||||
|
||||
if a.key ~= b.key then
|
||||
return false
|
||||
end
|
||||
|
||||
return TableUtilities.ShallowEqual(a, b)
|
||||
end
|
||||
|
||||
local function scenesAreShallowEqual(a, b)
|
||||
return
|
||||
a.key == b.key and
|
||||
a.index == b.index and
|
||||
a.isStale == b.isStale and
|
||||
a.isActive == b.isActive and
|
||||
routesAreShallowEqual(a, b)
|
||||
end
|
||||
|
||||
return function(scenes, nextState, prevState, descriptors)
|
||||
-- Always update descriptors. See react-navigation's bug, here:
|
||||
-- https://github.com/react-navigation/react-navigation/issues/4271
|
||||
-- TODO: Do we need this? Can we do a real fix?
|
||||
for _, scene in ipairs(scenes) do
|
||||
local route = scene.route
|
||||
if descriptors and descriptors[route.key] then
|
||||
scene.descriptor = descriptors[route.key]
|
||||
end
|
||||
end
|
||||
|
||||
-- Bail out early if state is not updated
|
||||
if prevState == nextState then
|
||||
return scenes
|
||||
end
|
||||
|
||||
local prevScenes = {}
|
||||
local freshScenes = {}
|
||||
local staleScenes = {}
|
||||
|
||||
-- previously stale scenes should be marked stale
|
||||
for _, scene in ipairs(scenes) do
|
||||
local key = scene.key
|
||||
if scene.isStale then
|
||||
staleScenes[key] = scene
|
||||
end
|
||||
|
||||
prevScenes[key] = scene
|
||||
end
|
||||
|
||||
local nextKeys = {} -- fake set!
|
||||
local nextRoutes = nextState.routes
|
||||
local nextRoutesLength = #nextRoutes
|
||||
|
||||
-- Clip nextRoutes to stop at index because index is top of stack!
|
||||
if nextRoutesLength > nextState.index then
|
||||
print("Warning: StackRouter provided invalid state. Index should always be the top route")
|
||||
nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength)
|
||||
end
|
||||
|
||||
for index, route in ipairs(nextRoutes) do
|
||||
local key = SCENE_KEY_PREFIX .. route.key
|
||||
local descriptor = descriptors and descriptors[route.key] or nil
|
||||
|
||||
local scene = {
|
||||
index = index,
|
||||
isActive = false,
|
||||
isStale = false,
|
||||
key = key,
|
||||
route = route,
|
||||
descriptor = descriptor,
|
||||
}
|
||||
|
||||
validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key)
|
||||
nextKeys[key] = true
|
||||
|
||||
if staleScenes[key] then
|
||||
-- Previously stale scene was added to nextState, so we remove it from
|
||||
-- the map of stale scenes.
|
||||
staleScenes[key] = nil
|
||||
end
|
||||
|
||||
freshScenes[key] = scene
|
||||
end
|
||||
|
||||
if prevState then
|
||||
local prevRoutes = prevState.routes
|
||||
local prevRoutesLength = #prevRoutes
|
||||
if prevRoutesLength > prevState.index then
|
||||
print("StackRouter provided invalid state. Index should always be the top route.")
|
||||
prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength)
|
||||
end
|
||||
|
||||
-- Search previous routes and mark any removed scenes as stale
|
||||
for index, route in ipairs(prevRoutes) do
|
||||
local key = SCENE_KEY_PREFIX .. route.key
|
||||
-- Skip any refreshed scenes
|
||||
if not freshScenes[key] then
|
||||
local lastScene = nil
|
||||
for _, scene in ipairs(scenes) do
|
||||
if scene.route.key == route.key then
|
||||
lastScene = scene
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local descriptor = descriptors[route.key]
|
||||
if lastScene then
|
||||
descriptor = lastScene.descriptor
|
||||
end
|
||||
|
||||
if descriptor then
|
||||
staleScenes[key] = {
|
||||
index = index,
|
||||
isActive = false,
|
||||
isStale = true,
|
||||
key = key,
|
||||
route = route,
|
||||
descriptor = descriptor,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local nextScenes = {}
|
||||
|
||||
local function mergeScene(nextScene)
|
||||
local key = nextScene.key
|
||||
local prevScene = prevScenes[key] or nil
|
||||
if prevScene and scenesAreShallowEqual(prevScene, nextScene) then
|
||||
-- reuse prevScene to avoid re-render
|
||||
table.insert(nextScenes, prevScene)
|
||||
else
|
||||
table.insert(nextScenes, nextScene)
|
||||
end
|
||||
end
|
||||
|
||||
for _, scene in pairs(staleScenes) do
|
||||
mergeScene(scene)
|
||||
end
|
||||
|
||||
for _, scene in pairs(freshScenes) do
|
||||
mergeScene(scene)
|
||||
end
|
||||
|
||||
table.sort(nextScenes, compareScenes)
|
||||
|
||||
local activeScenesCount = 0
|
||||
for index, scene in ipairs(nextScenes) do
|
||||
local isActive = not scene.isStale and scene.index == nextState.index
|
||||
if isActive ~= scene.isActive then
|
||||
nextScenes[index] = Cryo.Dictionary.join(scene, {
|
||||
isActive = isActive,
|
||||
})
|
||||
end
|
||||
|
||||
if isActive then
|
||||
activeScenesCount = activeScenesCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount)
|
||||
|
||||
-- Conditionally return nextScenes based upon shallow comparison, for performance
|
||||
if #nextScenes ~= #scenes then
|
||||
return nextScenes
|
||||
end
|
||||
|
||||
for index, scene in ipairs(nextScenes) do
|
||||
if not scenesAreShallowEqual(scenes[index], scene) then
|
||||
return nextScenes
|
||||
end
|
||||
end
|
||||
|
||||
-- Scenes have not changed
|
||||
return scenes
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
|
||||
local ScreenGuiWrapper = Roact.PureComponent:extend("ScreenGuiWrapper")
|
||||
|
||||
ScreenGuiWrapper.defaultProps = {
|
||||
DisplayOrder = 0,
|
||||
OnTopOfCoreBlur = false,
|
||||
visible = true,
|
||||
}
|
||||
|
||||
function ScreenGuiWrapper:render()
|
||||
local props = self.props
|
||||
local component = props.component
|
||||
local visible = props.visible
|
||||
local displayOrder = props.DisplayOrder
|
||||
local onTopOfCoreBlur = props.OnTopOfCoreBlur
|
||||
|
||||
local filteredProps = Cryo.Dictionary.join(props, {
|
||||
component = Cryo.None,
|
||||
DisplayOrder = Cryo.None,
|
||||
OnTopOfCoreBlur = Cryo.None,
|
||||
-- visible prop is passed down for convenience of inner component.
|
||||
})
|
||||
|
||||
return Roact.createElement("ScreenGui", {
|
||||
Enabled = visible,
|
||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||
DisplayOrder = displayOrder,
|
||||
OnTopOfCoreBlur = onTopOfCoreBlur,
|
||||
}, {
|
||||
InnerComponent = Roact.createElement(component, filteredProps)
|
||||
})
|
||||
end
|
||||
|
||||
return ScreenGuiWrapper
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol)
|
||||
|
||||
local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT")
|
||||
local MODAL_SYMBOL = NavigationSymbol("MODAL")
|
||||
local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY")
|
||||
|
||||
--[[
|
||||
StackPresentationStyle is used with stack navigators/views to determine
|
||||
the behavior of a given screen when it is pushed/popped from the stack, as
|
||||
well as the visual effects/transitions applied while the view is on screen.
|
||||
]]
|
||||
return {
|
||||
--[[
|
||||
The Default presentation style draws stack cards so that they will
|
||||
slide in and out from the right side of the screen one at a time. No
|
||||
special visual effects are applied, and cards always fill the entire space
|
||||
available. Your screen content is rendered over an opaque background color
|
||||
by default, but you have the option to draw the cards with a semi- or fully
|
||||
transparent background via navigationOptions. Cards always prevent
|
||||
tap-through of the content underneath them (in case your navigation
|
||||
container is transparent).
|
||||
]]
|
||||
Default = DEFAULT_SYMBOL,
|
||||
|
||||
--[[
|
||||
The Modal presentation style causes screens to animate up/down from the
|
||||
bottom of the navigation container and visually stack on top of each
|
||||
other. Cards are opaque by default, but you may set navigationOptions
|
||||
to make them semi- or fully transparent so that you can see the underlying
|
||||
cards. Modal cards always prevent tap-through of any underlying UI, including
|
||||
other cards in the same stack.
|
||||
]]
|
||||
Modal = MODAL_SYMBOL,
|
||||
|
||||
--[[
|
||||
The Overlay presentation style causes screens to pop in (later they will fade in)
|
||||
on top of the underlying screens. Like modals, they visually stack on top of each
|
||||
other. Cards are opaque by default, but you may set navigationOptions to make
|
||||
them semi- or fully transparent. Overlay cards always prevent tap-through of any
|
||||
underlying UI, including other cards in the same stack.
|
||||
]]
|
||||
Overlay = OVERLAY_SYMBOL
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
local StackViewLayout = require(script.Parent.StackViewLayout)
|
||||
local Transitioner = require(script.Parent.Parent.Transitioner)
|
||||
local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
|
||||
local defaultNavigationConfig = {
|
||||
mode = StackPresentationStyle.Default,
|
||||
}
|
||||
|
||||
|
||||
local StackView = Roact.Component:extend("StackView")
|
||||
|
||||
function StackView:init()
|
||||
self._doRender = function(...)
|
||||
return self:_render(...)
|
||||
end
|
||||
|
||||
self._doConfigureTransition = function(...)
|
||||
return self:_configureTransition(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionStart = function(...)
|
||||
return self:_onTransitionStart(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionEnd = function(...)
|
||||
return self:_onTransitionEnd(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionStep = function(...)
|
||||
return self:_onTransitionStep(...)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:render()
|
||||
local screenProps = self.props.screenProps
|
||||
local navigation = self.props.navigation
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
-- Transitioner handles setting up the animation motors and making that data
|
||||
-- available to the lower layer.
|
||||
return Roact.createElement(Transitioner, {
|
||||
render = self._doRender,
|
||||
configureTransition = self._doConfigureTransition,
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
descriptors = descriptors,
|
||||
onTransitionStart = self._doOnTransitionStart,
|
||||
onTransitionEnd = self._doOnTransitionEnd,
|
||||
onTransitionStep = self._doOnTransitionStep,
|
||||
})
|
||||
end
|
||||
|
||||
function StackView:didMount()
|
||||
local navigation = self.props.navigation
|
||||
if navigation.state.isTransitioning then
|
||||
navigation.dispatch(NavigationActions.completeTransition({
|
||||
key = navigation.state.key,
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_render(transition, lastTransition)
|
||||
local screenProps = self.props.screenProps
|
||||
local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig)
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, {
|
||||
screenProps = screenProps,
|
||||
descriptors = descriptors,
|
||||
transitionProps = transition,
|
||||
lastTransitionProps = lastTransition,
|
||||
}))
|
||||
end
|
||||
|
||||
function StackView:_configureTransition(transition, lastTransition)
|
||||
return StackViewTransitionConfigs.getTransitionConfig(
|
||||
self.props.navigationConfig.transitionConfig,
|
||||
transition,
|
||||
lastTransition,
|
||||
self.props.navigationConfig.mode
|
||||
).transitionSpec
|
||||
end
|
||||
|
||||
function StackView:_onTransitionStart(transition, lastTransition)
|
||||
local onTransitionStart = self.props.onTransitionStart
|
||||
or self.props.navigationConfig.onTransitionStart
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionStart and transition.index ~= lastTransition.index then
|
||||
onTransitionStart(transition.navigation, lastTransition.navigation)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_onTransitionEnd(transition, lastTransition)
|
||||
local navigationConfig = self.props.navigationConfig
|
||||
local navigation = self.props.navigation
|
||||
local onTransitionEnd = self.props.onTransitionEnd or navigationConfig.onTransitionEnd
|
||||
local transitionDestKey = transition.scene.route.key
|
||||
local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey
|
||||
|
||||
if transition.navigation.state.isTransitioning and isCurrentKey then
|
||||
navigation.dispatch(NavigationActions.completeTransition({
|
||||
key = navigation.state.key,
|
||||
toChildKey = transitionDestKey,
|
||||
}))
|
||||
end
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionEnd and transition.index ~= lastTransition.index then
|
||||
onTransitionEnd(transition.navigation, lastTransition.navigation)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_onTransitionStep(transition, lastTransition, value)
|
||||
local onTransitionStep = self.props.onTransitionStep
|
||||
or self.props.navigationConfig.onTransitionStep
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionStep and transition.index ~= lastTransition.index then
|
||||
onTransitionStep(transition.navigation, lastTransition.navigation, value)
|
||||
end
|
||||
end
|
||||
|
||||
return StackView
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local validate = require(script.Parent.Parent.Parent.utils.validate)
|
||||
|
||||
--[[
|
||||
Render a scene as a card for use in a StackView. This component is
|
||||
responsible for correctly positioning the scene content in relation
|
||||
to the other scenes. The content will be rendered inside a Frame
|
||||
whose position and size are controlled by the transition logic. The
|
||||
frame may either be transparent or a solid color, depending upon props.
|
||||
Any additional visual effects must be supplied by the container or the
|
||||
child element created by renderScene().
|
||||
|
||||
Props:
|
||||
renderScene(scene) -- Render prop to draw the scene inside the card.
|
||||
initialPosition -- Starting position for the card. (Animated by Otter from there).
|
||||
positionStep -- Stepper function from StackViewInterpolator.
|
||||
position -- Otter motor for the position of the card.
|
||||
scene -- Scene that the card is to render.
|
||||
forceHidden -- Forcibly disable card rendering (e.g. animated off-screen).
|
||||
transparent -- Card allows underlying content to show through (default: false).
|
||||
cardColor3 -- Color of the card background if it's not transparent (default: white).
|
||||
]]
|
||||
local StackViewCard = Roact.Component:extend("StackViewCard")
|
||||
|
||||
StackViewCard.defaultProps = {
|
||||
transparent = false,
|
||||
cardColor3 = Color3.new(1, 1, 1),
|
||||
}
|
||||
|
||||
function StackViewCard:init()
|
||||
local currentNavIndex = self.props.navigation.state.index
|
||||
|
||||
self._isMounted = false
|
||||
self._positionLastValue = currentNavIndex
|
||||
|
||||
local selfRef = Roact.createRef()
|
||||
self._getRef = function()
|
||||
return self.props[Roact.Ref] or selfRef
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:render()
|
||||
local forceHidden = self.props.forceHidden
|
||||
local cardColor3 = self.props.cardColor3
|
||||
local transparent = self.props.transparent
|
||||
local initialPosition = self.props.initialPosition
|
||||
local renderScene = self.props.renderScene
|
||||
local scene = self.props.scene
|
||||
|
||||
validate(type(renderScene) == "function", "renderScene must be a function")
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Position = initialPosition,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = cardColor3,
|
||||
BackgroundTransparency = transparent and 1 or nil,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
Visible = not forceHidden,
|
||||
[Roact.Ref] = self:_getRef(),
|
||||
}, {
|
||||
Content = renderScene(scene),
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewCard:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
local position = self.props.position
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewCard:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._positionDisconnector then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:didUpdate(oldProps)
|
||||
local position = self.props.position
|
||||
local positionStep = self.props.positionStep
|
||||
|
||||
if position ~= oldProps.position then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
if positionStep ~= oldProps.positionStep then
|
||||
-- The motor won't fire just because stepper function has changed. We have to
|
||||
-- update the position to match new requirements based upon last motor value.
|
||||
self:_onPositionStep(self._positionLastValue)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:_onPositionStep(value)
|
||||
if not self._isMounted then
|
||||
return
|
||||
end
|
||||
|
||||
local positionStep = self.props.positionStep
|
||||
|
||||
if positionStep then
|
||||
positionStep(self:_getRef(), value)
|
||||
end
|
||||
|
||||
self._positionLastValue = value
|
||||
end
|
||||
|
||||
return StackViewCard
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
--[[
|
||||
Provides builders to create functions that interpolate the current Otter motor
|
||||
position into the correct translation for stack cards based upon their associated
|
||||
scene.
|
||||
|
||||
Interpolator builders expect the following props as input:
|
||||
{
|
||||
initialPositionValue = <value of position motor that transition began from>,
|
||||
scene = <scene for the particular card being animated>,
|
||||
layout = {
|
||||
initWidth = <expected width of card>,
|
||||
initHeight = <expected height of card>,
|
||||
isMeasured = <boolean: true if initWidth+Height have been measured, else false>,
|
||||
}
|
||||
}
|
||||
|
||||
Each builder returns a props table to be merged onto your other StackViewCard props, ex:
|
||||
{
|
||||
positionStep = <stepper function, or nil if not needed>,
|
||||
initialPosition = <starting position UDim2 for card based upon current active scene index>,
|
||||
forceHidden = true, -- May disable card visibility if it's outside interpolating range.
|
||||
}
|
||||
|
||||
The props table may contain other changes, depending on the requirements of the animation.
|
||||
]]
|
||||
local getSceneIndicesForInterpolationInputRange = require(
|
||||
script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange)
|
||||
local lerp = require(script.Parent.Parent.Parent.utils.lerp)
|
||||
|
||||
-- Render initial style when layout hasn't been measured yet.
|
||||
local function forInitial(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local scene = props.scene
|
||||
|
||||
local forceHidden = initialPositionValue ~= scene.index
|
||||
local translate = forceHidden and 1000000 or 0
|
||||
|
||||
return {
|
||||
forceHidden = forceHidden,
|
||||
initialPosition = UDim2.new(0, translate, 0, translate),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- Slide-in from right style (e.g. navigation stack view).
|
||||
local function forHorizontal(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
-- getSceneIndices* returns nil if card is not visible and need not be
|
||||
-- considered for the animation until state changes.
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local first = interpolate.first
|
||||
local last = interpolate.last
|
||||
local index = scene.index
|
||||
|
||||
local width = layout.initWidth
|
||||
|
||||
local function calculate(positionValue)
|
||||
-- 3 range LERP
|
||||
if positionValue < first then
|
||||
return width
|
||||
elseif positionValue < index then
|
||||
return lerp(width, 0, (positionValue - first) / (index - first))
|
||||
elseif positionValue == index then
|
||||
return 0
|
||||
elseif positionValue < last then
|
||||
return lerp(0, -width, (positionValue - index) / (last - index))
|
||||
else
|
||||
return -width
|
||||
end
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
local oldPosition = cardInstance.Position
|
||||
cardInstance.Position = UDim2.new(
|
||||
oldPosition.X.Scale,
|
||||
calculate(positionValue),
|
||||
oldPosition.Y.Scale,
|
||||
oldPosition.Y.Offset
|
||||
)
|
||||
end
|
||||
|
||||
local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0)
|
||||
|
||||
return {
|
||||
initialPosition = initialPosition,
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
-- Slide-in from bottom style (e.g. modals).
|
||||
local function forVertical(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local first = interpolate.first
|
||||
local index = scene.index
|
||||
local height = layout.initHeight
|
||||
|
||||
local function calculate(positionValue)
|
||||
-- 2 range LERP
|
||||
if positionValue < first then
|
||||
return height
|
||||
elseif positionValue < index then
|
||||
return lerp(height, 0, (positionValue - first) / (index - first))
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
local oldPosition = cardInstance.Position
|
||||
cardInstance.Position = UDim2.new(
|
||||
oldPosition.X.Scale,
|
||||
oldPosition.X.Offset,
|
||||
oldPosition.Y.Scale,
|
||||
calculate(positionValue)
|
||||
)
|
||||
end
|
||||
|
||||
local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue))
|
||||
|
||||
return {
|
||||
initialPosition = initialPosition,
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have
|
||||
-- group transparency, this 'animation' just pops the views in for now.
|
||||
local function forFade(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local index = scene.index
|
||||
|
||||
local function calculate(positionValue)
|
||||
return positionValue >= index - 0.5
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
cardInstance.Visible = calculate(positionValue)
|
||||
end
|
||||
|
||||
return {
|
||||
forceHidden = not calculate(initialPositionValue),
|
||||
initialPosition = UDim2.new(0, 0, 0, 0),
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
forHorizontal = forHorizontal,
|
||||
forVertical = forVertical,
|
||||
forFade = forFade,
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs)
|
||||
local StackViewOverlayFrame = require(script.Parent.StackViewOverlayFrame)
|
||||
local StackViewCard = require(script.Parent.StackViewCard)
|
||||
local SceneView = require(script.Parent.Parent.SceneView)
|
||||
|
||||
local defaultScreenOptions = {
|
||||
absorbInput = true,
|
||||
overlayEnabled = false,
|
||||
overlayColor3 = Color3.new(0, 0, 0),
|
||||
overlayTransparency = 0.7,
|
||||
-- cardColor3 default is provided by StackViewCard
|
||||
renderOverlay = function(navigationOptions, initialTransitionValue, transitionChangedSignal)
|
||||
-- NOTE: renderOverlay will not be called if sceneOptions.overlayEnabled evaluates false
|
||||
return Roact.createElement(StackViewOverlayFrame, {
|
||||
navigationOptions = navigationOptions,
|
||||
initialTransitionValue = initialTransitionValue,
|
||||
transitionChangedSignal = transitionChangedSignal,
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
local function calculateTransitionValue(index, position)
|
||||
return math.max(math.min(1 + position - index, 1), 0)
|
||||
end
|
||||
|
||||
|
||||
local StackViewLayout = Roact.Component:extend("StackViewLayout")
|
||||
|
||||
function StackViewLayout:init()
|
||||
local startingIndex = self.props.transitionProps.navigation.state.index
|
||||
|
||||
self._isMounted = false
|
||||
self._positionLastValue = startingIndex
|
||||
|
||||
self._renderScene = function(scene)
|
||||
return self:_renderInnerScene(scene)
|
||||
end
|
||||
|
||||
self._subscribeToOverlayUpdates = function(callback)
|
||||
local position = self.props.transitionProps.position
|
||||
local index = self.props.transitionProps.scene.index
|
||||
|
||||
return position:onStep(function(value)
|
||||
callback(calculateTransitionValue(index, value))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:_renderCard(scene, navigationOptions)
|
||||
local transitionProps = self.props.transitionProps -- Core animation info from Transitioner.
|
||||
local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info.
|
||||
local transitionConfig = self.state.transitionConfig -- State based info from scene config.
|
||||
|
||||
local cardColor3 = navigationOptions.cardColor3
|
||||
local overlayEnabled = navigationOptions.overlayEnabled
|
||||
|
||||
local initialPositionValue = transitionProps.scene.index
|
||||
if lastTransitionProps then
|
||||
initialPositionValue = lastTransitionProps.scene.index
|
||||
end
|
||||
|
||||
local cardInterpolationProps = {}
|
||||
local screenInterpolator = transitionConfig.screenInterpolator
|
||||
if screenInterpolator then
|
||||
cardInterpolationProps = screenInterpolator(
|
||||
Cryo.Dictionary.join(transitionProps, {
|
||||
initialPositionValue = initialPositionValue,
|
||||
scene = scene,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
-- Merge down the various prop packages to be applied to StackViewCard.
|
||||
return Roact.createElement(StackViewCard, Cryo.Dictionary.join(
|
||||
transitionProps, cardInterpolationProps, {
|
||||
key = "card_" .. tostring(scene.key),
|
||||
scene = scene,
|
||||
renderScene = self._renderScene,
|
||||
transparent = overlayEnabled,
|
||||
cardColor3 = cardColor3,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
function StackViewLayout:_renderInnerScene(scene)
|
||||
local navigation = scene.descriptor.navigation
|
||||
|
||||
local sceneComponent = scene.descriptor.getComponent()
|
||||
local screenProps = self.props.screenProps
|
||||
|
||||
return Roact.createElement(SceneView, {
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
component = sceneComponent,
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewLayout:render()
|
||||
local transitionProps = self.props.transitionProps
|
||||
local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex
|
||||
local scenes = transitionProps.scenes
|
||||
|
||||
local renderedScenes = Cryo.List.map(scenes, function(scene)
|
||||
-- The card is obscured if:
|
||||
-- It's not the active card (e.g. we're transitioning TO it).
|
||||
-- It's hidden underneath an opaque card that is NOT currently transitioning.
|
||||
-- It's completely off-screen.
|
||||
local cardObscured = scene.index < topMostOpaqueSceneIndex and not scene.isActive
|
||||
|
||||
local screenOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {})
|
||||
local overlayEnabled = screenOptions.overlayEnabled
|
||||
local absorbInput = screenOptions.absorbInput
|
||||
local renderOverlay = screenOptions.renderOverlay
|
||||
|
||||
local stationaryContent = nil
|
||||
if overlayEnabled then
|
||||
stationaryContent = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 1,
|
||||
}, {
|
||||
Overlay = renderOverlay(
|
||||
screenOptions,
|
||||
calculateTransitionValue(scene.index, self._positionLastValue),
|
||||
self._subscribeToOverlayUpdates)
|
||||
})
|
||||
end
|
||||
|
||||
-- Wrapper frame holds default/custom card background and the card content.
|
||||
-- It MUST be a Frame when absorbInput=false because of legacy behavior on desktop
|
||||
-- for GuiObject.Active=false blocking mouse clicks from falling through.
|
||||
-- (Active=false DOES work on mobile, but not desktop).
|
||||
-- When absorbInput=true, we add a TextButton behind the frame that will catch
|
||||
-- mouse clicks
|
||||
local absorbInputElement = nil
|
||||
if not cardObscured and absorbInput then
|
||||
absorbInputElement = Roact.createElement("TextButton", {
|
||||
Active = true,
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Text = " ",
|
||||
ZIndex = 2 * scene.index - 1,
|
||||
})
|
||||
end
|
||||
|
||||
return Roact.createFragment({
|
||||
AbsorbInput = absorbInputElement,
|
||||
-- use scene index for key, it makes testing with Rhodium easier
|
||||
[tostring(scene.index)] = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
ZIndex = 2 * scene.index,
|
||||
Visible = not cardObscured,
|
||||
}, {
|
||||
StationaryContent = stationaryContent,
|
||||
DynamicContent = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 2,
|
||||
}, {
|
||||
-- Cards need to have unique keys so that instances of the same components are not
|
||||
-- reused for different scenes. (Could lead to unanticipated lifecycle problems).
|
||||
["card_" .. scene.key] = self:_renderCard(scene, screenOptions),
|
||||
})
|
||||
}),
|
||||
})
|
||||
end)
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
}, renderedScenes)
|
||||
end
|
||||
|
||||
function StackViewLayout.getDerivedStateFromProps(nextProps, lastState)
|
||||
local transitionProps = nextProps.transitionProps
|
||||
local scenes = transitionProps.scenes
|
||||
local state = transitionProps.navigation.state
|
||||
local isTransitioning = state.isTransitioning
|
||||
local topMostIndex = #scenes
|
||||
|
||||
local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or
|
||||
nextProps.mode == StackPresentationStyle.Overlay
|
||||
|
||||
-- Find the last opaque scene in a modal stack so that we can optimize rendering.
|
||||
local topMostOpaqueSceneIndex = 0
|
||||
if isOverlayMode then
|
||||
for idx = topMostIndex, 1, -1 do
|
||||
local scene = scenes[idx]
|
||||
local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {})
|
||||
|
||||
-- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning.
|
||||
if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then
|
||||
topMostOpaqueSceneIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
for idx = topMostIndex, 1, -1 do
|
||||
if not (isTransitioning and idx == topMostIndex) then
|
||||
topMostOpaqueSceneIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
topMostOpaqueSceneIndex = topMostOpaqueSceneIndex,
|
||||
transitionConfig = StackViewTransitionConfigs.getTransitionConfig(
|
||||
nextProps.transitionConfig,
|
||||
nextProps.transitionProps,
|
||||
nextProps.lastTransitionProps,
|
||||
nextProps.mode),
|
||||
}
|
||||
end
|
||||
|
||||
function StackViewLayout:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
self._positionDisconnector = self.props.transitionProps.position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewLayout:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._positionDisconnector then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:didUpdate(oldProps)
|
||||
local position = self.props.transitionProps.position
|
||||
|
||||
if position ~= oldProps.transitionProps.position then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:_onPositionStep(value)
|
||||
if self._isMounted then
|
||||
self._positionLastValue = value
|
||||
end
|
||||
end
|
||||
|
||||
return StackViewLayout
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local lerp = require(script.Parent.Parent.Parent.utils.lerp)
|
||||
|
||||
local StackViewOverlayFrame = Roact.Component:extend("StackViewOverlayFrame")
|
||||
|
||||
function StackViewOverlayFrame:init()
|
||||
self._signalDisconnect = nil
|
||||
|
||||
local selfRef = Roact.createRef()
|
||||
self._getRef = function()
|
||||
return self.props[Roact.Ref] or selfRef
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:render()
|
||||
local navigationOptions = self.props.navigationOptions
|
||||
local initialTransitionValue = self.props.initialTransitionValue
|
||||
|
||||
local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, initialTransitionValue)
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = navigationOptions.overlayColor3,
|
||||
BackgroundTransparency = overlayTransparency,
|
||||
BorderSizePixel = 0,
|
||||
[Roact.Ref] = self:_getRef(),
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:didUpdate(oldProps)
|
||||
local transitionChangedSignal = self.props.transitionChangedSignal
|
||||
|
||||
if transitionChangedSignal ~= oldProps.transitionChangedSignal then
|
||||
if self._signalDisconnect then
|
||||
self._signalDisconnect()
|
||||
end
|
||||
|
||||
self._signalDisconnect = transitionChangedSignal(function(...)
|
||||
self:_transitionChanged(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:didMount()
|
||||
self._isMounted = true
|
||||
self._signalDisconnect = self.props.transitionChangedSignal(function(...)
|
||||
self:_transitionChanged(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:willUnmount()
|
||||
self._isMounted = false
|
||||
if self._signalDisconnect then
|
||||
self._signalDisconnect()
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:_transitionChanged(value)
|
||||
if not self._isMounted then
|
||||
return
|
||||
end
|
||||
|
||||
local myRef = self:_getRef()
|
||||
if myRef.current then
|
||||
local navigationOptions = self.props.navigationOptions
|
||||
local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, value)
|
||||
myRef.current.BackgroundTransparency = overlayTransparency
|
||||
end
|
||||
end
|
||||
|
||||
return StackViewOverlayFrame
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo)
|
||||
local StackViewInterpolator = require(script.Parent.StackViewInterpolator)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
|
||||
local DefaultTransitionSpec = {
|
||||
frequency = 3, -- Hz
|
||||
dampingRatio = 1,
|
||||
}
|
||||
|
||||
local SlideFromRight = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forHorizontal,
|
||||
}
|
||||
|
||||
local ModalSlideFromBottom = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forVertical,
|
||||
}
|
||||
|
||||
local FadeInPlace = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forFade,
|
||||
}
|
||||
|
||||
local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle)
|
||||
if presentationStyle == StackPresentationStyle.Modal then
|
||||
return ModalSlideFromBottom
|
||||
elseif presentationStyle == StackPresentationStyle.Overlay then
|
||||
return FadeInPlace
|
||||
else
|
||||
return SlideFromRight
|
||||
end
|
||||
end
|
||||
|
||||
local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle)
|
||||
local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle)
|
||||
if transitionConfigurer then
|
||||
return Cryo.Dictionary.join(
|
||||
defaultConfig,
|
||||
transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle)
|
||||
)
|
||||
end
|
||||
|
||||
return defaultConfig
|
||||
end
|
||||
|
||||
return {
|
||||
getDefaultTransitionConfig = getDefaultTransitionConfig,
|
||||
getTransitionConfig = getTransitionConfig,
|
||||
SlideFromRight = SlideFromRight,
|
||||
ModalSlideFromBottom = ModalSlideFromBottom,
|
||||
FadeInPlace = FadeInPlace,
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
return function()
|
||||
local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle)
|
||||
|
||||
describe("StackPresentationStyle token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default)
|
||||
expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal)
|
||||
expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT")
|
||||
expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL")
|
||||
expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
return function()
|
||||
-- local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
-- local StackView = require(script.Parent.Parent.StackView)
|
||||
|
||||
itSKIP("should have its tests implemented", function()
|
||||
|
||||
end)
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
return function()
|
||||
local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact)
|
||||
local StackViewCard = require(script.Parent.Parent.StackViewCard)
|
||||
|
||||
it("should mount its renderProp and pass it scene", function()
|
||||
local didRender = false
|
||||
local testScene = {
|
||||
isActive = true,
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local renderedScene = nil
|
||||
local element = Roact.createElement(StackViewCard, {
|
||||
renderScene = function(theScene)
|
||||
renderedScene = theScene
|
||||
return Roact.createElement(function()
|
||||
didRender = true -- verifies component is attached to tree
|
||||
end)
|
||||
end,
|
||||
scene = testScene,
|
||||
position = Otter.createSingleMotor(1),
|
||||
navigation = {
|
||||
state = {
|
||||
index = 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(renderedScene).to.equal(testScene)
|
||||
expect(didRender).to.equal(true)
|
||||
end)
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
return function()
|
||||
local StackViewInterpolator = require(script.Parent.Parent.StackViewInterpolator)
|
||||
|
||||
itSKIP("should have its tests implemented", function()
|
||||
|
||||
end)
|
||||
end
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
return function()
|
||||
-- local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
-- local StackViewLayout = require(script.Parent.Parent.StackViewLayout)
|
||||
|
||||
itSKIP("should have its tests implemented", function()
|
||||
|
||||
end)
|
||||
end
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
return function()
|
||||
itSKIP("should have its tests implemented", function()
|
||||
|
||||
end)
|
||||
end
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local SceneView = require(script.Parent.SceneView)
|
||||
|
||||
local defaultNavigationConfig = {
|
||||
keepVisitedScreensMounted = false,
|
||||
}
|
||||
|
||||
local SwitchView = Roact.Component:extend("SwitchView")
|
||||
|
||||
function SwitchView.getDerivedStateFromProps(nextProps, prevState)
|
||||
local navState = nextProps.navigation.state
|
||||
local activeKey = navState.routes[navState.index].key
|
||||
local descriptors = nextProps.descriptors
|
||||
|
||||
local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, nextProps.navigationConfig or {})
|
||||
local keepVisitedScreensMounted = navigationConfig.keepVisitedScreensMounted
|
||||
|
||||
local visitedScreenKeys = {
|
||||
[activeKey] = true
|
||||
}
|
||||
|
||||
if keepVisitedScreensMounted then
|
||||
-- prune visited screen keys if they are not included in incoming descriptors
|
||||
for prevKey in pairs(prevState.visitedScreenKeys or {}) do
|
||||
if descriptors[prevKey] ~= nil then
|
||||
visitedScreenKeys[prevKey] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
visitedScreenKeys = visitedScreenKeys,
|
||||
}
|
||||
end
|
||||
|
||||
function SwitchView:render()
|
||||
local navState = self.props.navigation.state
|
||||
local screenProps = self.props.screenProps
|
||||
local descriptors = self.props.descriptors
|
||||
local visitedScreenKeys = self.state.visitedScreenKeys
|
||||
local activeKey = navState.routes[navState.index].key
|
||||
|
||||
local screenElements = {}
|
||||
for key, descriptor in pairs(descriptors) do
|
||||
local isActiveKey = (key == activeKey)
|
||||
|
||||
if visitedScreenKeys[key] == true then
|
||||
screenElements["card_" .. key] = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
Visible = isActiveKey,
|
||||
}, {
|
||||
Content = Roact.createElement(SceneView, {
|
||||
component = descriptor.getComponent(),
|
||||
navigation = descriptor.navigation,
|
||||
screenProps = screenProps,
|
||||
})
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement("Folder", nil, screenElements)
|
||||
end
|
||||
|
||||
return SwitchView
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local Otter = require(script.Parent.Parent.Parent.Otter)
|
||||
local ScenesReducer = require(script.Parent.ScenesReducer)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local DEFAULT_TRANSITION_SPEC = {
|
||||
frequency = 4 -- Hz
|
||||
}
|
||||
|
||||
local function buildTransitionProps(props, state)
|
||||
local navigation = props.navigation
|
||||
local options = props.options
|
||||
|
||||
local layout = state.layout
|
||||
local position = state.position
|
||||
local scenes = state.scenes
|
||||
|
||||
local activeScene
|
||||
for _, x in ipairs(scenes) do
|
||||
if x.isActive then
|
||||
activeScene = x
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
validate(activeScene, "Could not find active scene")
|
||||
|
||||
return {
|
||||
layout = layout,
|
||||
navigation = navigation,
|
||||
position = position,
|
||||
scenes = scenes,
|
||||
scene = activeScene,
|
||||
options = options,
|
||||
index = activeScene.index,
|
||||
}
|
||||
end
|
||||
|
||||
local function filterStale(scenes)
|
||||
local filtered = Cryo.List.filter(scenes, function(scene)
|
||||
return not scene.isStale
|
||||
end)
|
||||
|
||||
if #filtered == #scenes then
|
||||
return scenes
|
||||
else
|
||||
return filtered
|
||||
end
|
||||
end
|
||||
|
||||
local Transitioner = Roact.Component:extend("Transitioner")
|
||||
|
||||
function Transitioner:init()
|
||||
local navigationState = self.props.navigation.state
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
self.state = {
|
||||
-- Layout is passed to StackViewLayout in order to allow it to
|
||||
-- sync animations.
|
||||
layout = {
|
||||
initWidth = 0,
|
||||
initHeight = 0,
|
||||
isMeasured = false,
|
||||
},
|
||||
position = Otter.createSingleMotor(navigationState.index),
|
||||
scenes = ScenesReducer({}, navigationState, nil, descriptors),
|
||||
}
|
||||
|
||||
self._doOnAbsoluteSizeChanged = function(...)
|
||||
return self:_onAbsoluteSizeChanged(...)
|
||||
end
|
||||
|
||||
self._positionLastValue = navigationState.index
|
||||
|
||||
self._prevTransitionProps = nil
|
||||
self._transitionProps = buildTransitionProps(self.props, self.state)
|
||||
|
||||
self._isMounted = false
|
||||
self._isTransitionRunning = false
|
||||
self._transitionQueue = {}
|
||||
|
||||
self._completeSignalDisconnector = self.state.position:onComplete(function()
|
||||
-- This spawn is required because of this Otter bug: https://github.com/Roblox/otter/issues/26
|
||||
-- Otter.SingleMotor's step function calls onComplete before it calls stop(). This leaves their
|
||||
-- __running=true, and the setGoal() in our _onTransitionEnd() does nothing. So the whole queue
|
||||
-- handling just stops cold, and the transition never actually happens!
|
||||
spawn(function()
|
||||
if self._isMounted then
|
||||
self:_onTransitionEnd()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
self._stepSignalDisconnector = self.state.position:onStep(function(value)
|
||||
if self._isMounted then
|
||||
self:_onPositionStep(value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Transitioner:didMount()
|
||||
self._isMounted = true
|
||||
end
|
||||
|
||||
function Transitioner:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._completeSignalDisconnector then
|
||||
self._completeSignalDisconnector()
|
||||
self._completeSignalDisconnector = nil
|
||||
end
|
||||
|
||||
if self._stepSignalDisconnector then
|
||||
self._stepSignalDisconnector()
|
||||
self._stepSignalDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:didUpdate(prevProps)
|
||||
-- React-navigation uses componentWillReceiveProps that is only called when Parent
|
||||
-- re-renders or when this component is actually being given new props, so we need to
|
||||
-- filter here. If not, this would trigger on setState and enter an infinite loop.
|
||||
if self.props ~= prevProps then
|
||||
if self._isTransitionRunning then
|
||||
local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {}
|
||||
-- don't enqueue spurious extra copies of same transition props
|
||||
if mostRecentTransition.prevProps ~= prevProps then
|
||||
table.insert(self._transitionQueue, { prevProps = prevProps })
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self:_startTransition(prevProps, self.props)
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
[Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged,
|
||||
}, {
|
||||
["TransitionerScenes"] = self.props.render(
|
||||
self._transitionProps, self._prevTransitionProps),
|
||||
})
|
||||
end
|
||||
|
||||
-- equivalent to React-Nav's Transitioner._onLayout
|
||||
function Transitioner:_onAbsoluteSizeChanged(rbx)
|
||||
local width = rbx.AbsoluteSize.X
|
||||
local height = rbx.AbsoluteSize.Y
|
||||
|
||||
if width == self.state.layout.initWidth and
|
||||
height == self.state.layout.initHeight then
|
||||
return
|
||||
end
|
||||
|
||||
local layout = Cryo.Dictionary.join(self.state.layout, {
|
||||
initWidth = width,
|
||||
initHeight = height,
|
||||
isMeasured = true,
|
||||
})
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
layout = layout,
|
||||
})
|
||||
|
||||
self._transitionProps = buildTransitionProps(self.props, nextState)
|
||||
|
||||
self:setState({
|
||||
layout = layout,
|
||||
})
|
||||
end
|
||||
|
||||
function Transitioner:_computeScenes(props, nextProps)
|
||||
local nextScenes = ScenesReducer(
|
||||
self.state.scenes,
|
||||
nextProps.navigation.state,
|
||||
props.navigation.state,
|
||||
nextProps.descriptors)
|
||||
|
||||
if not nextProps.navigation.state.isTransitioning then
|
||||
nextScenes = filterStale(nextScenes)
|
||||
end
|
||||
|
||||
if nextScenes == self.state.scenes then
|
||||
return nil
|
||||
end
|
||||
|
||||
return nextScenes
|
||||
end
|
||||
|
||||
function Transitioner:_startTransition(props, nextProps)
|
||||
local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index
|
||||
local nextScenes = self:_computeScenes(props, nextProps)
|
||||
|
||||
if not nextScenes then
|
||||
-- If nextScenes is nil, nothing has changed, so report transition end, then bail
|
||||
self._prevTransitionProps = self._transitionProps
|
||||
|
||||
-- Trigger end transition logic if we daisy-chained from queued transitions via _onTransitionEnd.
|
||||
if self._isTransitionRunning then
|
||||
self:_onTransitionEnd()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
scenes = nextScenes,
|
||||
})
|
||||
|
||||
local position = nextState.position
|
||||
local toValue = nextProps.navigation.state.index
|
||||
|
||||
-- compute transitionProps
|
||||
self._prevTransitionProps = self._transitionProps
|
||||
self._transitionProps = buildTransitionProps(nextProps, nextState)
|
||||
local isTransitioning = self._transitionProps.navigation.state.isTransitioning
|
||||
|
||||
if not isTransitioning or not indexHasChanged then
|
||||
-- If state is not transitioning, then we go immediately to new index.
|
||||
-- Likewise, if the index has not changed then we still need to set up initial
|
||||
-- positions via setState.
|
||||
self:setState(nextState)
|
||||
|
||||
if nextProps.onTransitionStart then
|
||||
nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps)
|
||||
end
|
||||
|
||||
-- motor will call _endTransition for us
|
||||
position:setGoal(Otter.instant(toValue))
|
||||
elseif isTransitioning then
|
||||
self._isTransitionRunning = true
|
||||
self:setState(nextState)
|
||||
|
||||
if nextProps.onTransitionStart then
|
||||
nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps)
|
||||
end
|
||||
|
||||
local positionHasChanged = self._positionLastValue ~= toValue
|
||||
if indexHasChanged and positionHasChanged then
|
||||
-- get transition spec
|
||||
local transitionUserSpec = {}
|
||||
if nextProps.configureTransition then
|
||||
transitionUserSpec = nextProps.configureTransition(
|
||||
self._transitionProps, self._prevTransitionProps) or {}
|
||||
end
|
||||
|
||||
local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec)
|
||||
|
||||
-- motor will call _endTransition for us
|
||||
position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec))
|
||||
else
|
||||
-- Set motor to current state to trigger _endTransition call with correct sequencing.
|
||||
position:setGoal(Otter.instant(nextProps.navigation.state.index))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:_onTransitionEnd()
|
||||
local prevTransitionProps = self._prevTransitionProps
|
||||
self._prevTransitionProps = nil
|
||||
|
||||
local scenes = filterStale(self.state.scenes)
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
scenes = scenes,
|
||||
})
|
||||
|
||||
self._transitionProps = buildTransitionProps(self.props, nextState)
|
||||
|
||||
self:setState(nextState)
|
||||
|
||||
if self.props.onTransitionEnd then
|
||||
self.props.onTransitionEnd(self._transitionProps, prevTransitionProps)
|
||||
end
|
||||
|
||||
local firstQueuedTransition = self._transitionQueue[1]
|
||||
if firstQueuedTransition then
|
||||
local prevProps = firstQueuedTransition.prevProps
|
||||
self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1)
|
||||
self:_startTransition(prevProps, self.props)
|
||||
else
|
||||
self._isTransitionRunning = false
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:_onPositionStep(value)
|
||||
self._positionLastValue = value
|
||||
|
||||
local targetIndex = self._transitionProps.index
|
||||
|
||||
-- _prevTransitionProps can be nil, so guard against it.
|
||||
local startingIndex = targetIndex
|
||||
if self._prevTransitionProps then
|
||||
startingIndex = self._prevTransitionProps.index
|
||||
end
|
||||
|
||||
if self.props.onTransitionStep and startingIndex ~= targetIndex then
|
||||
local transitionValue = (value - startingIndex) / (targetIndex - startingIndex)
|
||||
self.props.onTransitionStep(self._transitionProps, self._prevTransitionProps, transitionValue)
|
||||
end
|
||||
end
|
||||
|
||||
return Transitioner
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext)
|
||||
|
||||
it("should propagate navigation prop from provider to consumer", function()
|
||||
local testNavigationContextProp = {}
|
||||
local testComponentNavContext = nil
|
||||
|
||||
local provider = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = testNavigationContextProp
|
||||
}, {
|
||||
Child = Roact.createElement(AppNavigationContext.Consumer, {
|
||||
render = function(navigation)
|
||||
testComponentNavContext = navigation
|
||||
end
|
||||
})
|
||||
})
|
||||
|
||||
local instance = Roact.mount(provider)
|
||||
expect(testComponentNavContext).to.equal(testNavigationContextProp)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should override context navigation prop if custom prop is set on consumer", function()
|
||||
local testCustomNavigationProp = {}
|
||||
local testComponentNavContext = nil
|
||||
|
||||
local provider = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = {}
|
||||
}, {
|
||||
Child = Roact.createElement(AppNavigationContext.Consumer, {
|
||||
navigation = testCustomNavigationProp,
|
||||
render = function(navigation)
|
||||
testComponentNavContext = navigation
|
||||
end
|
||||
})
|
||||
})
|
||||
|
||||
local instance = Roact.mount(provider)
|
||||
expect(testComponentNavContext).to.equal(testCustomNavigationProp)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should pass navigation prop to statically wrapped components", function()
|
||||
local testNavigationContextProp = {}
|
||||
local passedNavigationProp = nil
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
passedNavigationProp = self.props.navigation
|
||||
end
|
||||
|
||||
local WrappedComponent = AppNavigationContext.connect(TestComponent)
|
||||
|
||||
local element = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = testNavigationContextProp
|
||||
}, {
|
||||
Child = Roact.createElement(WrappedComponent)
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(passedNavigationProp).to.equal(testNavigationContextProp)
|
||||
end)
|
||||
|
||||
it("should override static wrapper navigation prop when navigation is directly set", function()
|
||||
local testNavigationProp = {}
|
||||
local passedNavigationProp = nil
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
passedNavigationProp = self.props.navigation
|
||||
end
|
||||
|
||||
local WrappedComponent = AppNavigationContext.connect(TestComponent)
|
||||
|
||||
local element = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = {}
|
||||
}, {
|
||||
Child = Roact.createElement(WrappedComponent, {
|
||||
navigation = testNavigationProp
|
||||
})
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(passedNavigationProp).to.equal(testNavigationProp)
|
||||
end)
|
||||
end
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents)
|
||||
local NavigationEventsAdapter = require(script.Parent.Parent.NavigationEventsAdapter)
|
||||
|
||||
it("should subscribe to events for each registered handler", function()
|
||||
local mockNavContext = {
|
||||
subscribedHandlers = {},
|
||||
calledHandlers = {},
|
||||
}
|
||||
|
||||
function mockNavContext:makeHandler(symbol)
|
||||
return function()
|
||||
self.calledHandlers[symbol] = true
|
||||
end
|
||||
end
|
||||
|
||||
function mockNavContext.addListener(symbol, callback)
|
||||
mockNavContext.subscribedHandlers[symbol] = callback
|
||||
return {
|
||||
disconnect = function()
|
||||
mockNavContext.subscribedHandlers[symbol] = nil
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local adapter = Roact.createElement(NavigationEventsAdapter, {
|
||||
navigation = mockNavContext,
|
||||
[NavigationEvents.WillFocus] = mockNavContext:makeHandler(NavigationEvents.WillFocus),
|
||||
[NavigationEvents.DidFocus] = mockNavContext:makeHandler(NavigationEvents.DidFocus),
|
||||
[NavigationEvents.WillBlur] = mockNavContext:makeHandler(NavigationEvents.WillBlur),
|
||||
[NavigationEvents.DidBlur] = mockNavContext:makeHandler(NavigationEvents.DidBlur),
|
||||
})
|
||||
|
||||
local instance = Roact.mount(adapter)
|
||||
|
||||
expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus])).to.equal("function")
|
||||
expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus])).to.equal("function")
|
||||
expect(type(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur])).to.equal("function")
|
||||
expect(type(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur])).to.equal("function")
|
||||
|
||||
mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]()
|
||||
expect(mockNavContext.calledHandlers[NavigationEvents.WillFocus]).to.equal(true)
|
||||
|
||||
mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]()
|
||||
expect(mockNavContext.calledHandlers[NavigationEvents.DidFocus]).to.equal(true)
|
||||
|
||||
mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]()
|
||||
expect(mockNavContext.calledHandlers[NavigationEvents.WillBlur]).to.equal(true)
|
||||
|
||||
mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]()
|
||||
expect(mockNavContext.calledHandlers[NavigationEvents.DidBlur]).to.equal(true)
|
||||
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(mockNavContext.subscribedHandlers[NavigationEvents.WillFocus]).to.equal(nil)
|
||||
expect(mockNavContext.subscribedHandlers[NavigationEvents.DidFocus]).to.equal(nil)
|
||||
expect(mockNavContext.subscribedHandlers[NavigationEvents.WillBlur]).to.equal(nil)
|
||||
expect(mockNavContext.subscribedHandlers[NavigationEvents.DidBlur]).to.equal(nil)
|
||||
end)
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local SceneView = require(script.Parent.Parent.SceneView)
|
||||
local withNavigation = require(script.Parent.Parent.withNavigation)
|
||||
|
||||
it("should mount inner component and pass down required props+context.navigation", function()
|
||||
local testComponentNavigationFromProp = nil
|
||||
local testComponentScreenProps = nil
|
||||
local testComponentNavigationFromContext = nil
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
testComponentNavigationFromProp = self.props.navigation
|
||||
testComponentScreenProps = self.props.screenProps
|
||||
|
||||
return withNavigation(function(navigation)
|
||||
testComponentNavigationFromContext = navigation
|
||||
end)
|
||||
end
|
||||
|
||||
local testScreenProps = {}
|
||||
local testNav = {}
|
||||
local element = Roact.createElement(SceneView, {
|
||||
screenProps = testScreenProps,
|
||||
navigation = testNav,
|
||||
component = TestComponent,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(testComponentScreenProps).to.equal(testScreenProps)
|
||||
expect(testComponentNavigationFromProp).to.equal(testNav)
|
||||
expect(testComponentNavigationFromContext).to.equal(testNav)
|
||||
end)
|
||||
end
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
return function()
|
||||
-- ScenesReducer(scenes, nextState, prevState, descriptors)
|
||||
local ScenesReducer = require(script.Parent.Parent.ScenesReducer)
|
||||
|
||||
local initialRouteKey = "id-1"
|
||||
local initialRouteName = "First Route"
|
||||
local initialRoute = {
|
||||
routeName = initialRouteName,
|
||||
key = initialRouteKey,
|
||||
}
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = false,
|
||||
routes = {
|
||||
initialRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local initialDescriptors = {
|
||||
[initialRouteKey] = {
|
||||
key = initialRouteKey,
|
||||
navigation = {
|
||||
state = initialRoute,
|
||||
},
|
||||
state = initialRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local initialScenes = nil
|
||||
|
||||
it("should generate valid initial scene", function()
|
||||
local scenes = ScenesReducer({}, initialState, nil, initialDescriptors)
|
||||
local sceneOne = scenes[1]
|
||||
expect(#scenes).to.be.equal(1)
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
initialScenes = scenes
|
||||
end)
|
||||
|
||||
it("should update descriptor", function()
|
||||
local scenes = ScenesReducer({}, initialState, nil, initialDescriptors)
|
||||
local dummyDescriptor = { key = "this is a dummy descriptor" }
|
||||
scenes[1].descriptor = dummyDescriptor
|
||||
local updatedScenes = ScenesReducer(scenes, initialState, initialState, initialDescriptors)
|
||||
expect(#updatedScenes).to.be.equal(1)
|
||||
local sceneOne = updatedScenes[1]
|
||||
expect(sceneOne.descriptor).to.never.be.equal(dummyDescriptor)
|
||||
expect(sceneOne.descriptor).to.be.equal(initialDescriptors[initialRouteKey])
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
end)
|
||||
|
||||
it("should bail out early", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(initialScenes, nil, nil, nil)
|
||||
expect(scenes).to.be.equal(initialScenes)
|
||||
scenes = ScenesReducer(initialScenes, initialState, initialState, initialDescriptors)
|
||||
expect(scenes).to.be.equal(initialScenes)
|
||||
end)
|
||||
|
||||
local secondRouteKey = "id-2"
|
||||
local secondRouteName = "Second Route"
|
||||
local secondRoute = {
|
||||
key = secondRouteKey,
|
||||
routeName = secondRouteName,
|
||||
}
|
||||
|
||||
local secondState = {
|
||||
index = 2,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local secondDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] ={
|
||||
key = secondRouteKey,
|
||||
navigation = {
|
||||
state = secondRoute,
|
||||
},
|
||||
state = secondRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local secondScenes = nil
|
||||
|
||||
it("should add second scene", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(initialScenes, secondState, initialState, secondDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(2)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(true)
|
||||
expect(sceneTwo.isStale).to.be.equal(false)
|
||||
|
||||
secondScenes = scenes
|
||||
end)
|
||||
|
||||
local thirdRouteKey = "id-3"
|
||||
local thirdRouteName = "Third Route"
|
||||
local thirdRoute = {
|
||||
key = thirdRouteKey,
|
||||
routeName = thirdRouteName,
|
||||
}
|
||||
|
||||
local thirdState = {
|
||||
index = 3,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondRoute,
|
||||
thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local thirdDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] = secondDescriptors[secondRouteKey],
|
||||
[thirdRouteKey] = {
|
||||
key = thirdRouteKey,
|
||||
navigation = {
|
||||
state = thirdRoute,
|
||||
},
|
||||
state = thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local thirdScenes = nil
|
||||
|
||||
it("should add third scene", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(secondScenes, thirdState, secondState, thirdDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(3)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[3]
|
||||
expect(sceneTwo.route).to.be.equal(thirdRoute)
|
||||
expect(sceneTwo.index).to.be.equal(3)
|
||||
expect(sceneTwo.isActive).to.be.equal(true)
|
||||
expect(sceneTwo.isStale).to.be.equal(false)
|
||||
|
||||
thirdScenes = scenes
|
||||
end)
|
||||
|
||||
it("should mark removed scenes as stale", function()
|
||||
expect(secondState).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(thirdScenes, initialState, thirdState, initialDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(3)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(true)
|
||||
|
||||
local sceneThree = scenes[3]
|
||||
expect(sceneThree.route).to.be.equal(thirdRoute)
|
||||
expect(sceneThree.index).to.be.equal(3)
|
||||
expect(sceneThree.isActive).to.be.equal(false)
|
||||
expect(sceneThree.isStale).to.be.equal(true)
|
||||
end)
|
||||
|
||||
local secondScreenReplacementKey = "id-22"
|
||||
local secondScreenReplacementName = "Second Route Replacement"
|
||||
local secondScreenReplacementRoute = {
|
||||
key = secondScreenReplacementKey,
|
||||
routeName = secondScreenReplacementName,
|
||||
}
|
||||
|
||||
local replacedSecondSceneState = {
|
||||
index = 3,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondScreenReplacementRoute,
|
||||
thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local replacedSceneDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] = {
|
||||
key = secondScreenReplacementKey,
|
||||
navigation = {
|
||||
state = secondScreenReplacementRoute
|
||||
},
|
||||
state = secondScreenReplacementRoute,
|
||||
},
|
||||
[thirdRouteKey] = thirdDescriptors[thirdRouteKey],
|
||||
}
|
||||
|
||||
it("should mark replaced scene as stale", function()
|
||||
expect(secondState).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(thirdScenes, replacedSecondSceneState, thirdState, replacedSceneDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(4) -- replaced scene is marked stale, it is not removed
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(true)
|
||||
|
||||
-- because of comparison algorithm in SceneReducer.lua compareScenes
|
||||
-- the replacement scene is after the scene it replaced because id-2 < id-22
|
||||
local sceneTwoReplacement = scenes[3]
|
||||
expect(sceneTwoReplacement.route).to.be.equal(secondScreenReplacementRoute)
|
||||
-- index is still 2 because the scene index come from route index in the nextState
|
||||
-- this is ok because filterStale in Transitioner.lua will remove the stale scene
|
||||
expect(sceneTwoReplacement.index).to.be.equal(2)
|
||||
expect(sceneTwoReplacement.isActive).to.be.equal(false)
|
||||
expect(sceneTwoReplacement.isStale).to.be.equal(false)
|
||||
|
||||
local sceneThree = scenes[4]
|
||||
expect(sceneThree.route).to.be.equal(thirdRoute)
|
||||
expect(sceneThree.index).to.be.equal(3)
|
||||
expect(sceneThree.isActive).to.be.equal(true)
|
||||
expect(sceneThree.isStale).to.be.equal(false)
|
||||
end)
|
||||
end
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local ScreenGuiWrapper = require(script.Parent.Parent.ScreenGuiWrapper)
|
||||
|
||||
it("should mount the inner component if visible", function()
|
||||
local innerComponentProps = nil
|
||||
|
||||
local innerComponent = function(props)
|
||||
innerComponentProps = props
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 50, 0, 50)
|
||||
})
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, {
|
||||
component = innerComponent,
|
||||
visible = true,
|
||||
myPassedInValue = 5,
|
||||
}))
|
||||
|
||||
expect(innerComponentProps).to.never.equal(nil)
|
||||
expect(innerComponentProps.visible).to.equal(true)
|
||||
expect(innerComponentProps.DisplayOrder).to.equal(nil)
|
||||
expect(innerComponentProps.component).to.equal(nil)
|
||||
expect(innerComponentProps.myPassedInValue).to.equal(5)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should still mount the inner component if ScreenGui is not visible", function()
|
||||
local innerComponentProps = nil
|
||||
|
||||
local innerComponent = function(props)
|
||||
innerComponentProps = props
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(0, 50, 0, 50)
|
||||
})
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(ScreenGuiWrapper, {
|
||||
component = innerComponent,
|
||||
visible = false,
|
||||
}))
|
||||
|
||||
expect(innerComponentProps).to.never.equal(nil)
|
||||
expect(innerComponentProps.visible).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local SwitchView = require(script.Parent.Parent.SwitchView)
|
||||
local withNavigation = require(script.Parent.Parent.withNavigation)
|
||||
|
||||
it("should mount and pass required props and context", function()
|
||||
local testScreenProps = {}
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" }
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
}
|
||||
|
||||
local testComponentNavigationFromProp = nil
|
||||
local testComponentScreenProps = nil
|
||||
local testComponentNavigationFromContext = nil
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
testComponentNavigationFromProp = self.props.navigation
|
||||
testComponentScreenProps = self.props.screenProps
|
||||
|
||||
return withNavigation(function(navigation)
|
||||
testComponentNavigationFromContext = navigation
|
||||
end)
|
||||
end
|
||||
|
||||
local testDescriptors = {
|
||||
Foo = {
|
||||
getComponent = function()
|
||||
return TestComponent
|
||||
end,
|
||||
navigation = testNavigation,
|
||||
}
|
||||
}
|
||||
|
||||
local element = Roact.createElement(SwitchView, {
|
||||
screenProps = testScreenProps,
|
||||
navigation = testNavigation,
|
||||
descriptors = testDescriptors,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(testComponentNavigationFromProp).to.equal(testNavigation)
|
||||
expect(testComponentScreenProps).to.equal(testScreenProps)
|
||||
expect(testComponentNavigationFromContext).to.equal(testNavigation)
|
||||
end)
|
||||
|
||||
it("should unmount inactive pages when keepVisitedScreensMounted is false", function()
|
||||
local fooUnmounted = false
|
||||
local TestComponentFoo = Roact.Component:extend("TestComponentFoo")
|
||||
function TestComponentFoo:render() end
|
||||
function TestComponentFoo:willUnmount()
|
||||
fooUnmounted = true
|
||||
end
|
||||
|
||||
local TestComponentBar = Roact.Component:extend("TestComponentBar")
|
||||
function TestComponentBar:render() end
|
||||
|
||||
local function makeDescriptors(navProp)
|
||||
return {
|
||||
Foo = {
|
||||
getComponent = function() return TestComponentFoo end,
|
||||
navigation = navProp,
|
||||
},
|
||||
Bar = {
|
||||
getComponent = function() return TestComponentBar end,
|
||||
navigation = navProp,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local testNavigation1 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
}
|
||||
|
||||
local element = Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation1,
|
||||
descriptors = makeDescriptors(testNavigation1),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = false,
|
||||
},
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
expect(fooUnmounted).to.equal(false)
|
||||
|
||||
local testNavigation2 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
}
|
||||
|
||||
instance = Roact.update(instance, Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation2,
|
||||
descriptors = makeDescriptors(testNavigation2),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = false,
|
||||
}
|
||||
}))
|
||||
|
||||
expect(fooUnmounted).to.equal(true)
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should not unmount inactive pages when keepVisitedScreensMounted is true", function()
|
||||
local fooUnmounted = false
|
||||
local TestComponentFoo = Roact.Component:extend("TestComponentFoo")
|
||||
function TestComponentFoo:render() end
|
||||
function TestComponentFoo:willUnmount()
|
||||
fooUnmounted = true
|
||||
end
|
||||
|
||||
local TestComponentBar = Roact.Component:extend("TestComponentBar")
|
||||
function TestComponentBar:render() end
|
||||
|
||||
local function makeDescriptors(navProp)
|
||||
return {
|
||||
Foo = {
|
||||
getComponent = function() return TestComponentFoo end,
|
||||
navigation = navProp,
|
||||
},
|
||||
Bar = {
|
||||
getComponent = function() return TestComponentBar end,
|
||||
navigation = navProp,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local testNavigation1 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
}
|
||||
|
||||
local element = Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation1,
|
||||
descriptors = makeDescriptors(testNavigation1),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = true,
|
||||
},
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
expect(fooUnmounted).to.equal(false)
|
||||
|
||||
local testNavigation2 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
}
|
||||
|
||||
instance = Roact.update(instance, Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation2,
|
||||
descriptors = makeDescriptors(testNavigation2),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = true,
|
||||
}
|
||||
}))
|
||||
|
||||
expect(fooUnmounted).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
expect(fooUnmounted).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should unmount inactive pages when keepVisitedScreensMounted switches from true to false", function()
|
||||
local fooUnmounted = false
|
||||
local TestComponentFoo = Roact.Component:extend("TestComponentFoo")
|
||||
function TestComponentFoo:render() end
|
||||
function TestComponentFoo:willUnmount()
|
||||
fooUnmounted = true
|
||||
end
|
||||
|
||||
local TestComponentBar = Roact.Component:extend("TestComponentBar")
|
||||
function TestComponentBar:render() end
|
||||
|
||||
local function makeDescriptors(navProp)
|
||||
return {
|
||||
Foo = {
|
||||
getComponent = function() return TestComponentFoo end,
|
||||
navigation = navProp,
|
||||
},
|
||||
Bar = {
|
||||
getComponent = function() return TestComponentBar end,
|
||||
navigation = navProp,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
local testNavigation1 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
}
|
||||
|
||||
local element = Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation1,
|
||||
descriptors = makeDescriptors(testNavigation1),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = true,
|
||||
},
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
|
||||
local testNavigation2 = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
{ routeName = "Bar", key = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
}
|
||||
|
||||
-- We must update tree to make sure active screens list gets updated first!
|
||||
instance = Roact.update(instance, Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation2,
|
||||
descriptors = makeDescriptors(testNavigation2),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = true,
|
||||
}
|
||||
}))
|
||||
|
||||
expect(fooUnmounted).to.equal(false)
|
||||
|
||||
instance = Roact.update(instance, Roact.createElement(SwitchView, {
|
||||
screenProps = {},
|
||||
navigation = testNavigation2,
|
||||
descriptors = makeDescriptors(testNavigation2),
|
||||
navigationConfig = {
|
||||
keepVisitedScreensMounted = false,
|
||||
}
|
||||
}))
|
||||
|
||||
expect(fooUnmounted).to.equal(true)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
return function()
|
||||
-- local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
-- local Transitioner = require(script.Parent.Parent.Transitioner)
|
||||
|
||||
itSKIP("should have its tests implemented", function()
|
||||
|
||||
end)
|
||||
end
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local withNavigation = require(script.Parent.Parent.withNavigation)
|
||||
local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext)
|
||||
|
||||
it("should throw if no renderProp is provided", function()
|
||||
local status, err = pcall(function()
|
||||
withNavigation(nil)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "withNavigation must be passed a render prop")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should extract navigation object from provider and pass it through", function()
|
||||
local testNavigation = {}
|
||||
local extractedNavigation = nil
|
||||
|
||||
local rootElement = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = testNavigation,
|
||||
}, {
|
||||
Child = Roact.createElement(function()
|
||||
return withNavigation(function(nav)
|
||||
extractedNavigation = nav
|
||||
end)
|
||||
end)
|
||||
})
|
||||
|
||||
local rootInstance = Roact.mount(rootElement)
|
||||
Roact.unmount(rootInstance)
|
||||
|
||||
expect(extractedNavigation).to.equal(testNavigation)
|
||||
end)
|
||||
end
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local AppNavigationContext = require(script.Parent.Parent.AppNavigationContext)
|
||||
local NavigationEvents = require(script.Parent.Parent.Parent.NavigationEvents)
|
||||
local withNavigationFocus = require(script.Parent.Parent.withNavigationFocus)
|
||||
|
||||
it("should pass focused=true when initially focused", function()
|
||||
local testNavigation, testFocused
|
||||
local component = function()
|
||||
return withNavigationFocus(function(navigation, focused)
|
||||
testNavigation = navigation
|
||||
testFocused = focused
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
local navigationProp = {
|
||||
isFocused = function()
|
||||
return true
|
||||
end,
|
||||
addListener = function()
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
local rootElement = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = navigationProp,
|
||||
}, {
|
||||
child = Roact.createElement(component)
|
||||
})
|
||||
|
||||
local instance = Roact.mount(rootElement)
|
||||
expect(testNavigation).to.equal(navigationProp)
|
||||
expect(testFocused).to.equal(true)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should pass focused=false when initially unfocused", function()
|
||||
local testNavigation, testFocused
|
||||
local component = function()
|
||||
return withNavigationFocus(function(navigation, focused)
|
||||
testNavigation = navigation
|
||||
testFocused = focused
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
local navigationProp = {
|
||||
isFocused = function()
|
||||
return false
|
||||
end,
|
||||
addListener = function()
|
||||
return {
|
||||
disconnect = function() end
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
local rootElement = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = navigationProp,
|
||||
}, {
|
||||
child = Roact.createElement(component)
|
||||
})
|
||||
|
||||
local instance = Roact.mount(rootElement)
|
||||
expect(testNavigation).to.equal(navigationProp)
|
||||
expect(testFocused).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should re-render and set focused status for events", function()
|
||||
local testListeners = {}
|
||||
local testFocused = false
|
||||
local component = function()
|
||||
return withNavigationFocus(function(navigation, focused)
|
||||
testFocused = focused
|
||||
return nil
|
||||
end)
|
||||
end
|
||||
|
||||
local navigationProp = {
|
||||
isFocused = function()
|
||||
return false
|
||||
end,
|
||||
addListener = function(event, listener)
|
||||
testListeners[event] = listener
|
||||
return {
|
||||
disconnect = function()
|
||||
testListeners[event] = nil
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
|
||||
local rootElement = Roact.createElement(AppNavigationContext.Provider, {
|
||||
navigation = navigationProp,
|
||||
}, {
|
||||
child = Roact.createElement(component)
|
||||
})
|
||||
|
||||
local instance = Roact.mount(rootElement)
|
||||
expect(testFocused).to.equal(false)
|
||||
expect(type(testListeners[NavigationEvents.DidFocus])).to.equal("function")
|
||||
expect(type(testListeners[NavigationEvents.WillBlur])).to.equal("function")
|
||||
|
||||
testListeners[NavigationEvents.DidFocus]()
|
||||
expect(testFocused).to.equal(true)
|
||||
|
||||
testListeners[NavigationEvents.WillBlur]()
|
||||
expect(testFocused).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
expect(testListeners[NavigationEvents.DidFocus]).to.equal(nil)
|
||||
expect(testListeners[NavigationEvents.WillBlur]).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when renderProp is not provided", function()
|
||||
local success, err = pcall(function()
|
||||
withNavigationFocus(nil)
|
||||
end)
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(string.find(err,
|
||||
"withNavigationFocus must be passed a render prop")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when used outside of a navigation provider", function()
|
||||
local component = function()
|
||||
return withNavigationFocus(function(navigation, focused)
|
||||
|
||||
end)
|
||||
end
|
||||
|
||||
local element = Roact.createElement(component)
|
||||
|
||||
local success, _ = pcall(function()
|
||||
Roact.unmount(Roact.mount(element))
|
||||
end)
|
||||
|
||||
expect(success).to.equal(false)
|
||||
-- We do not test the message because NavigationConsumer gets in the way here.
|
||||
end)
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user