Skip to content

Commit

Permalink
Hide popovers on navigate
Browse files Browse the repository at this point in the history
- Allow non-component EventManipulator hosts
- Emit 'Navigate' event from EventManipulator
- Ensure navigator exists before any components
  • Loading branch information
ChiriVulpes committed Feb 9, 2025
1 parent 0cbf45e commit 5402b34
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 24 deletions.
5 changes: 4 additions & 1 deletion src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ async function App (): Promise<App> {
Async.sleep(Time.seconds(2)),
])

const navigate = Navigator()

HoverListener.listen()
ActiveListener.listen()
FocusListener.listen()
Expand Down Expand Up @@ -115,9 +117,10 @@ async function App (): Promise<App> {
.append(masthead, masthead.sidebar, content)
.append(ToastList())
.extend<AppExtensions>(app => ({
navigate: Navigator(app),
navigate,
view,
}))
.tweak(Navigator.setApp)
.appendTo(document.body)

await app.navigate.fromURL()
Expand Down
35 changes: 32 additions & 3 deletions src/navigation/Navigate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type App from 'App'
import type { RoutePath } from 'navigation/Routes'
import Routes from 'navigation/Routes'
import EventManipulator from 'ui/utility/EventManipulator'
import ErrorView from 'ui/view/ErrorView'
import type ViewContainer from 'ui/view/shared/component/ViewContainer'
import Env from 'utility/Env'
Expand All @@ -9,7 +10,12 @@ declare global {
export const navigate: Navigator
}

export interface NavigatorEvents {
Navigate (route: RoutePath | undefined): void
}

interface Navigator {
event: EventManipulator<this, NavigatorEvents>
isURL (glob: string): boolean
fromURL (): Promise<void>
toURL (route: RoutePath): Promise<void>
Expand All @@ -18,9 +24,10 @@ interface Navigator {
ephemeral: ViewContainer['showEphemeral']
}

function Navigator (app: App): Navigator {
function Navigator (): Navigator {
let lastURL: URL | undefined
const navigate = {
event: undefined! as Navigator['event'],
isURL: (glob: string) => {
const pattern = glob
.replace(/(?<=\/)\*(?!\*)/g, '[^/]*')
Expand All @@ -31,9 +38,13 @@ function Navigator (app: App): Navigator {
if (location.href === lastURL?.href)
return

if (!app)
throw new Error('Cannot navigate yet, no app instance')

const oldURL = lastURL
lastURL = new URL(location.href)

let matchedRoute: RoutePath | undefined
let errored = false
if (location.pathname !== oldURL?.pathname) {
const url = location.pathname
Expand All @@ -43,6 +54,8 @@ function Navigator (app: App): Navigator {
if (!params)
continue

matchedRoute = route.path

await route.handler(app, (!Object.keys(params).length ? undefined : params) as never)
handled = true
break
Expand All @@ -66,6 +79,8 @@ function Navigator (app: App): Navigator {
element.scrollIntoView()
element.focus()
}

navigate.event.emit('Navigate', matchedRoute)
},
toURL: async (url: string) => {
navigate.setURL(url)
Expand Down Expand Up @@ -101,15 +116,29 @@ function Navigator (app: App): Navigator {
console.error(`Unsupported raw URL to navigate to: "${url}"`)
return false
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return
ephemeral: (...args: unknown[]) => (app.view.showEphemeral as any)(...args),
ephemeral: (...args: unknown[]) => {
if (!app)
throw new Error('Cannot show ephemeral view yet, no app instance')

// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return (app.view.showEphemeral as any)(...args)
},
}

navigate.event = EventManipulator(navigate)

// eslint-disable-next-line @typescript-eslint/no-misused-promises
window.addEventListener('popstate', navigate.fromURL)

Object.assign(window, { navigate })
return navigate
}

let app: App | undefined
namespace Navigator {
export function setApp (instance: App) {
app = instance
}
}

export default Navigator
2 changes: 1 addition & 1 deletion src/ui/component/OAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const OAuthService = Component.Builder((component, service: AuthService, reauthD
await Session.Auth.auth(service)

auth = Session.Auth.get(service.name)
event.component.setChecked(!!auth)
event.host.setChecked(!!auth)
})
})

Expand Down
4 changes: 2 additions & 2 deletions src/ui/component/core/ConfirmDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ const ConfirmDialog = Object.assign(
(await ConfirmDialog(definition))
.appendTo(document.body)
.event.subscribe('close', event =>
event.component.event.subscribe('transitionend', event =>
event.component.remove()))
event.host.event.subscribe('transitionend', event =>
event.host.remove()))
.await(owner),
},
)
Expand Down
9 changes: 8 additions & 1 deletion src/ui/component/core/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Component.extend(component => {
})
.tweak(initialiser, component)
.event.subscribe('toggle', e => {
const event = e as ToggleEvent & { component: Popover }
const event = e as ToggleEvent & { host: Popover }
if (event.newState === 'closed') {
isShown = false
component.clickState = false
Expand All @@ -89,6 +89,13 @@ Component.extend(component => {
component.ariaLabel.use((quilt, { arg }) => quilt['component/popover/button'](arg(ariaLabel), arg(ariaRole)))
popover.ariaLabel.use((quilt, { arg }) => quilt['component/popover'](arg(ariaLabel)))

navigate.event.subscribe('Navigate', forceClose)
popover.removed.awaitManual(true, () => navigate.event.unsubscribe('Navigate', forceClose))
function forceClose () {
component.clickState = false
popover.hide()
}

component.clickState = false
if (!component.popover) {
component.event.subscribe('click', async event => {
Expand Down
8 changes: 4 additions & 4 deletions src/ui/component/core/TextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ const TextEditor = Component.Builder((component): TextEditor => {
.receiveFocusedClickEvents()
.event.subscribe('click', event => {
event.preventDefault()
handler(event.component)
handler(event.host)
})
})

Expand All @@ -911,7 +911,7 @@ const TextEditor = Component.Builder((component): TextEditor => {
.receiveFocusedClickEvents()
.event.subscribe('click', event => {
event.preventDefault()
toggler(event.component)
toggler(event.host)
})
})

Expand All @@ -925,7 +925,7 @@ const TextEditor = Component.Builder((component): TextEditor => {
.receiveFocusedClickEvents()
.event.subscribe('click', event => {
event.preventDefault()
toggler(event.component)
toggler(event.host)
})
})

Expand All @@ -946,7 +946,7 @@ const TextEditor = Component.Builder((component): TextEditor => {
})
.receiveAncestorInsertEvents()
.event.subscribe(['insert', 'ancestorInsert'], event =>
event.component.style.toggle(!!event.component.closest(Popover), 'text-editor-toolbar-button--has-popover--within-popover'))
event.host.style.toggle(!!event.host.closest(Popover), 'text-editor-toolbar-button--has-popover--within-popover'))
})

//#endregion
Expand Down
30 changes: 19 additions & 11 deletions src/ui/utility/EventManipulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type Component from 'ui/Component'
import Arrays from 'utility/Arrays'
import type { AnyFunction } from 'utility/Type'

type EventParameters<HOST, EVENTS, EVENT extends keyof EVENTS> = EVENTS[EVENT] extends (...params: infer PARAMS) => unknown ? PARAMS extends [infer EVENT extends Event, ...infer PARAMS] ? [EVENT & { component: HOST }, ...PARAMS] : [Event & { component: HOST }, ...PARAMS] : never
type EventParameters<HOST, EVENTS, EVENT extends keyof EVENTS> = EVENTS[EVENT] extends (...params: infer PARAMS) => unknown ? PARAMS extends [infer EVENT extends Event, ...infer PARAMS] ? [EVENT & { host: HOST }, ...PARAMS] : [Event & { host: HOST }, ...PARAMS] : never
type EventParametersEmit<EVENTS, EVENT extends keyof EVENTS> = EVENTS[EVENT] extends (...params: infer PARAMS) => unknown ? PARAMS extends [Event, ...infer PARAMS] ? PARAMS : PARAMS : never
type EventResult<EVENTS, EVENT extends keyof EVENTS> = EVENTS[EVENT] extends (...params: any[]) => infer RESULT ? RESULT : never

Expand Down Expand Up @@ -50,7 +50,15 @@ interface EventHandlerRegistered extends AnyFunction {
[SYMBOL_REGISTERED_FUNCTION]?: AnyFunction
}

function EventManipulator (component: Component): EventManipulator<Component, NativeEvents> {
function isComponent (host: unknown): host is Component {
return typeof host === 'object' && host !== null && 'isComponent' in host
}

function EventManipulator<T extends object> (host: T): EventManipulator<T, NativeEvents> {
const elementHost = isComponent(host)
? host
: { element: document.createElement('span') }

return {
emit (event, ...params) {
const detail: EventDetail = { result: [], params }
Expand All @@ -71,7 +79,7 @@ function EventManipulator (component: Component): EventManipulator<Component, Na
},
}
)
component.element.dispatchEvent(eventObject)
elementHost.element.dispatchEvent(eventObject)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.assign(detail.result, { defaultPrevented: eventObject.defaultPrevented || preventedDefault, stoppedPropagation }) as any
},
Expand All @@ -94,7 +102,7 @@ function EventManipulator (component: Component): EventManipulator<Component, Na
},
}
)
component.element.dispatchEvent(eventObject)
elementHost.element.dispatchEvent(eventObject)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Object.assign(detail.result, { defaultPrevented: eventObject.defaultPrevented || preventedDefault, stoppedPropagation }) as any
},
Expand All @@ -107,37 +115,37 @@ function EventManipulator (component: Component): EventManipulator<Component, Na
unsubscribe (events, handler) {
const realHandler = (handler as EventHandlerRegistered)[SYMBOL_REGISTERED_FUNCTION]
if (!realHandler)
return component
return host

delete (handler as EventHandlerRegistered)[SYMBOL_REGISTERED_FUNCTION]

for (const event of Arrays.resolve(events))
component.element.removeEventListener(event, realHandler)
elementHost.element.removeEventListener(event, realHandler)

return component
return host
},
}

function subscribe (handler: EventHandlerRegistered, events: Arrays.Or<keyof NativeEvents>, options?: AddEventListenerOptions) {
if (handler[SYMBOL_REGISTERED_FUNCTION]) {
console.error(`Can't register handler for event(s) ${Arrays.resolve(events).join(', ')}, already used for other events`, handler)
return component
return host
}

const realHandler = (event: Event) => {
const customEvent = event instanceof CustomEvent ? event : undefined
const eventDetail = customEvent?.detail as EventDetail | undefined
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
const result = (handler as any)(Object.assign(event, { component }), ...eventDetail?.params ?? [])
const result = (handler as any)(Object.assign(event, { host }), ...eventDetail?.params ?? [])
eventDetail?.result.push(result)
}

Object.assign(handler, { [SYMBOL_REGISTERED_FUNCTION]: realHandler })

for (const event of Arrays.resolve(events))
component.element.addEventListener(event, realHandler, options)
elementHost.element.addEventListener(event, realHandler, options)

return component
return host
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/ui/view/TestView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default ViewDefinition({
.style.setProperty('padding', '0.5em')
.style.setProperty('box-sizing', 'border-box')
.event.subscribe('input', event => {
const text = event.component.element.textContent ?? ''
const text = event.host.element.textContent ?? ''
const md = new MarkdownIt('commonmark', { html: true, breaks: true })
MarkdownItHTML.use(md, MarkdownItHTML.Options()
.disallowTags('img', 'figure', 'figcaption', 'map', 'area'))
Expand Down

0 comments on commit 5402b34

Please sign in to comment.