-
-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add FocusGesture feature to enhance focus handling in motion co…
…mponents - Introduced FocusGesture to manage focus states and interactions. - Updated FeatureManager to include FocusGesture. - Added focus-related props and types for better integration. - Updated motion state to support focus as a valid state type. - Adjusted package dependencies and versions in package.json for compatibility. - Added tests for focus behavior in motion components.
- Loading branch information
Showing
9 changed files
with
867 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
packages/motion/src/features/gestures/focus/__tests__/focus.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { render } from '@testing-library/vue' | ||
import { motion } from '@/components' | ||
import { defineComponent, nextTick, ref } from 'vue' | ||
import { frame, motionValue } from 'framer-motion/dom' | ||
import { delay } from '@/shared/test' | ||
|
||
describe('focus Gesture', () => { | ||
it('focus applied', async () => { | ||
const promise = new Promise(async (resolve) => { | ||
const opacity = motionValue(1) | ||
const { getByTestId } = render(defineComponent({ | ||
setup() { | ||
return () => ( | ||
<motion.a | ||
data-testid="myAnchorElement" | ||
href="#" | ||
focus={{ opacity: 0.1 }} | ||
transition={{ duration: 0 }} | ||
style={{ opacity }} | ||
> | ||
</motion.a> | ||
) | ||
}, | ||
})) | ||
await nextTick() | ||
const element = getByTestId('myAnchorElement') as HTMLElement | ||
element.matches = () => true | ||
element.focus() | ||
await delay(0) | ||
resolve(opacity.get()) | ||
}) | ||
|
||
return expect(promise).resolves.toBe(0.1) | ||
}) | ||
|
||
it('whileFocus not applied when :focus-visible is false', async () => { | ||
const promise = new Promise(async (resolve) => { | ||
const opacity = motionValue(1) | ||
const { getByTestId } = render(defineComponent({ | ||
setup() { | ||
return () => ( | ||
<motion.a | ||
data-testid="myAnchorElement" | ||
href="#" | ||
focus={{ opacity: 0.1 }} | ||
transition={{ duration: 0 }} | ||
style={{ opacity }} | ||
> | ||
</motion.a> | ||
) | ||
}, | ||
})) | ||
await nextTick() | ||
const element = getByTestId('myAnchorElement') as HTMLElement | ||
element.matches = () => false | ||
element.focus() | ||
await delay(0) | ||
resolve(opacity.get()) | ||
}) | ||
|
||
return expect(promise).resolves.toBe(1) | ||
}) | ||
|
||
it('focus applied if focus-visible selector throws unsupported', async () => { | ||
const promise = new Promise(async (resolve) => { | ||
const opacity = motionValue(1) | ||
const { getByTestId } = render(defineComponent({ | ||
setup() { | ||
return () => ( | ||
<motion.a | ||
data-testid="myAnchorElement" | ||
href="#" | ||
focus={{ opacity: 0.1 }} | ||
transition={{ duration: 0 }} | ||
style={{ opacity }} | ||
> | ||
</motion.a> | ||
) | ||
}, | ||
})) | ||
await nextTick() | ||
const element = getByTestId('myAnchorElement') as HTMLElement | ||
element.matches = () => { | ||
/** | ||
* Explicitly throw as while Jest throws we want to ensure this | ||
* behaviour isn't silently fixed should it fix this in the future. | ||
*/ | ||
throw new Error('this selector not supported') | ||
} | ||
element.focus() | ||
await delay(0) | ||
resolve(opacity.get()) | ||
}) | ||
|
||
return expect(promise).resolves.toBe(0.1) | ||
}) | ||
|
||
it('whileFocus applied as variant', async () => { | ||
const target = 0.5 | ||
const promise = new Promise(async (resolve) => { | ||
const variant = { | ||
hidden: { opacity: target }, | ||
} | ||
const opacity = motionValue(1) | ||
const { getByTestId } = render(defineComponent({ | ||
setup() { | ||
const Aref = ref<HTMLAnchorElement>() | ||
return () => ( | ||
<motion.a | ||
ref={Aref} | ||
data-testid="myAnchorElement" | ||
href="#" | ||
focus="hidden" | ||
variants={variant} | ||
transition={{ type: false }} | ||
style={{ opacity }} | ||
> | ||
</motion.a> | ||
) | ||
}, | ||
})) | ||
await nextTick() | ||
const element = getByTestId('myAnchorElement') as HTMLElement | ||
element.matches = () => true | ||
element.focus() | ||
await delay(0) | ||
resolve(opacity.get()) | ||
}) | ||
|
||
return expect(promise).resolves.toBe(target) | ||
}) | ||
|
||
it('focus is unapplied when blur', () => { | ||
const promise = new Promise(async (resolve) => { | ||
const variant = { | ||
hidden: { opacity: 0.5, transitionEnd: { opacity: 0.75 } }, | ||
} | ||
const opacity = motionValue(1) | ||
|
||
let blurred = false | ||
const onComplete = () => { | ||
frame.postRender(() => blurred && resolve(opacity.get())) | ||
} | ||
|
||
const { getByTestId } = render(defineComponent({ | ||
setup() { | ||
const aRef = ref<HTMLAnchorElement>() | ||
return () => ( | ||
<motion.a | ||
ref={aRef} | ||
data-testid="myAnchorElement" | ||
href="#" | ||
focus="hidden" | ||
variants={variant} | ||
transition={{ type: false }} | ||
style={{ opacity }} | ||
onMotioncomplete={onComplete} | ||
> | ||
</motion.a> | ||
) | ||
}, | ||
})) | ||
|
||
await nextTick() | ||
const element = getByTestId('myAnchorElement') as HTMLElement | ||
element.matches = () => true | ||
element.focus() | ||
await nextTick() | ||
setTimeout(() => { | ||
blurred = true | ||
element.blur() | ||
}, 10) | ||
}) | ||
|
||
return expect(promise).resolves.toBe('1') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { pipe } from 'framer-motion/dom' | ||
import { addDomEvent } from '@/events' | ||
import { Feature } from '@/features/feature' | ||
|
||
export class FocusGesture extends Feature { | ||
private isActive = false | ||
|
||
onFocus() { | ||
let isFocusVisible = false | ||
/** | ||
* If this element doesn't match focus-visible then don't | ||
* apply whileHover. But, if matches throws that focus-visible | ||
* is not a valid selector then in that browser outline styles will be applied | ||
* to the element by default and we want to match that behaviour with whileFocus. | ||
*/ | ||
try { | ||
isFocusVisible = this.state.element.matches(':focus-visible') | ||
} | ||
catch (e) { | ||
isFocusVisible = true | ||
} | ||
if (!isFocusVisible) | ||
return | ||
this.state.setActive('focus', true) | ||
this.isActive = true | ||
} | ||
|
||
onBlur() { | ||
if (!this.isActive) | ||
return | ||
this.state.setActive('focus', false) | ||
this.isActive = false | ||
} | ||
|
||
mount() { | ||
this.unmount = pipe( | ||
addDomEvent(this.state.element!, 'focus', () => this.onFocus()), | ||
addDomEvent(this.state.element!, 'blur', () => this.onBlur()), | ||
) as VoidFunction | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { Variant } from '@/types' | ||
|
||
export type FocusProps = { | ||
focus?: string | Variant | ||
onFocus?: (e: FocusEvent) => void | ||
onBlur?: (e: FocusEvent) => void | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.