Skip to content

Commit

Permalink
chore: impl vue and solid
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Jan 9, 2025
1 parent 675cf47 commit 511d7ab
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 14 deletions.
Binary file modified bun.lockb
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'
import { createSignal, Show } from 'solid-js'

export const Autofocus = () => {
const [trapped, setTrapped] = createSignal(false)
let buttonRef: HTMLButtonElement | undefined
let buttonRef!: HTMLButtonElement

return (
<div>
<button ref={buttonRef} onClick={() => setTrapped((v) => !v)}>
{trapped() ? 'End Trap' : 'Start Trap'}
</button>
{trapped() && (
<Show when={trapped()}>
<FocusTrap disabled={!trapped()} setReturnFocus={buttonRef}>
<div
style={{
Expand All @@ -25,7 +25,7 @@ export const Autofocus = () => {
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
)}
</Show>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { createSignal } from 'solid-js'

export const Basic = () => {
const [trapped, setTrapped] = createSignal(false)

return (
<>
<button onClick={() => setTrapped(true)}>Start Trap</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { createSignal } from 'solid-js'

export const InitialFocus = () => {
const [trapped, setTrapped] = createSignal(false)
let inputRef: HTMLInputElement | undefined
const toggle = () => setTrapped((v) => !v)

let inputRef!: HTMLInputElement

return (
<div>
<button onClick={() => setTrapped((v) => !v)}>{trapped() ? 'End Trap' : 'Start Trap'}</button>
<button onClick={toggle}>{trapped() ? 'End Trap' : 'Start Trap'}</button>
<FocusTrap disabled={!trapped()} initialFocus={() => inputRef}>
<div
style={{
Expand All @@ -20,7 +22,7 @@ export const InitialFocus = () => {
<input type="text" placeholder="First input" />
<input ref={inputRef} type="text" placeholder="Second input (initial focus)" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
<button onClick={toggle}>End Trap</button>
</div>
</FocusTrap>
</div>
Expand Down
15 changes: 9 additions & 6 deletions packages/solid/src/components/focus-trap/focus-trap.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type FocusTrapOptions, trapFocus } from '@zag-js/focus-trap'
import { createEffect, onCleanup } from 'solid-js'
import type { Assign } from '../../types'
import { composeRefs } from '../../utils/compose-refs'
import { createSplitProps } from '../../utils/create-split-props'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { createEffect } from 'solid-js'
import { composeRefs } from '../../utils/compose-refs'

export interface TrapOptions extends Omit<FocusTrapOptions, 'document' | 'trapStack'> {
disabled?: boolean
Expand All @@ -13,7 +13,7 @@ export interface FocusTrapBaseProps extends PolymorphicProps<'div'>, TrapOptions
export interface FocusTrapProps extends Assign<HTMLProps<'div'>, FocusTrapBaseProps> {}

export const FocusTrap = (props: FocusTrapProps) => {
let localRef: HTMLDivElement | undefined
let localNode!: HTMLDivElement

const [trapProps, localProps] = createSplitProps<TrapOptions>()(props, [
'disabled',
Expand Down Expand Up @@ -41,9 +41,12 @@ export const FocusTrap = (props: FocusTrapProps) => {
])

createEffect(() => {
if (!localRef || trapProps.disabled) return
return trapFocus(localRef, trapProps)
if (!localNode || trapProps.disabled) return
const autoFocusNode = localNode.querySelector<HTMLElement>('[autofocus], [data-autofocus]')
trapProps.initialFocus ||= autoFocusNode ?? undefined
onCleanup(trapFocus(localNode, trapProps))
})

return <ark.div ref={composeRefs(localRef, props.ref)} {...localProps} />
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
return <ark.div {...localProps} ref={composeRefs((el) => (localNode = el), props.ref)} />
}
1 change: 1 addition & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@
"@zag-js/highlight-word": "0.81.0",
"@zag-js/hover-card": "0.81.0",
"@zag-js/i18n-utils": "0.81.0",
"@zag-js/focus-trap": "0.81.0",
"@zag-js/file-utils": "0.81.0",
"@zag-js/menu": "0.81.0",
"@zag-js/number-input": "0.81.0",
Expand Down
22 changes: 22 additions & 0 deletions packages/vue/src/components/focus-trap/examples/autofocus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { ref } from 'vue'
import { FocusTrap } from '@ark-ui/vue/focus-trap'
const trapped = ref(false)
const buttonRef = ref<HTMLButtonElement>()
</script>

<template>
<div>
<button ref="buttonRef" @click="trapped = !trapped">
{{ trapped ? 'End Trap' : 'Start Trap' }}
</button>
<FocusTrap v-if="trapped" :disabled="!trapped" :set-return-focus="buttonRef">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="Regular input" />
<input type="text" placeholder="Autofocused input" autofocus />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</div>
</template>
17 changes: 17 additions & 0 deletions packages/vue/src/components/focus-trap/examples/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import { ref } from 'vue'
import { FocusTrap } from '@ark-ui/vue/focus-trap'
const trapped = ref(false)
</script>

<template>
<button @click="trapped = true">Start Trap</button>
<FocusTrap :return-focus-on-deactivate="false" :disabled="!trapped">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</template>
24 changes: 24 additions & 0 deletions packages/vue/src/components/focus-trap/examples/initial-focus.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import { FocusTrap } from '@ark-ui/vue/focus-trap'
const trapped = ref(false)
const inputRef = ref<HTMLInputElement>()
const toggle = () => {
trapped.value = !trapped.value
}
</script>

<template>
<div>
<button @click="toggle">{{ trapped ? 'End Trap' : 'Start Trap' }}</button>
<FocusTrap :disabled="!trapped" :initial-focus="() => inputRef">
<div style="display: flex; flex-direction: column; gap: 1rem; padding-block: 1rem">
<input type="text" placeholder="First input" />
<input ref="inputRef" type="text" placeholder="Second input (initial focus)" />
<textarea placeholder="textarea" />
<button @click="trapped = false">End Trap</button>
</div>
</FocusTrap>
</div>
</template>
19 changes: 19 additions & 0 deletions packages/vue/src/components/focus-trap/focus-trap.stories.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import Basic from './examples/basic.vue'
import InitialFocus from './examples/initial-focus.vue'
import Autofocus from './examples/autofocus.vue'
</script>

<template>
<Story title="Focus Trap">
<Variant title="Basic">
<Basic />
</Variant>
<Variant title="Initial Focus">
<InitialFocus />
</Variant>
<Variant title="Autofocus">
<Autofocus />
</Variant>
</Story>
</template>
99 changes: 99 additions & 0 deletions packages/vue/src/components/focus-trap/focus-trap.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
export interface BaseProps {
/**
* Whether the focus trap is disabled
* @default false
*/
disabled?: boolean
/**
* Function called before focus trap activation to ensure it can be properly trapped
*/
checkCanFocusTrap?: () => Promise<void> | void
/**
* Function called before focus trap deactivation to ensure focus can be returned
*/
checkCanReturnFocus?: () => Promise<void> | void
/**
* Element to focus when trap is activated. By default, focuses on first tabbable element
*/
initialFocus?: HTMLElement | string | (() => HTMLElement) | false
/**
* Element to focus if initialFocus is not found. By default, focuses on container element
*/
fallbackFocus?: HTMLElement | string | (() => HTMLElement)
/**
* Whether to return focus to the element that had focus when trap was activated
* @default true
*/
returnFocusOnDeactivate?: boolean
/**
* Custom element to return focus to when trap is deactivated
*/
setReturnFocus?: HTMLElement | string | (() => HTMLElement) | false
/**
* Whether pressing Escape deactivates the focus trap
* @default true
*/
escapeDeactivates?: boolean
/**
* Whether clicking outside the trap deactivates it
* @default false
*/
clickOutsideDeactivates?: boolean
/**
* Custom handler for clicks outside the trap
*/
allowOutsideClick?: boolean | ((event: MouseEvent) => boolean)
/**
* Whether to prevent scrolling when trap is activated
* @default true
*/
preventScroll?: boolean
/**
* Whether to delay initial focus
* @default true
*/
delayInitialFocus?: boolean
/**
* Custom function to determine forward tab navigation
*/
isKeyForward?: (event: KeyboardEvent) => boolean
/**
* Custom function to determine backward tab navigation
*/
isKeyBackward?: (event: KeyboardEvent) => boolean
}

export interface BaseEmits {
/**
* Function called when the focus trap is activated
*/
activate: []
/**
* Function called after the focus trap is activated
*/
'post-activate': []
/**
* Function called when the focus trap is paused
*/
pause: []
/**
* Function called after the focus trap is paused
*/
'post-pause': []
/**
* Function called when the focus trap is unpaused
*/
unpause: []
/**
* Function called after the focus trap is unpaused
*/
'post-unpause': []
/**
* Function called when the focus trap is deactivated
*/
deactivate: []
/**
* Function called after the focus trap is deactivated
*/
'post-deactivate': []
}
69 changes: 69 additions & 0 deletions packages/vue/src/components/focus-trap/focus-trap.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import type { FocusTrapOptions } from '@zag-js/focus-trap'
import type { HTMLProps, PolymorphicProps } from '../factory'
import type { Assign } from '../../types'
import type { BaseProps, BaseEmits } from './focus-trap.types'
export interface FocusTrapBaseProps extends BaseProps, PolymorphicProps {}
export interface FocusTrapProps
extends FocusTrapBaseProps,
/**
* @vue-ignore
*/
HTMLAttributes {}
export interface FocusTrapEmits extends BaseEmits {}
</script>

<script setup lang="ts">
import { trapFocus } from '@zag-js/focus-trap'
import { watchEffect, ref, onWatcherCleanup, onBeforeUnmount } from 'vue'
import { createSplitProps } from '../create-split-props'
import { ark } from '../factory'
import { useForwardExpose, cleanProps } from '../../utils'
const props = withDefaults(defineProps<FocusTrapProps>(), {
disabled: undefined,
allowOutsideClick: undefined,
returnFocusOnDeactivate: undefined,
escapeDeactivates: undefined,
clickOutsideDeactivates: undefined,
preventScroll: undefined,
delayInitialFocus: undefined,
initialFocus: undefined,
fallbackFocus: undefined,
setReturnFocus: undefined,
checkCanFocusTrap: undefined,
checkCanReturnFocus: undefined,
isKeyForward: undefined,
isKeyBackward: undefined,
})
const emits = defineEmits<BaseEmits>()
const localRef = ref<HTMLDivElement>()
let cleanup: (() => void) | undefined
watchEffect(() => {
if (props.disabled) return
const node = localRef.value?.$el
if (!node) return
const autoFocusNode = node.querySelector<HTMLElement>('[autofocus], [data-autofocus]')
const trapProps = cleanProps(props)
trapProps.initialFocus ||= autoFocusNode ?? undefined
cleanup = trapFocus(node, trapProps)
onWatcherCleanup(() => cleanup?.())
})
onBeforeUnmount(() => {
cleanup?.()
})
useForwardExpose()
</script>

<template>
<ark.div ref="localRef" :as-child="asChild">
<slot />
</ark.div>
</template>
2 changes: 2 additions & 0 deletions packages/vue/src/components/focus-trap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as FocusTrap } from './focus-trap.vue'
export type { FocusTrapBaseProps, FocusTrapProps } from './focus-trap.vue'
1 change: 1 addition & 0 deletions packages/vue/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './factory'
export * from './field'
export * from './fieldset'
export * from './file-upload'
export * from './focus-trap'
export * from './format'
export * from './frame'
export * from './highlight'
Expand Down

0 comments on commit 511d7ab

Please sign in to comment.