Skip to content

Commit

Permalink
chore: init
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Jan 9, 2025
1 parent 18d52db commit 675cf47
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 0 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"@zag-js/dom-query": "0.81.0",
"@zag-js/editable": "0.81.0",
"@zag-js/file-upload": "0.81.0",
"@zag-js/focus-trap": "0.81.0",
"@zag-js/file-utils": "0.81.0",
"@zag-js/highlight-word": "0.81.0",
"@zag-js/hover-card": "0.81.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/react/src/components/focus-trap/examples/autofocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useRef, useState } from 'react'
import { FocusTrap } from '../focus-trap'

export const Autofocus = () => {
const [trapped, setTrapped] = useState(false)
const toggle = () => setTrapped((c) => !c)

const buttonRef = useRef<HTMLButtonElement | null>(null)
const getButtonNode = () => {
const node = buttonRef.current
if (!node) throw new Error('Button not found')
return node
}

return (
<div>
<button ref={buttonRef} onClick={toggle}>
{trapped ? 'End Trap' : 'Start Trap'}
</button>
{trapped && (
<FocusTrap disabled={!trapped} setReturnFocus={getButtonNode}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: '1rem', paddingBlock: '1rem' }}
>
<input type="text" placeholder="Regular input" />
{/* biome-ignore lint/a11y/noAutofocus: <explanation> */}
<input type="text" placeholder="Autofocused input" autoFocus />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
)}
</div>
)
}
20 changes: 20 additions & 0 deletions packages/react/src/components/focus-trap/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useState } from 'react'
import { FocusTrap } from '../focus-trap'

export const Basic = () => {
const [trapped, setTrapped] = useState(false)
return (
<>
<button onClick={() => setTrapped(true)}>Start Trap</button>
<FocusTrap returnFocusOnDeactivate={false} disabled={!trapped}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: '1rem', paddingBlock: '1rem' }}
>
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useState, useRef } from 'react'
import { FocusTrap } from '../focus-trap'

export const InitialFocus = () => {
const [trapped, setTrapped] = useState(false)
const toggle = () => setTrapped((c) => !c)

const inputRef = useRef<HTMLInputElement>(null)

return (
<div>
<button onClick={toggle}>{trapped ? 'End Trap' : 'Start Trap'}</button>
<FocusTrap disabled={!trapped} initialFocus={() => inputRef.current}>
<div
style={{ display: 'flex', flexDirection: 'column', gap: '1rem', paddingBlock: '1rem' }}
>
<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>
</div>
</FocusTrap>
</div>
)
}
11 changes: 11 additions & 0 deletions packages/react/src/components/focus-trap/focus-trap.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Meta } from '@storybook/react'

const meta: Meta = {
title: 'Components / Focus Trap',
}

export default meta

export { Basic } from './examples/basic'
export { InitialFocus } from './examples/initial-focus'
export { Autofocus } from './examples/autofocus'
51 changes: 51 additions & 0 deletions packages/react/src/components/focus-trap/focus-trap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { forwardRef, useRef } from 'react'
import { type HTMLProps, type PolymorphicProps, ark } from '../factory'
import { trapFocus, type FocusTrapOptions } from '@zag-js/focus-trap'
import { useSafeLayoutEffect } from '../../utils/use-safe-layout-effect'
import { composeRefs } from '../../utils/compose-refs'
import type { Assign } from '../../types'
import { createSplitProps } from '../../utils/create-split-props'

export interface TrapOptions extends Omit<FocusTrapOptions, 'document' | 'trapStack'> {
disabled?: boolean
}

export interface FocusTrapBaseProps extends PolymorphicProps, TrapOptions {}

export interface FocusTrapProps extends Assign<HTMLProps<'div'>, FocusTrapBaseProps> {}

export const FocusTrap = forwardRef<HTMLDivElement, FocusTrapProps>((props, ref) => {
const localRef = useRef<HTMLDivElement | null>(null)
const [trapProps, localProps] = createSplitProps<TrapOptions>()(props, [
'disabled',
'onActivate',
'onPostActivate',
'onPause',
'onPostPause',
'onUnpause',
'onPostUnpause',
'checkCanFocusTrap',
'onDeactivate',
'onPostDeactivate',
'checkCanReturnFocus',
'initialFocus',
'fallbackFocus',
'returnFocusOnDeactivate',
'setReturnFocus',
'escapeDeactivates',
'clickOutsideDeactivates',
'allowOutsideClick',
'preventScroll',
'delayInitialFocus',
'isKeyForward',
'isKeyBackward',
])

useSafeLayoutEffect(() => {
const node = localRef.current
if (!node || trapProps.disabled) return
return trapFocus(node, trapProps)
}, [ref, trapProps])

return <ark.div ref={composeRefs(localRef, ref)} {...localProps} />
})
2 changes: 2 additions & 0 deletions packages/react/src/components/focus-trap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FocusTrap } from './focus-trap'
export type { FocusTrapBaseProps, FocusTrapProps } from './focus-trap'
1 change: 1 addition & 0 deletions packages/solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"@zag-js/dialog": "0.81.0",
"@zag-js/dom-query": "0.81.0",
"@zag-js/editable": "0.81.0",
"@zag-js/focus-trap": "0.81.0",
"@zag-js/file-upload": "0.81.0",
"@zag-js/file-utils": "0.81.0",
"@zag-js/highlight-word": "0.81.0",
Expand Down
31 changes: 31 additions & 0 deletions packages/solid/src/components/focus-trap/examples/autofocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'

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

return (
<div>
<button ref={buttonRef} onClick={() => setTrapped((v) => !v)}>
{trapped() ? 'End Trap' : 'Start Trap'}
</button>
{trapped() && (
<FocusTrap disabled={!trapped()} setReturnFocus={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 onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
)}
</div>
)
}
26 changes: 26 additions & 0 deletions packages/solid/src/components/focus-trap/examples/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'

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

return (
<>
<button onClick={() => setTrapped(true)}>Start Trap</button>
<FocusTrap returnFocusOnDeactivate={false} disabled={!trapped()}>
<div
style={{
display: 'flex',
'flex-direction': 'column',
gap: '1rem',
'padding-block': '1rem',
}}
>
<input type="text" placeholder="input" />
<textarea placeholder="textarea" />
<button onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FocusTrap } from '@ark-ui/solid/focus-trap'
import { createSignal } from 'solid-js'

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

return (
<div>
<button onClick={() => setTrapped((v) => !v)}>{trapped() ? 'End Trap' : 'Start Trap'}</button>
<FocusTrap disabled={!trapped()} initialFocus={() => 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 onClick={() => setTrapped(false)}>End Trap</button>
</div>
</FocusTrap>
</div>
)
}
11 changes: 11 additions & 0 deletions packages/solid/src/components/focus-trap/focus-trap.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Meta } from 'storybook-solidjs'

const meta: Meta = {
title: 'Components / Focus Trap',
}

export default meta

export { Basic } from './examples/basic'
export { InitialFocus } from './examples/initial-focus'
export { Autofocus } from './examples/autofocus'
49 changes: 49 additions & 0 deletions packages/solid/src/components/focus-trap/focus-trap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { type FocusTrapOptions, trapFocus } from '@zag-js/focus-trap'
import type { Assign } from '../../types'
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
}

export interface FocusTrapBaseProps extends PolymorphicProps<'div'>, TrapOptions {}
export interface FocusTrapProps extends Assign<HTMLProps<'div'>, FocusTrapBaseProps> {}

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

const [trapProps, localProps] = createSplitProps<TrapOptions>()(props, [
'disabled',
'onActivate',
'onPostActivate',
'onPause',
'onPostPause',
'onUnpause',
'onPostUnpause',
'checkCanFocusTrap',
'onDeactivate',
'onPostDeactivate',
'checkCanReturnFocus',
'initialFocus',
'fallbackFocus',
'returnFocusOnDeactivate',
'setReturnFocus',
'escapeDeactivates',
'clickOutsideDeactivates',
'allowOutsideClick',
'preventScroll',
'delayInitialFocus',
'isKeyForward',
'isKeyBackward',
])

createEffect(() => {
if (!localRef || trapProps.disabled) return
return trapFocus(localRef, trapProps)
})

return <ark.div ref={composeRefs(localRef, props.ref)} {...localProps} />
}
2 changes: 2 additions & 0 deletions packages/solid/src/components/focus-trap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FocusTrap } from './focus-trap'
export type { FocusTrapBaseProps, FocusTrapProps } from './focus-trap'
1 change: 1 addition & 0 deletions packages/solid/src/components/index.tsx
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 675cf47

Please sign in to comment.