Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbe42b6164 |
Binary file not shown.
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.warren.iwanttoheal"
|
applicationId "com.warren.iwanttoheal"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 78
|
versionCode 79
|
||||||
versionName "1.0.59"
|
versionName "1.0.60"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -37,6 +38,18 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
if (hasFocus) enableImmersiveMode();
|
if (hasFocus) enableImmersiveMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean dispatchTouchEvent(MotionEvent event) {
|
||||||
|
if (
|
||||||
|
event.getActionMasked() == MotionEvent.ACTION_DOWN
|
||||||
|
&& bridge != null
|
||||||
|
&& bridge.getWebView() != null
|
||||||
|
) {
|
||||||
|
bridge.getWebView().requestFocus();
|
||||||
|
}
|
||||||
|
return super.dispatchTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
private void loadIntentUrl() {
|
private void loadIntentUrl() {
|
||||||
if (bridge == null || getIntent() == null) return;
|
if (bridge == null || getIntent() == null) return;
|
||||||
String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL);
|
String initialUrl = getIntent().getStringExtra(EXTRA_INITIAL_URL);
|
||||||
@@ -86,7 +99,10 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
|||||||
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
|
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
|
||||||
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
|
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
|
||||||
bridge.getWebView().post(
|
bridge.getWebView().post(
|
||||||
() -> bridge.getWebView().evaluateJavascript(script, null)
|
() -> {
|
||||||
|
bridge.getWebView().requestFocus();
|
||||||
|
bridge.getWebView().evaluateJavascript(script, null);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -2782,6 +2782,19 @@ function updatePvpMatchState(session, matchId, payload) {
|
|||||||
alive: Boolean(payload.alive),
|
alive: Boolean(payload.alive),
|
||||||
elapsedTicks: Math.max(0, Math.floor(Number(payload.elapsedTicks) || 0)),
|
elapsedTicks: Math.max(0, Math.floor(Number(payload.elapsedTicks) || 0)),
|
||||||
}
|
}
|
||||||
|
const currentProgress = match.progress[side]
|
||||||
|
if (
|
||||||
|
currentProgress
|
||||||
|
&& (
|
||||||
|
progress.stage < currentProgress.stage
|
||||||
|
|| (
|
||||||
|
progress.stage === currentProgress.stage
|
||||||
|
&& progress.encounterIndex < currentProgress.encounterIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return pvpSnapshot(match)
|
||||||
|
}
|
||||||
match.states[side] = payload.state ?? null
|
match.states[side] = payload.state ?? null
|
||||||
match.statuses[side] = status
|
match.statuses[side] = status
|
||||||
match.progress[side] = progress
|
match.progress[side] = progress
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ export function PvpStadiumScreen({
|
|||||||
const { enabled: dualScreenEnabled } = useDualScreen()
|
const { enabled: dualScreenEnabled } = useDualScreen()
|
||||||
const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}`
|
const opponentLabel = liveMatch ? liveMatch.opponentName : `CPU ${cpuDifficulty ?? 1}`
|
||||||
const playerAlive = playerSide.party.some((member) => member.health > 0)
|
const playerAlive = playerSide.party.some((member) => member.health > 0)
|
||||||
|
const partyColumns = 3
|
||||||
|
|
||||||
const setSelectedTargetId = useCallback((id: string) => {
|
const setSelectedTargetId = useCallback((id: string) => {
|
||||||
selectedIdRef.current = id
|
selectedIdRef.current = id
|
||||||
@@ -841,6 +842,55 @@ export function PvpStadiumScreen({
|
|||||||
if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal')
|
if (succeeded) addLog(`${spell.name} cast on ${playerRef.current.party.find((member) => member.id === targetId)?.name ?? 'target'}.`, 'heal')
|
||||||
}, [addLog, applySpell, playerAlive, status])
|
}, [addLog, applySpell, playerAlive, status])
|
||||||
|
|
||||||
|
const selectRelativeTarget = useCallback((direction: -1 | 1) => {
|
||||||
|
const living = playerRef.current.party.filter((member) => member.health > 0)
|
||||||
|
if (living.length === 0) return
|
||||||
|
const currentIndex = living.findIndex((member) => member.id === selectedIdRef.current)
|
||||||
|
const nextIndex = currentIndex < 0
|
||||||
|
? 0
|
||||||
|
: (currentIndex + direction + living.length) % living.length
|
||||||
|
setSelectedTargetId(living[nextIndex].id)
|
||||||
|
}, [setSelectedTargetId])
|
||||||
|
|
||||||
|
const selectDirectionalTarget = useCallback((action: InputAction) => {
|
||||||
|
const currentIndex = playerRef.current.party.findIndex((member) => member.id === selectedIdRef.current)
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
const firstLiving = playerRef.current.party.find((member) => member.health > 0)
|
||||||
|
if (firstLiving) setSelectedTargetId(firstLiving.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentRow = Math.floor(currentIndex / partyColumns)
|
||||||
|
const currentColumn = currentIndex % partyColumns
|
||||||
|
const candidates = playerRef.current.party
|
||||||
|
.map((member, index) => ({
|
||||||
|
member,
|
||||||
|
index,
|
||||||
|
row: Math.floor(index / partyColumns),
|
||||||
|
column: index % partyColumns,
|
||||||
|
}))
|
||||||
|
.filter(({ member, index, row, column }) => {
|
||||||
|
if (member.health <= 0 || index === currentIndex) return false
|
||||||
|
if (action === 'navigateLeft') return row === currentRow && column < currentColumn
|
||||||
|
if (action === 'navigateRight') return row === currentRow && column > currentColumn
|
||||||
|
if (action === 'navigateUp') return row < currentRow
|
||||||
|
return row > currentRow
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const horizontal = action === 'navigateLeft' || action === 'navigateRight'
|
||||||
|
const aPrimary = horizontal ? Math.abs(a.column - currentColumn) : Math.abs(a.row - currentRow)
|
||||||
|
const bPrimary = horizontal ? Math.abs(b.column - currentColumn) : Math.abs(b.row - currentRow)
|
||||||
|
const aSecondary = horizontal ? 0 : Math.abs(a.column - currentColumn)
|
||||||
|
const bSecondary = horizontal ? 0 : Math.abs(b.column - currentColumn)
|
||||||
|
return aPrimary - bPrimary || aSecondary - bSecondary
|
||||||
|
})
|
||||||
|
if (candidates[0]) setSelectedTargetId(candidates[0].member.id)
|
||||||
|
}, [partyColumns, setSelectedTargetId])
|
||||||
|
|
||||||
|
const selectDirectTarget = useCallback((slot: number) => {
|
||||||
|
const member = playerRef.current.party[slot]
|
||||||
|
if (member?.health > 0) setSelectedTargetId(member.id)
|
||||||
|
}, [setSelectedTargetId])
|
||||||
|
|
||||||
const cpuTakeTurn = useCallback(() => {
|
const cpuTakeTurn = useCallback(() => {
|
||||||
if (!cpuDifficulty || status !== 'playing') return
|
if (!cpuDifficulty || status !== 'playing') return
|
||||||
const behavior = CPU_BEHAVIOR[cpuDifficulty]
|
const behavior = CPU_BEHAVIOR[cpuDifficulty]
|
||||||
@@ -1032,6 +1082,18 @@ export function PvpStadiumScreen({
|
|||||||
setShopReady(false)
|
setShopReady(false)
|
||||||
setShopPoints(0)
|
setShopPoints(0)
|
||||||
addLog(`Round ${nextRound} starts. HP and mana restored.`, 'system')
|
addLog(`Round ${nextRound} starts. HP and mana restored.`, 'system')
|
||||||
|
if (liveMatchRef.current) {
|
||||||
|
publishPvpMatchState<StadiumSideState>(liveMatchRef.current.id, {
|
||||||
|
state: nextPlayer,
|
||||||
|
status: 'playing',
|
||||||
|
stage: nextRound,
|
||||||
|
encounterIndex: nextRound,
|
||||||
|
encountersCleared: roundWins.player,
|
||||||
|
enemyHealth: 0,
|
||||||
|
alive: true,
|
||||||
|
elapsedTicks: 0,
|
||||||
|
}).catch(() => undefined)
|
||||||
|
}
|
||||||
beginRoundCountdown()
|
beginRoundCountdown()
|
||||||
}, [addLog, beginRoundCountdown, cpuPartyTemplate, partyTemplate, roundIndex, roundWins.opponent, roundWins.player, setSelectedTargetId])
|
}, [addLog, beginRoundCountdown, cpuPartyTemplate, partyTemplate, roundIndex, roundWins.opponent, roundWins.player, setSelectedTargetId])
|
||||||
|
|
||||||
@@ -1086,7 +1148,7 @@ export function PvpStadiumScreen({
|
|||||||
.then((snapshot) => {
|
.then((snapshot) => {
|
||||||
if (stopped) return
|
if (stopped) return
|
||||||
const opponentState = snapshot.states[liveMatch.opponentSide]
|
const opponentState = snapshot.states[liveMatch.opponentSide]
|
||||||
if (opponentState) {
|
if (opponentState && opponentState.roundIndex >= roundIndex) {
|
||||||
cpuRef.current = opponentState
|
cpuRef.current = opponentState
|
||||||
setCpuSide(opponentState)
|
setCpuSide(opponentState)
|
||||||
}
|
}
|
||||||
@@ -1094,6 +1156,7 @@ export function PvpStadiumScreen({
|
|||||||
if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') setStatus('lost')
|
if (opponentStatus === 'won' && status !== 'won' && status !== 'lost') setStatus('lost')
|
||||||
if (opponentStatus === 'lost' && status !== 'won' && status !== 'lost') setStatus('won')
|
if (opponentStatus === 'lost' && status !== 'won' && status !== 'lost') setStatus('won')
|
||||||
if (!opponentState) return
|
if (!opponentState) return
|
||||||
|
if (opponentState.roundIndex < roundIndex) return
|
||||||
if (status === 'playing' && opponentState.roundIndex === roundIndex && opponentState.roundStatus === 'shop' && opponentState.lastRoundOutcome) {
|
if (status === 'playing' && opponentState.roundIndex === roundIndex && opponentState.roundStatus === 'shop' && opponentState.lastRoundOutcome) {
|
||||||
const key = `${roundIndex}-${opponentState.lastRoundOutcome}`
|
const key = `${roundIndex}-${opponentState.lastRoundOutcome}`
|
||||||
if (loggedOpponentRoundRef.current === key) return
|
if (loggedOpponentRoundRef.current === key) return
|
||||||
@@ -1102,7 +1165,14 @@ export function PvpStadiumScreen({
|
|||||||
else if (opponentState.lastRoundOutcome === 'win') finishRound('loss')
|
else if (opponentState.lastRoundOutcome === 'win') finishRound('loss')
|
||||||
else finishRound('tie')
|
else finishRound('tie')
|
||||||
}
|
}
|
||||||
if (status === 'shop' && shopReady && opponentState.roundIndex === roundIndex && opponentState.shopReady) {
|
if (
|
||||||
|
status === 'shop'
|
||||||
|
&& shopReady
|
||||||
|
&& (
|
||||||
|
(opponentState.roundIndex === roundIndex && opponentState.shopReady)
|
||||||
|
|| opponentState.roundIndex > roundIndex
|
||||||
|
)
|
||||||
|
) {
|
||||||
startNextRound()
|
startNextRound()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1178,15 +1248,31 @@ export function PvpStadiumScreen({
|
|||||||
window.requestAnimationFrame(() => focusFirstControl())
|
window.requestAnimationFrame(() => focusFirstControl())
|
||||||
}, [status])
|
}, [status])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!paused) return
|
||||||
|
window.requestAnimationFrame(() => focusFirstControl())
|
||||||
|
}, [paused])
|
||||||
|
|
||||||
useGameAction((action) => {
|
useGameAction((action) => {
|
||||||
if (action === 'pause') {
|
if (action === 'pause' || action === 'back') {
|
||||||
if (status === 'playing') setPaused((value) => !value)
|
if (status === 'playing') setPaused((value) => !value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (paused || status !== 'playing') return
|
||||||
|
if (action.startsWith('navigate')) {
|
||||||
|
selectDirectionalTarget(action)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action === 'previousTarget') {
|
||||||
|
selectRelativeTarget(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (action === 'nextTarget') {
|
||||||
|
selectRelativeTarget(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (action.startsWith('targetParty')) {
|
if (action.startsWith('targetParty')) {
|
||||||
const index = Number(action.slice('targetParty'.length)) - 1
|
selectDirectTarget(Number(action.slice('targetParty'.length)) - 1)
|
||||||
const member = playerRef.current.party[index]
|
|
||||||
if (member?.health > 0) setSelectedTargetId(member.id)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (action.startsWith('ability')) {
|
if (action.startsWith('ability')) {
|
||||||
|
|||||||
+60
-17
@@ -125,6 +125,9 @@ const STORAGE_KEY = 'ashen-halls-input-bindings-v1'
|
|||||||
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
|
const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
|
||||||
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
|
const GAME_ACTION_EVENT = 'ashen-halls-game-action'
|
||||||
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
|
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
|
||||||
|
const FOCUSABLE_SELECTOR = 'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])'
|
||||||
|
|
||||||
|
let lastControllerFocus: HTMLElement | null = null
|
||||||
|
|
||||||
type CaptureState = {
|
type CaptureState = {
|
||||||
device: InputDevice
|
device: InputDevice
|
||||||
@@ -234,25 +237,41 @@ function focusableElements() {
|
|||||||
).find(isVisible)
|
).find(isVisible)
|
||||||
const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
|
const scope: ParentNode = keyboard ?? pauseMenu ?? dialog ?? document
|
||||||
return Array.from(
|
return Array.from(
|
||||||
scope.querySelectorAll<HTMLElement>(
|
scope.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
|
||||||
'button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])',
|
|
||||||
),
|
|
||||||
).filter(isVisible)
|
).filter(isVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rememberFocusableControl(element: HTMLElement) {
|
||||||
|
lastControllerFocus = element
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusControl(element: HTMLElement) {
|
||||||
|
rememberFocusableControl(element)
|
||||||
|
element.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentFocusableControl(candidates = focusableElements()) {
|
||||||
|
const active = document.activeElement
|
||||||
|
if (active instanceof HTMLElement && candidates.includes(active)) {
|
||||||
|
rememberFocusableControl(active)
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
if (lastControllerFocus && candidates.includes(lastControllerFocus) && isVisible(lastControllerFocus)) {
|
||||||
|
return lastControllerFocus
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export function focusFirstControl() {
|
export function focusFirstControl() {
|
||||||
const first = focusableElements()[0]
|
const first = focusableElements()[0]
|
||||||
first?.focus({ preventScroll: true })
|
if (first) focusControl(first)
|
||||||
return first
|
return first
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveFocus(action: InputAction) {
|
function moveFocus(action: InputAction) {
|
||||||
const candidates = focusableElements()
|
const candidates = focusableElements()
|
||||||
if (candidates.length === 0) return
|
if (candidates.length === 0) return
|
||||||
const current = document.activeElement instanceof HTMLElement
|
const current = currentFocusableControl(candidates)
|
||||||
&& candidates.includes(document.activeElement)
|
|
||||||
? document.activeElement
|
|
||||||
: null
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
focusFirstControl()
|
focusFirstControl()
|
||||||
return
|
return
|
||||||
@@ -279,7 +298,7 @@ function moveFocus(action: InputAction) {
|
|||||||
|
|
||||||
const next = ranked[0]?.candidate
|
const next = ranked[0]?.candidate
|
||||||
if (!next) return
|
if (!next) return
|
||||||
next.focus({ preventScroll: true })
|
focusControl(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasUiOverlay() {
|
function hasUiOverlay() {
|
||||||
@@ -456,12 +475,12 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
|||||||
if (action.startsWith('navigate')) {
|
if (action.startsWith('navigate')) {
|
||||||
if (uiOverlay || !combatActive) moveFocus(action)
|
if (uiOverlay || !combatActive) moveFocus(action)
|
||||||
} else if (action === 'confirm') {
|
} else if (action === 'confirm') {
|
||||||
const active = document.activeElement
|
const active = currentFocusableControl()
|
||||||
if (isTextInput(active)) {
|
if (isTextInput(active)) {
|
||||||
setKeyboardInput(active)
|
setKeyboardInput(active)
|
||||||
window.requestAnimationFrame(() => focusFirstControl())
|
window.requestAnimationFrame(() => focusFirstControl())
|
||||||
} else if (
|
} else if (
|
||||||
active instanceof HTMLElement
|
active
|
||||||
&& active.matches('button:not(:disabled), [role="button"]')
|
&& active.matches('button:not(:disabled), [role="button"]')
|
||||||
&& isVisible(active)
|
&& isVisible(active)
|
||||||
) {
|
) {
|
||||||
@@ -581,6 +600,29 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
|||||||
return () => window.removeEventListener('keydown', onKeyDown)
|
return () => window.removeEventListener('keydown', onKeyDown)
|
||||||
}, [assignBinding, dispatchAction])
|
}, [assignBinding, dispatchAction])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onFocusIn = (event: FocusEvent) => {
|
||||||
|
const target = event.target
|
||||||
|
if (!(target instanceof HTMLElement)) return
|
||||||
|
if (!target.matches(FOCUSABLE_SELECTOR) || !isVisible(target)) return
|
||||||
|
rememberFocusableControl(target)
|
||||||
|
}
|
||||||
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
|
document.documentElement.dataset.inputDevice = 'pc'
|
||||||
|
const target = event.target
|
||||||
|
if (!(target instanceof Element)) return
|
||||||
|
const control = target.closest<HTMLElement>(FOCUSABLE_SELECTOR)
|
||||||
|
if (!control || !isVisible(control)) return
|
||||||
|
rememberFocusableControl(control)
|
||||||
|
}
|
||||||
|
document.addEventListener('focusin', onFocusIn)
|
||||||
|
document.addEventListener('pointerdown', onPointerDown, { capture: true })
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('focusin', onFocusIn)
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown, { capture: true })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (event: Event) => {
|
const listener = (event: Event) => {
|
||||||
const detail = (event as CustomEvent<{ token: string; repeat?: boolean }>).detail
|
const detail = (event as CustomEvent<{ token: string; repeat?: boolean }>).detail
|
||||||
@@ -595,16 +637,17 @@ export function InputProvider({ children }: { children: ReactNode }) {
|
|||||||
const combatActive = document.querySelector('[data-combat-active="true"]')
|
const combatActive = document.querySelector('[data-combat-active="true"]')
|
||||||
if (combatActive) return
|
if (combatActive) return
|
||||||
const candidates = focusableElements()
|
const candidates = focusableElements()
|
||||||
const active = document.activeElement
|
const activeControl = currentFocusableControl(candidates)
|
||||||
const activeIsUsable = active instanceof HTMLElement
|
|
||||||
&& candidates.includes(active)
|
|
||||||
&& isVisible(active)
|
|
||||||
if (
|
if (
|
||||||
(!activeIsUsable || document.activeElement === document.body)
|
(!activeControl || document.activeElement === document.body)
|
||||||
&& !keyboardInputRef.current
|
&& !keyboardInputRef.current
|
||||||
&& !captureRef.current
|
&& !captureRef.current
|
||||||
) {
|
) {
|
||||||
focusFirstControl()
|
if (activeControl) {
|
||||||
|
focusControl(activeControl)
|
||||||
|
} else {
|
||||||
|
focusFirstControl()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user